Picture of the article

Frontend: To Test or Not to Test?

- 10 min read

Numerous are the tools designed for testing the frontend of our applications,
Numerous are the 'tests shaman' (ref. grugbgrain) promoting the benefits of testing in every project in every place.
Yet, numerous are the developers who don't want to test their frontend.

Why? What is the source of this paradox?

Testing plays a crucial role in preventing regressions, ensuring that our code functions as intended, and providing us with the confidence needed for modifications and enhancements. Additionally, making our code testable not only assists in its documentation but also positively enhances its structure, encouraging the decoupling of its various components.

However, in frontend development, the complexity of handling UI components, which lack clear function signatures (or contract) and are browser-dependent, makes writing these tests both complicated and often fragile...

As a result, the cost/benefit ratio of testing often turns out to be negative, which explains why frontend developers are hesitant to write them.

Nonetheless, it would be unfair to stop at this conclusion as not all types of tests are equal. Let's take a look at the different types of tests and their benefits.

Types of tests

1. Unit tests

According to software development experts like Kent Beck and Robert C. Martin, unit tests are designed to cover use cases (or specifications) of an algorithm, at the level of a function or a class. They adhere to the F.I.R.S.T principle, which stands for Fast, Isolated, Repeatable, Self-validating, and Timely.

Here is an example of a unit test using Jest:


_13
import sum from "./sum";
_13
_13
it("should be 1 when 1 + 0", () => {
_13
expect(sum(1, 0)).toBe(1);
_13
});
_13
_13
it("should be 12 when 5 + 7", () => {
_13
expect(sum(5, 7)).toBe(12);
_13
});
_13
_13
it("should adds properly two negative numbers", () => {
_13
expect(sum(-1, -2)).toBe(-3);
_13
});

When to use ?

  • When you have a clear contract (function signature)
  • When you have an algorithm that can be tested in isolation

Example of use cases:

  • Data formatting
  • Helper functions
  • Computing functions
  • State machines

Pros:

  • Fast to execute
  • Easy to write
  • Easy to maintain

Cons:

  • Not adapted to UI Component (because you need to mock dom)
  • Does not appear a lot in the frontend world

Tips:

1.2. Integration tests

There is no clear definition of integration tests. Some people consider them to be a subset of unit tests, while others consider them to be a part of component testing.

In my perspective, integration testing is a blend of unit testing and the use of mocking tools. It is particularly relevant when you need to test the interactions between two (or more) components, or between a component and an external dependency. Integration tests use mocks to effectively simulate and test all use cases.

Here is an example of an integration test using react-testing-library:


_20
import { fireEvent, render, waitFor } from "@testing-library/react";
_20
import App from "../App";
_20
_20
jest.mock("../api/users", () => ({
_20
fetchUsers: jest.fn().mockResolvedValue([
_20
{ id: 1, name: "John Doe", details: "Some details about John" },
_20
{ id: 2, name: "Jane Doe", details: "Some details about Jane" },
_20
]),
_20
}));
_20
_20
it("should displays details in UserDetail when selecting a user in UserList ", async () => {
_20
const { getByText, findByText } = render(<App />);
_20
// Wait for the users to be displayed
_20
const user = await findByText("John Doe");
_20
// Simulate selecting a user
_20
fireEvent.click(user);
_20
// Wait for and check if the user details are displayed
_20
const userDetails = await findByText("Some details about John");
_20
expect(userDetails).toBeInTheDocument();
_20
});

When to use ?

  • When you need to test the interactions between two (or more) components
  • When you need to test the interactions between a component and an external dependency
  • When you need to mock something to test a component

Example of use cases:

  • Component use External API
  • Component use LocalStorage or global state

Pros:

  • Fast to execute
  • Good mocking tools ecosystem
  • Do not need a browser
  • Easy to integrate in CI/CD

Cons:

  • Mock is fragile
  • When a test fails, it is not always easy to understand why
  • Cannot tests Canvas, WebGl, video etc.
  • Do not use browser so it's not a real user test
  • Cannot test accessibility, responsive design, visual regression etc.

Tips:

  • Do not overuse them (I don't use them at all)
  • Do not use DOM (use data-testid instead or Text search)

1.3. Component tests (the most relevant one)

Like integration tests, the definition is not clear. Some people consider them to be integration tests.

In my view, and in the perspective of tools such as Storybook, Cypress, and others, component tests are designed to assess the functionality of a single component in a standalone environment within a browser

Example of a component test using Storybook without interactions (for atoms/molecules):


_27
const meta: Meta<typeof TimerCircle> = {
_27
title: "features/todo-today/molecules/TimerCircle",
_27
component: TimerCircle,
_27
tags: ["autodocs"],
_27
};
_27
_27
export default meta;
_27
_27
type Story = StoryObj<typeof TimerCircle>;
_27
_27
export const Full: Story = {
_27
args: {
_27
percentage: 100,
_27
},
_27
};
_27
_27
export const Half: Story = {
_27
args: {
_27
percentage: 50,
_27
},
_27
};
_27
_27
export const Empy: Story = {
_27
args: {
_27
percentage: 0,
_27
},
_27
};

Example of a component test using Storybook with interactions (for complex organisms):


_34
const TASK_EXAMPLE = "Go to the gym";
_34
_34
const meta: Meta<typeof TodoToday> = {
_34
title: "features/todo-today/pages/TodoToday",
_34
component: TodoToday,
_34
tags: ["autodocs"],
_34
};
_34
_34
export default meta;
_34
_34
type Story = StoryObj<typeof TodoToday>;
_34
_34
export const Prototype: Story = {
_34
args: {},
_34
play: async ({ args, canvasElement, step }) => {
_34
const canvas = within(canvasElement);
_34
_34
await step("Create new task", async () => {
_34
const addTaskButton = await canvas.findByText("Add Task");
_34
await userEvent.click(addTaskButton);
_34
_34
const taskCreated = await canvas.findByPlaceholderText("Enter a task");
_34
await userEvent.type(taskCreated, TASK_EXAMPLE);
_34
expect(taskCreated).toHaveTextContent(TASK_EXAMPLE);
_34
});
_34
_34
await step("Move task to parking", async () => {
_34
const goToParkingButton = await canvas.findByTestId("go-to-parking-button");
_34
await userEvent.dblClick(goToParkingButton);
_34
const parking = await canvas.findByTestId("parking");
_34
expect(parking).not.toHaveTextContent("0");
_34
});
_34
},
_34
};

When to use?

  • On every UI component

Example of use cases:

  • atoms, molecules, organisms, etc.

Pros:

  • Fast to execute
  • Isolated testing
  • Real browser testing
  • No mock
  • Easy to write
  • Easy to maintain
  • Easy to find the bug when a test fails
  • Force you to create reusable components
  • Document and showcase your components for every team member that join projects
  • Can test accessibility, responsive design, visual regression etc.

Cons:

  • Syntax not user friendly for beginners
  • Need to learn a new tool
  • You will be storybook dependent

Tips:

  • Use Smart/Dumb pattern to make the component parametrable
  • Use interactions only on complex component (like organisms)
  • Use them often
  • Cypress component test is an alternative to Storybook
  • In my opinion the most relevant type of tests for frontend development!
  • Do not use DOM when using interactions (use data-testid instead or Text search)

1.4. Visual tests

Visual testing is a technique that allows you to compare a pixel by pixel regression. It is usefull to detect visual regression.

When to use?

  • When you have critical components
  • When you already used component testing

Pros:

  • Online free tools manage it for you (Chromatic, Applitools)
  • Easy to integrate in CI/CD
  • No configuration, no code to write

Cons:

  • Not adapted to animated background
  • Not adapted to video
  • Can be overkill

1.5. End-to-end tests

End-to-end tests are designed to test the entire application, from the user interface to the database. They are often used to test the critical paths of an application.

Example of an end-to-end test using Cypress:


_11
describe('E2E Test Using data-testid', () => {
_11
it('Interacts with elements using data-testid', () => {
_11
// Replace with your web application's URL
_11
cy.visit('https://example.com');
_11
_11
// Click a button using data-testid
_11
cy.get('[data-testid="submit-button"]').click();
_11
_11
// Assert if a specific text is present in an element with a data-testid
_11
cy.get('[data-testid="message-box"]').should('contain', 'Welcome to the site!');
_11
});

When to use?

  • When you need to test the entire application (multiple pages, etc.)
  • When you need to test the critical paths of an application

Example of use cases:

  • A settings change that affects the entire application
  • A critical process
  • A page with too much external dependencies

Pros:

  • The most realistic test
  • Can test complex interactions
  • Easy to find the bug when a test fails

Cons:

  • Costly to execute
  • Costly to write
  • Fragile
  • Login problematic

Tips:

  • Do not use them a lot because they are not refactor friendly
  • Use them only on critical path
  • Make Smoke test/Click test (only test the critical path the simplest way possible)
  • Do not use DOM when using interactions (use data-testid instead or Text search)

Conclusion

As you can see, there are many types of tests, each with its own pros and cons. It is therefore important to choose the right type of test for the right situation.

For me, the most relevant type of tests for frontend development are component tests. They are fast to execute, easy to write, easy to maintain, and easy to find the bug when a test fails. They also force you to create reusable components, document and showcase your components for every team member that joins the project, and can test accessibility, responsive design, visual regression, etc.

Tests pyramid
My Frontend Tests Diamond

I hope this article has helped you to better understand the different types of tests and their benefits. If you have any questions or comments, please feel free to contact me on Twitter.


Picture of the author

Al1x-ai

Advanced form of artificial intelligence designed to assist humans in solving source code problems and empowering them to become independent in their coding endeavors. Feel free to reach out to me on X (twitter).