A Practical Guide to Frontend Testing

Automated testing isn’t as common on the frontend as it is on the backend, and there are many opinions out there about it. Some people say you should have tests for every line of code you write. Others argue it doesn't provide enough value and that it's better to focus on monitoring and fixing bugs quickly when they happen.

I sit somewhere in the middle. Personally, I like my pull requests to include tests that cover the behavior I'm implementing—it just feels right. But I also believe we need to be cautious when writing automated tests. You don’t want them to get in your way when you need to change your code. Tests should be reliable and easy to maintain—otherwise, they become a burden.

In this post, I’ll go over the different types of tests you can write, the tools that are commonly used for each type, and the trade-offs you should consider when choosing them. For every type of test, I’ve also included a practical example to help you grasp the concept more easily. I hope it helps!

Unit testing

These are tests focused on small units of your application. Tests that run against a module or a component to ensure the logic written in there works as expected.

Examples: a function that receives a list of prices and returns its total price, or a Badge component that receives text and generates a beautifully styled badge.

Tools: jest/vitest + react-testing-library or cypress with component testing. Please check the Tools and Trade-offs section to help you determine which one to pick.

// Jest test
describe("calculateTotal", () => {
  it("should return the total sum of the prices", () => {
    const prices = [10, 20, 30];
    const result = calculateTotal(prices);

    expect(result).toBe(60);
  });
});

Integration Testing

With integration tests, we want to be sure that different modules or components work well together. Since we want to focus on the communication between components, we don't need to cover all the edge cases each module implement—we leave that for the unit tests. Covering the main scenarios is good enough.

Examples: a success or error message being displayed after a form is submitted, or a new item becoming visible in the shopping cart when the "add to cart" button is clicked.

Tools: jest/vitest + react-testing-library or cypress with component testing. Please check the Tools and Trade-offs section to help you determine which one to pick.

import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";

describe("ShoppingCart Integration Test", () => {
  it('should display item in the cart after clicking "Add to Cart"', () => {
    // Render both ProductCard and ShoppingCart components 
    render(
      <>
        <ProductCard product={{ id: 1, name: "Black shoes", price: 100 }} />
        <ShoppingCart />
      </>
    );

    // Click on the add to cart button
    userEvent.click(getByRole("button", { name: /add to cart/i }));

    // Assert the added product is on the shopping cart list
    expect(
      within(screen.getByTestId('shopping-cart')).getByText("Black shoes"))
    .toBeInTheDocument();
  });
});

Worth mentioning that depending on what you are trying to test, setting it up can get a bit complex. Since now modules are connected to each other, you might have to mock some dependencies here and there to make it work. It is important to keep in mind that sometimes the effort to set up and maintain the test doesn't justify the value it brings. Also, it might be that the complexity of testing it is coming from the way your code is designed or how your application is architected.

End-to-End (E2E) Testing

End-to-end tests are written to ensure the full user's journey works as expected. It is not about rendering a component or two anymore. Our tests are now going to spin up the whole application and navigate and perform actions in the browser as if they were real users. The main goal here is to make sure critical business operations are still going to run smoothly after the deploy of our code changes. We do that because We want to avoid introducing bugs that could impact the business. I mean, we want to make sure the company is still making money so they can pay our salary at the end of the month, right?

Examples: For an e-commerce application, an example would be making sure the user is able to place orders. Or, for a job board application, a user being able to apply for a vacancy.

Tools: Cypress or Playwright.

describe("End-to-End Test for Job Application Submission", () => {
  it("should allow a user to apply for a job and display a confirmation message", () => {
    // Visit the job listing page
    cy.visit("/job-listings");

    // Click on the "Apply" button for a specific job vacancy
    cy.get('[data-testid="apply-button-123"]').click(); // assuming job ID is 123

    // Fill out the application form
    cy.get('[data-testid="first-name-input"]').type("John");
    cy.get('[data-testid="last-name-input"]').type("Doe");
    cy.get('[data-testid="email-input"]').type("john.doe@example.com");
    cy.get('[data-testid="cover-letter-input"]').type(
      "I am very interested in this job because..."
    );

    // Submit the application
    cy.get('[data-testid="submit-application-button"]').click();

    // Check if the confirmation message appears
    cy.contains("Your application has been submitted successfully!").should(
      "be.visible"
    );
  });
});

Keep in mind that E2E tests are harder to set up, especially if the architecture of your application is complex and has lots of external dependencies. You need to have a proper infrastructure set up on your CI so your tests are reliable and can run as fast as possible.

Tools and Trade-offs

We’ve gone through the types of tests, examples, and recommended tools—but you shouldn’t just pick one at random. It’s important to understand the trade-offs first. Below are some key considerations to keep in mind when choosing the right testing tool for the job:

Jest or Vitest

Jest and Vitest run in a Node environment, meaning your components and functions are not actually executed in a real browser.

  • (+) This makes them quite fast and ideal for unit and integration tests.

  • (-) However, not all browser APIs are fully implemented in this environment, which limits what can be tested accurately and might lead to some unexpected errors on your test suite.

React Testing Library

This library helps you test your React components from the perspective of the user, focusing on behavior rather than implementation details. It’s commonly used with Jest or Vitest.

  • (+) Encourages good testing practices.
  • (+) Accessibility test comes for free when using the right query.
  • (-) Still subject to the limitations of the Node environment when running with Jest/Vitest.

Cypress (Component Testing)

You can also use Cypress to test components, not just end-to-end flows.

  • (+) These tests run in a real browser, giving you more confidence.
  • (-) They’re slower to run because they require spinning up a browser every time.

Cypress or Playwright (End to end Testing)

E2E tests with these tools simulate real user behavior by running in an actual browser and interacting with your full app.

  • (+) These tests run in a real browser, giving you more confidence.
  • (-) They’re slower to run because they require spinning up a browser every time.

Now, Now it’s your turn...

Alright, I think I’ve shared enough to help you get started with frontend testing. Just keep in mind that you shouldn't write tests because someone told you so. It needs to make sense. It needs to be simple and bring confidence to your team that what you built works.

If you’d like to see more real-world examples, I’ve put together a repo: frontend-automated-tests-example. In it, you’ll find open PRs where features are implemented step by step, each one covered with automated tests.

That's it for today. Good luck—and happy testing!