DEV Community

Cover image for Unit vs Integration vs E2E Tests
Alberto Yanes
Alberto Yanes

Posted on • Edited on

Unit vs Integration vs E2E Tests

Many of us have surely heard some of these terms in our life cycle as programmers. Our daily life consists of writing code, new functionalities, and requirements, launching to production, and waiting for good news that no problem happened with the new code. There are many ways to achieve that peace of mind that everything will work well, or at least, that what is not related to the new functionality will not be damaged, one of the most effective is to perform tests on our lines, files, and components that are important to the product.

Regardless of the testing method, pattern, or architecture you choose, the idea of ​​doing it is to be sure that the code delivery is correct, sleep peacefully and have a certain degree of confidence that the PR that you merged 5 minutes ago will not generate possible bugs, or simply be sure of having analyzed all possible spaces/fronts where an error could be generated.

For example, let's look at the following feature request:

Our friend Carl, the Product Manager πŸ‘·, asks us to make a button that generates a certain action. It sounds easy, right? But what if you forgot to take the correct action or tomorrow a coworker accidentally changes the aesthetics and now instead of a button it looks like a giant unintelligible box? (Believe me, some of you will have gone through something similar for sure 😜)

This is what I mean by being sure of your code for the small, medium, and possibly long term.

For each test method, the examples will have as a reference this little module of SumCalculator made in React.

const sum = (a, b) => a + b;

const SumCalculator = () => {
  const handleSubmit = (e) => {
    e.preventDefault();
    const [foo, bar] = e.target.elements;
    const fooValue = parseInt(foo.value);
    const barValue = parseInt(bar.value);

    const result = sum(fooValue, barValue);
    alert(result);
  };

  return (
    <div>
      <h1>Calculator Sum Module</h1>
      <form onSubmit={handleSubmit}>
        <label htmlFor="fooInput">Foo</label>
        <input type="number" id="fooInput" />

        <label htmlFor="barInput">Bar</label>
        <input type="number" id="barInput" />

        <button>Submit</button>
      </form>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Unit Testing

One of the most classic techniques of our era is unit testing, the concept is quite simple and straightforward, the idea is to isolate the code as much as possible to be able to perform a unit test in a simple, fast, and efficient way.

What can unit tests be applied to? in theory, any piece of code could apply it, some class, function, line of code, component, you name it! But remember: the smaller the chunk of code, the better.

This form of testing is one of the most essential tools for any developer, generally, in whatever development life cycle we are in, we should consider unit testing. It brings us great advantages such as making sure to fragment our code as much as possible to facilitate the use of the technique, if it becomes complicated, we know that we will have to give some small adjustments to the code to be able to isolate it as much as possible.

test("render all elements", () => {
  render(<Calculator />);

  // check if all the elements are rendered
  expect(screen.getByText(/calculator sum module/i)).toBeInTheDocument();
  expect(screen.getByLabelText(/foo/i)).toBeInTheDocument();
  expect(screen.getByLabelText(/bar/i)).toBeInTheDocument();
  expect(screen.getByRole("button", { name: /submit/i })).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

Integration Testing

One of my favorites and extremely important. This technique is responsible for joining and combining parts of our application that are part of a flow and making sure that the interaction of the parts of our component is correct, allowing us to perform at the time of developing the tests if the interactions with the different pieces are correct.

It is one of the perfect unit testings complements since this method allows us to test the entire flows of the application.

window.alert = jest.fn();

test("should render alert", () => {
  render(<Calculator />);

  // fill out the form
  fireEvent.change(screen.getByLabelText(/foo/i), {
    target: { value: 5 },
  });
  fireEvent.change(screen.getByLabelText(/bar/i), {
    target: { value: 5 },
  });

  // submit the form
  fireEvent.click(screen.getByRole("button", { name: /submit/i }));
  expect(window.alert).toHaveBeenCalledWith(10);
});
Enter fullscreen mode Exit fullscreen mode

End to end testing

Finally, the idea is to test and imitate behaviors that a user would have using our application, interacting with all the possible functionalities from the beginning to the end.

By adding this testing layer to our application, we will make sure to cover possible human interactions that our application may have, preventing bugs due to it.

Be very careful to confuse end to end with integration. Something that I have seen is that we usually mix these two concepts, although the idea is to test application flows, we can easily differentiate one and the other in that, end to end they run in the browser, unlike integration.

// Here I'm using Cypress for e2e testing very friendly for JS developers
describe("...", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("render all elements", () => {
    cy.findByText(/calculator sum module/i).should("exist");
    cy.findByLabelText(/foo/i).should("exist");
    cy.findByLabelText(/bar/i).should("exist");
    cy.findByRole("button", { name: /submit/i }).should("exist");
  });

  it("should render alert", () => {
    const stub = cy.stub();
    cy.on("window:alert", stub);

    cy.log("fill out the form");
    cy.findByLabelText(/foo/i).clear().type(5);
    cy.findByLabelText(/bar/i).clear().type(5);

    cy.log("submit the form");
    cy.findByRole("button", { name: /submit/i }).click();

    cy.log("alert should be called with 10");
    cy.on("window:alert", (txt) => {
      // Mocha assertions
      expect(txt).to.contains("10");
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Finally, this image is very useful to get an idea of ​​what are the considerations to have when we use each type of test:

unit-integration-tests

The more you scale, the more expensive the maintenance and development of the tests will be, also, it will be slower since it requires greater requirements to be able to build them.

Conclusion

Regardless of the type of test we choose, or we want to combine them, the important thing is to have confidence and certainty that what we have done is safe and that it meets the requirements of said functionality.

Implementing any type of test provides us with great benefits to our project, it not only generates trust, but it also serves as a code documentation base, helps us identify possible bugs as we generate the code, and many other benefits.

What has been the type of test that has impacted you the most at work? Do you apply any methodology? How does your work team agree to contemplate this practice in the flow of the application? Leave us a comment!

Follow me on LinkedIn or Twitter to up-to-date with my publications πŸš€.

Top comments (0)