Complete guide to frontend unit testing with Jest

  • Unit testing with Jest allows you to validate frontend functions, components, and services quickly, in isolation, and reliably.
  • The Test Trophy balances static, unit, integration, and E2E testing to maximize confidence without skyrocketing costs.
  • React Testing Library and jest-preset-angular integrate Jest with React and Angular, encouraging tests focused on user behavior.
  • Good practices such as descriptive names, well-used mocks, and controlled coverage turn tests into living documentation of the system.

How to do unit testing on the frontend with Jest

When you start taking frontend development seriously, sooner or later you reach the same point: you need unit tests that give you confidence to touch the code without fear. That's where Jest, along with tools like React Testing Library or jest-preset-angular, become your best allies.

In this guide we'll see, calmly but to the point, how Plan and implement a frontend unit testing strategy using JestWhat kind of tests make sense to run, how to set them up in React, Angular, or TypeScript projects, how to write good tests (without getting bogged down in implementation details), and what tools you have to compare results, work with the DOM, mocks, async, coverage, and much more.

Why do unit tests matter so much in frontend development?

Experienced developers assume something very simple: Your code is going to have errorsIt's not about being more or less smart, but about accepting that without a good battery of tests, any change can break key functionalities... and the user will find out before you do.

How to create dark mode code on a website using CSS variables
Related article:
How to implement JWT authentication in a Node.js API

Unit tests consist of test small pieces of code in isolationFunctions, methods, components, or services. The idea is clear: given a specific input, you expect a specific output. If that holds true today and in the future, the behavior remains stable even if the internal code changes.

Among the most important benefits of having good unit tests are some quite tangible things: Detect bugs early, document system behavior, and facilitate large refactors without turning your life into a Russian roulette of deployments.

Test trophy and types of tests in frontend

In recent years, the so-called “Test Trophy”A modern alternative to the classic testing pyramid. Rather than obsessing over a coverage percentage, this model attempts to balance effort, cost, and level of confidence throughout different types of tests.

The Test Trophy usually considers four levels: static, unit, integration, and end-to-end testingEach one provides a different type of feedback, and it's best to combine them rather than relying on just one type.

Static testing: linters and code analysis

Static tests are the ones that are applied without executing the codeThey're like proofreading a book for spelling mistakes before even reading the story. This is where tools like ESLint for JavaScript or specific rules for TypeScript come in.

  • They verify that the code follows certain rules and standards. (naming, format, prohibited patterns, etc.).
  • They detect common errors before they reach the runtime, such as suspicious uses of variables, dead imports, or incorrectly inferred types.
  • They favor a homogeneous style which facilitates teamwork and maintenance.

In modern TypeScript projects, it's typical to combine ESLint and TS's own compiler to catch both style errors and type problems before even running a test with Jest.

Unit tests: the first line of defense

Unit tests are those performed on the smallest unit of logicA unit of logic can be a pure function, a method, a hook, a small component, or a simple service. In backend, we usually talk about classes and methods; in frontend, the unit can also be a component or a piece of encapsulated logic.

In the JavaScript and TypeScript ecosystem you'll see names like Jest, Mocha, Chai or Vitest, although Jest has become the de facto standard for frontend, mainly thanks to its integration with React Testing Library and present as jest-preset-angular.

It is important to remember that Unit tests do not concern themselves with internal implementationbut of the expected output: given a known input, the function or component must produce a certain result, regardless of how it is written internally.

Integration tests: making sure the pieces communicate with each other

Integration testing focuses on how to combine various modules or componentsYou don't just end up with an isolated function, but with the interaction between services, components, and business logic.

  • They define scenarios where several parties collaborate: a form that calls a service, which in turn transforms data and displays it in the interface.
  • They help detect errors in the boundaries between moduleswhich is where data bugs, types, or API contracts often slip through.
  • In frontend with TypeScript you can rely on Supertest, Cypress, or even tests with Jest that exercise several components and services at once.

In the React world, this “integration” is usually tested with React Testing Library + Jestrendering components that are already used by other internal components and mocked services. In Angular, it's common to build the component with TestBed and use Jest as the rendering engine instead of Karma/Jasmine.

End-to-end (E2E) testing: the complete user journey

E2E tests simulate the actual behavior of a user navigating the application: they open pages, click, fill out forms, and wait for responses from the server side.

  • They practice end-to-end application, from the interface to the API (or a simulated environment very close to production).
  • They validate critical flowssuch as login, purchases, complex forms, or onboarding processes.
  • They are usually implemented with Cypress, Playwright, Puppeteer or Selenium in web environments.

These tests are slower and more fragile, but They will give the highest level of confidence for key functionalities. The trick is to use them wisely, covering only the truly critical paths and relying on unit and integration solutions for the rest.

What exactly is a unit test and what makes it good?

frontend unit testing with Jest

Beyond the labels, many people wonder: What counts as unit testing in frontend development?A single component? A function? An entire page?

A very useful definition comes from classic TDD: A unit test represents a small scenario, functional requirement, or acceptance criterionIt doesn't have to be limited to a single tiny function, but it should be fast, stable, and focused.

Key features of a useful unit test

  • It focuses on a limited unit of behavior: a function, a method, a component, or a small flow.
  • It is fast and isolatedIt does not depend on the network, real databases, or long timers. If there are external dependencies, they are simulated.
  • Provide clear feedbackWhen it fails, you know which requirement is no longer being met, not just that "something" has broken.
  • It serves as living documentationBy reading the test, you understand what that piece of code is expected to do.

In modern frontend development, it makes perfect sense that many of those unit tests are, in reality, small integration tests: a component with its children, a hook that uses a service, etc., as long as they remain fast and reliable.

Jest as a frontend testing framework

Jest has earned his place as predominant testing framework in the JavaScript ecosystemLarge companies like Facebook, Airbnb, Twitter, and Spotify use it to test their React, Angular, and other frontend technologies.

One of its great advantages is that It requires almost no configuration to start up in JavaScript or TypeScript projects, and comes with built-in tools for mocks, spies, assertions, and code cover, avoiding dependence on a thousand additional libraries.

Basic functions: describe, test/it, beforeEach and afterEach

The typical structure of a Jest test file is based on a few key blocks that you will see over and over again:

  • describe(name, fn)This groups related tests under a single description. It helps organize and read the results in the console.
  • test(name, fn) o it(name, fn)Define a specific test. Both functions are equivalent; choose the one you prefer.
  • beforeEach(fn)It is run before each group test. Ideal for configure the initial state, mocks or common instances.
  • afterEach(fn)This command runs after each test. It's useful for cleanups, restoring mocks, or closing simulated connections.

In any unit test, within the function you pass to test o itYou'll always follow more or less the same pattern: you prepare the data, execute the function or render the component, and then use expect(…) to confirm the expected result.

Matchers and comparisons in Jest

The heart of assertions in Jest is function expectwhich is linked with different methods to check different conditions. Some of the most common are:

  • .toBe(value): checks strict equality using Object.isIdeal for numbers, strings, and booleans.
  • .toEqual(obj): performs a recursive comparison of objects and arrays, perfect for complex data structures.
  • .not: reverses the matcher, for example expect(false).not.toBe(true).
  • .toBeDefined(): checks that a value is not undefined.
  • .toContain o expect.arrayContaining: validate that an array or string includes certain elements.

Building on this foundation, Jest adds many other specific features: .toHaveBeenCalled, .toHaveBeenCalledTimes, .toHaveBeenCalledWith for mocks, .toThrow for errors, or extended matchers when you integrate libraries like @testing-library/jest-dom to work with the DOM.

DOM Testing with React Testing Library

When it comes to React components, the most common thing nowadays is to use React Testing Library along with JestThe philosophy of this library is clear: test the components as a user would, leaving aside the internal implementation details.

Instead of checking the internal state of the component, it focuses on consult the DOM through visible text, accessibility role, or associated labels, which, in turn, promotes good accessibility practices.

Types of queries: getBy, queryBy and findBy

The React Testing Library exposes different types of functions for searching for elements in the rendered DOM, usually through the object screen:

  • getBy…: search for an element and It throws an error if it cannot find it.This is what you want when the element must exist for the test to pass.
  • queryBy…: performs the same search but returns null If it doesn't exist, without breaking the test. Useful for checking that something is not present.
  • findBy…: same as getBy, but oriented towards asynchronous operationsIt returns a promise and waits for the element to appear in the DOM after an HTTP request, a timeout, etc.

These variants allow covering both synchronous scenarios (elements that must be there from the beginning) and asynchronous scenarios (content that arrives after an API call or user interaction).

Recommended queries to find elements

The official Testing Library documentation proposes an order of preference for queries, always trying to mimic how a user perceives the interface:

  1. getByRole (with option nameThis is the most semantic query, as it locates elements by their accessible role (button, heading, textbox, etc.). It should be your first choice almost always.
  2. getByLabelText: perfect for form fieldsIt allows you to find inputs and textareas by their associated label, just like a screen reader would.
  3. getByPlaceholderText: useful when there is only one placeholder visible, although it does not replace a proper label.
  4. getByText: for visible content in non-interactive elements: paragraphs, divs, spans, etc.
  5. getByDisplayValue: very practical for pre-filled inputs, checking the value that the user sees.
Excel with Python
Related article:
Excel with Python: Integrating scripts and automating analysis

In addition to these, you have the so-called “semantic queries”:

  1. getByAltText: locates images or other elements with attribute alt, essential for accessibility.
  2. getByTitle: it relies on the attribute titleHowever, this is not always read by screen readers.

Finally, as a last resort, getByTestId, which searches for elements with an attribute data-testid. It is best reserved for cases where there is no other reasonable way to locate the item (dynamic text, purely decorative elements, etc.).

Synchronous vs. asynchronous testing in Jest

In JavaScript, it is becoming increasingly common to work with promises, HTTP calls, and timersAnd your tests need to adapt to that. A test is considered asynchronous when wait for trades that are not resolved in the same tick of the event loop.

A test is clearly synchronous when:

  • It does not use async functions ni await.
  • It does not call asynchronous APIs nor to services that deliver on promises.
  • It does not depend on setTimeout, setInterval or similar.

Instead, it's a test asynchronous when:

  • The test function is marked with async and uses await.
  • It works with functions that return promises (fetch, axios, HTTP services, etc.).
  • In the React Testing Library, use findBy or wait methods to wait for changes in the DOM.

The key is to make sure that Jest Wait patiently for those operations to finish.To do this, you can return a promise from the test, use async/await, or, in older APIs, use a callback. done that Jest injects into the test function.

Creating unit tests with Jest in UI components

To illustrate how real components are tested, imagine you have a button and some more complex components in React. The usual practice is to create files like Button.test.tsx o HomeComponent.spec.ts and use Jest with the appropriate testing library.

A typical first case is verify that the component is rendered with the correct textYou render the button with the React Testing Library, search for the text "Click me" or something similar, and verify that this element is in the document with a matcher like toBeInTheDocument.

Another pattern that is repeated a lot is the use of jest.fn() To create mock functions that act as event handlers or callbacks. This way you can check if they were called when the user clicks, changes an input, or triggers a specific action.

In more complex components, such as forms or modals, it is very common to prepare a handler object with several mocked functions using jest.fn()pass it as props and then use expect(handlers.something).toHaveBeenCalled() to ensure that the event logic is triggered when it should be.

You can even check more elaborate conditions, such as that according to a certain state (for example, a property) currentState (from a container) a button appears with one text or another. In these cases you assemble the component with different initial values ​​and verify that the text or the role of the buttons changes as expected.

Basic Jest configuration in React projects with TypeScript

To use Jest in a React application with TypeScript, the workflow usually begins by ensuring you have Node.js and npm (or yarn) installed and a base React project (e.g., created with Create React App or Vite, adapted to TS).

In an existing project, the usual practice is to install the following development software: jest, @testing-library/react, @testing-library/jest-dom, and Jest types for TypeScriptOnce installed, you can create a configuration file like jest.config.js o jest.config.cjs where you define the environment, the TypeScript transformer, and relevant paths.

At the package.json These scripts are usually added at least: "test": "jest -watch" to run tests in observer mode while you develop, and «test:coverage»: «jest –coverage» To generate coverage reports and see which parts of the code you're not touching with tests, and to integrate them into CI/CD pipelines, you can follow a guide for Configure a CI/CD flow using GitHub Actions.

Jest, by default, can be configured to search for files whose name ends in .test.tsx or .spec.tsx (in the case of React with TypeScript), which greatly simplifies organization: you place the tests next to the components or in a specific tests folder, and Jest takes care of locating them.

Manually integrate Jest into Angular projects

In Angular, historically, tests have been set up with Jasmine and KarmaThis is something many people consider slow and not very user-friendly for CI/CD integrations. Experimental options for using Jest were introduced in Angular 16, but if you want something stable, The manual route remains the most reliable.

In a recent Angular project (for example, created with Angular CLI 17), the typical process for switching to Jest usually follows these steps:

  • Create the project with ng new project-test-jest and the configuration you need.
  • Uninstall Karma and Jasmine with npm, removing packages like karma, jasmine-core and other related departments.
  • Remove from angular.json The Karma-based test configuration section, since you won't be using it.
  • Install Jest and jest-preset-angular with npm install jest jest-preset-angular @types/jest --save-dev.
  • Update the scripts in package.json so that "test" runs Jest instead of ng testand add a "coverage" script that launches jest --coverage.
  • Modify tsconfig.spec.json so that the types change from Jasmine to Jest, indicating "types":

Then you need to create a file jest.config.js at the root of the project, usually relying on jest-preset-angularThere you define things like the test root, the setup file path, module aliases (moduleNameMapper), and which files count towards coverage.

Finally, you add a src/setup-jest.ts that matters 'jest-preset-angular/setup-jest'This prepares the Angular environment to run correctly in Jest, including JSDOM and other features.

Examples of testing in Angular with Jest: components and services

In Angular, component tests typically rely on TestBed to mount the component within a testing module, inject services, and manage the change detection cycle.

For example, imagine a HomeComponent which has properties such as title, count, isVisible y data, and that in its ngOnInit requests data from a SampleService. The beforeEach Typically, I would configure TestBed with the necessary module or imports, register the service, and create the component fixture.

A first, fairly basic test would simply be expect(component).toBeTruthy()to ensure the component is created without errors. Although it may seem trivial, it helps detect configuration problems or broken dependencies.

Then you can test the user interaction with buttons that increment a counter or toggle a Boolean visibility value. With fixture.debugElement.query(By.css('.class')) You find the buttons, you trigger events with triggerEventHandler('click', null) and you verify that the component's properties change as expected after calling fixture.detectChanges().

For Angular services that call real APIs, the pair HttpClientTestingModule and HttpTestingController It is essential. It is configured in a beforeEach, the service and controller are injected, and after each test, it is called. httpMock.verify() to ensure that no requests remain pending.

A typical HTTP service test might subscribe to service.getPokemon('pikachu'), check that HttpTestingController receive a GET request to the correct URL and respond with req.flush(mockPokemon)In the assertions, it is verified that the data returned to the subscriber corresponds to the mock.

Snapshot testing and custom transformers

Jest also allows you to do snapshot testingThis is very common in React components. The idea is simple: you render a component (for example with react-test-renderer), you save its output in a snapshot file and, in future runs, you compare what is rendered with that file.

When the component changes intentionally, you can update the snapshot with a command like jest -uIf the difference was unexpected, the test will fail and force you to check exactly what has changed in the rendered output.

Excel with Python
Related article:
Excel with Python: Integrating scripts and automating analysis

With React 16+ and libraries like Enzyme, there may be console warnings when mocking certain componentsThis is due to how React validates element types. Some shortcuts include rendering as plain text, using custom elements (labels with hyphens and lowercase letters), and relying on... react-test-renderer or, in extreme cases, disable certain warnings in the Jest settings.

If your project requires more advanced code transformation, you can also define custom transformers in Jest. Instead of just using babel-jest, you can resort to @babel/core y babel-preset-jestconfiguring the Jest "transform" key to process files with your own rules.

Best practices when writing unit tests with Jest

To ensure your tests are truly useful and not a burden, it's advisable to follow a few simple guidelines that, over time, become almost instinctive:

  • Keep each test focused on one thing.A specific behavior, a clear scenario. This makes them easier to understand and maintain.
  • Use expressive test descriptionsThey should explain what the functionality does, not just repeat the method name. When a test fails, the message should make sense to you and your team.
  • Perform the tests independently of each other.They should not depend on the order of execution or the state left by the others. Each test should be able to be run in isolation.
  • Take advantage of beforeEach and afterEach to reuse common configurations and clean up resources or mocks after each test.
  • Rely on mocks and spies to isolate external dependencies (APIs, global services) and focus on the logic you really want to validate.
  • Don't forget the edge cases and the mistakesDon't just test the success path; include rare inputs, incorrect types, server errors, and empty states.

Once you launch Jest with cover (for example, using npm run coverage), you will get a directory coverage with a highly visual HTML report showing which files and lines were executed in the tests. Opening coverage/lcov-report/index.html You can navigate file by file, which is ideal for detecting dark areas of your application that should be covered.

Ultimately, this entire ecosystem of Jest, React Testing Library, jest-preset-angular, linters, and coverage tools has one pretty clear goal: that You can change your frontend with confidence, without living in constant fear of breaking the user experience.If your tests are well-focused, reflect how the application is actually used, and are not tied to internal details, they become a lifeline for your projects and a solid foundation on which to calmly evolve your code, even in large teams with frequent deployments.