The test types you need | neoco

/the-test-types-you-need

The test types you need

Joan Antoni Morey

Joan Antoni Morey

8 min

09/05/2022

There are many types of tests, but which ones should you start with?

Choosing what to start with is important, since you may be choosing an efficient and progressive path or, on the other hand, having to make a great effort to learn much more than you will need in practice.

It is important to be familiar with the concept of "Test" and why you should start testing (if you don't already). If these are new concepts for you, I recommend that you read my previous article first Why you should start testing


From lowest to highest

As in many aspects of life and programming, it is better to go from lowest to highest. It is more worthwhile to start with small tests that deal with small and/or simple functions.

That said, we are going to see the lowest level tests that deal with very small pieces of code, the so-called unit tests.


Unit tests

They are tests that evaluate specific and isolated functions, although there is no maximum complexity that a function must have in order to be tested with this type of test.

It is good practice to fragment the code into small blocks and dedicate tests to each fragment. This makes it much easier and faster to detect if there is a bug and where in the code it originates. Otherwise, if you have functions that are too large and/or complex, you may only know that something is causing the entire function to fail when it fails. As they say, divide and conquer.

This is an example of a unit test in JavaScript:

getNameById
function getNameById(users, id) {
  const user = users.find((user) => user.id === id);

  return user.name;
}
test
describe("given an array of users", () => {
  describe("given the 'id' of the desired user", () => {
    it("should return the name of that user", () => {
      const users = [
        { id: 1, name: "Emily" },
        { id: 2, name: "John" },
      ];
      expect(getNameById(users, 2)).toEqual("John");
    });
  });
});

As we can see, the getNameById function is quite simple and direct, so we can quickly implement a test to verify if it works properly. This way we can understand every little part of our project and build more and more confidence.

Although at first glance they may seem trivial, simple and obvious tests, they can become very complex when developing and contemplating many use cases. An example of this, with getNameById, would be the case where you only pass it one argument instead of the two it expects, or if you pass it an empty list of users, or a list of users without the id property... That is when these tests begin to gain importance.

For example, if we execute this other test, the following error is thrown:

describe("given an empty array and the 'id' of the desired user", () => {
  it("should return the name of that user", () => {
    expect(getNameById([], 2)).toEqual("John");
  });
});

TypeError: Cannot read properties of undefined (reading 'name')

So we have to modify the function so that it doesn't throw an error when it receives an empty collection of elements.

This is the result:

function getNameById(users, id) {
  const user = users?.find((user) => user.id === id);

  return user?.name;
}
describe("given an empty array and the 'id' of the desired user", () => {
  it("should return the name of that user", () => {
    expect(getNameById([], 2)).toEqual(undefined);
  });
});
The issue has been solved with optional chaining.

E2E tests (End to End)

Unlike unit tests, E2E tests deal with feature sets. More specifically, they check if the user's interactions with the app are and will be as expected. They need to render full screens of the app, as a user would see them, and interact with the elements.

An example of an E2E test that cannot be missed is the one that verifies that a user can log in. To do this, the test renders the form screen, fills in the fields with the appropriate information and presses the "Login" button. If everything is correct, the test will be successful. If not, one or more things may have failed. It may be that the "Login" button is off the screen, so a user would not be able to press it and the flow would be broken.

If so, it is infinitely better to detect it in an E2E test than to stop having users in the app for the simple fact that a button is not visible to press it.

In the past it was tedious to be able to implement E2E tests, but today there are very advanced libraries that greatly improve the development experience. In JavaScript, for example, we have the library Cypress.js.

This is what the code of an interaction test with forms in Cypress.js would look like:

describe("Form Interactions", function () {
  beforeEach(function () {
    cy.viewport(400, 300);
    cy.visit("/index.html");
  });

  it("updates range value when moving slider", function () {
    cy.get("input[type=range]").as("range").invoke("val", 25).trigger("change");

    cy.get("@range").siblings("p").should("have.text", "25");
  });
});

When you run the test suite, it opens a browser window in which you will render and run each test. It is very intuitive, visual and interactive.

Example taken from the Cypress.js documentation

Why do E2E tests?

The main reason is to maintain the proper functioning of the app flows, throughout the life of the project and not just in a specific phase. Not only does the user experience remain intact (or nearly so) even though the app goes through different stages and evolutions, but the development experience also benefits.

As with unit tests, the more tests there are, the more you cover your back when you need to modify part of the code or implement something new. Besides, in the E2E tests, there is another important factor that tips the scales to a YES with capital letters; capture the user requirements in the same test code.

It may seem like something that is clearly a point in favor, but it is not a big deal either. Well, it turns out to be very practical when it comes to knowing why the code is so complex, being able to make it simpler, and it is a way of taking into account all the main use cases that each screen of the app has to satisfy. It also helps to formulate questions about possible requirements that may not have been taken into account so far or simply nobody has compiled the user requirements, which can become critical when the day of the app's presentation.

Small comparison

This type of test requires more execution time since, as we mentioned, each one of them has to render screens. This is definitely much slower than unit tests. Even so, they are completely complementary to unit tests and, in my opinion, essential for an app without broken flows and, therefore, consistent over time.


Conclusion

Start little by little with the types of tests, since with unit tests and E2E you can reach a very high coverage and safely guarantee the correct functioning and integration of your code.