
Complete Guide to Unit Testing in React with TypeScript: Enhance the Quality of Your Frontend
Introduction
Unit testing is a fundamental part of application development to ensure functionality and prevent errors. In this article, I will guide you through the process of creating unit tests in a React application with TypeScript, using the Jest and React Testing Library tools, it is worth noting that both complement each other.
Jest is a testing framework developed by Facebook. Its goal is to make writing tests easy and effective. Jest provides a complete environment for writing, running, and analyzing tests in JavaScript projects.
React Testing Library is a library that helps test React components. It focuses on how a user interacts with the interface, rather than focusing on the internal implementation of the components. This encourages more realistic and user-centered testing.
Jest and React Testing Library complement each other perfectly. Jest offers the general capabilities for test execution, while React Testing Library focuses on interaction and user experience when testing React components. Together, these tools allow a comprehensive approach to ensure that a React application works correctly both in terms of functionality and usability.
What is the Test Trophy?
The concept of the "Test Trophy" can be applied to different levels of testing in software development, from static tests to end-to-end tests. Each level of testing contributes to winning this "trophy" by ensuring the quality and reliability of the software in different aspects. The testing trophy is a software testing model that focuses on finding the right balance between time investment and confidence return in the software application; this model focuses on writing enough tests, but not necessarily achieving 100% coverage.
Let's see how the "Test Trophy" applies to the different levels:

‍
Static Tests
Static tests are similar to reviewing a book for errors before reading it. Before running the software, the code is analyzed for errors and ensured to be well-written and documented. This is essential to guarantee the quality, reliability, and maintainability of the software.
- In static tests, the code is evaluated without running it to identify possible programming errors.
- It is checked that the code complies with coding standards and best practices.
- It is clearly documented to facilitate understanding.
For static testing, you can use tools like language-specific linters, such as ESLint for JavaScript or TSLint for TypeScript, which will help you find code quality issues and maintain a consistent coding style.
Unit Tests
These tests are performed close to the source of the application and consist of testing individual methods and functions of the classes, components, or modules used by the software. Unit tests are a useful tool to isolate and test specific units of code to determine the effectiveness of each component.
Regarding the technologies used in TypeScript, they are Jest, Mocha, Chai; in Java, JUnit, Mockito, TestNG; and in React, we have React Testing Library, Jest, Enzyme.
Integration Tests
Integration tests focus on verifying how different software components or modules interact when combined. The main goal is to ensure that the individual parts of the software work correctly when integrated to form a larger system.
Winning the "Test Trophy" in integration testing involves:
- Identifying key components and defining integration scenarios.
- Developing and automating specific test cases for scenarios.
- Running tests, managing issues, and continuously improving tests for effective integration.
Regarding the technologies used in TypeScript, they are Supertest, Cypress; in Java, Spring Boot Test, REST Assured, TestContainers; and in React, we have Cypress, Puppeteer.
End-to-End Tests
End-to-end tests evaluate the software as a whole, as an end user would. They ensure that all parts of the system are integrated and work correctly from the beginning to the end of the process.
To win the test trophy, the following features must be considered:
- Test the complete application flow, simulating user actions.
- Verify that all components integrate correctly and work as expected.
- Check that the application responds appropriately to different real-world situations.
For web applications, tools like Cypress, Puppeteer, or Selenium are widely used to perform end-to-end testing.
👉Winning the "Test Trophy" at each level increases the team's confidence in the software quality. Tests, including static, unit, integration, and end-to-end, ensure that the code remains robust and resilient to real-world challenges.
What are Unit Tests?
Unit tests are a fundamental practice in the applications you are creating. The idea is to test if a small part of the code works well on its own, regardless of how it connects with the rest of the code. This helps find errors early in the development process and create more reliable and higher-quality software.
Characteristics of unit tests:
- Focus on the Unit: Unit tests test the smallest part of our code in isolation, such as a function, method, or user interface component. Each unit is evaluated separately to ensure it works correctly in its own world, without considering the rest of the system.
- Isolation and Speed: Unit tests have the advantage of running in an isolated environment, which allows detecting specific problems accurately without affecting other system elements. Also, by focusing on small units, they are fast to execute, providing instant feedback to developers and saving time compared to longer tests.
- Coverage and Maintainability: 'Test coverage' refers to the proportion of our code that has been evaluated by our tests. While higher coverage can provide greater confidence, it does not always guarantee reliability. What really matters is the design of the tests and the quality of the components being evaluated. Coverage alone is not enough to guarantee test quality; we need to ensure the code works correctly. Additionally, unit tests help maintain our code over time. When we make changes to a part of the code, these tests tell us if they affect the expected behavior. This prevents problems in the future and gives us greater control over how the code evolves.
Why should you do unit testing?
Testing is essential because it ensures that our code works according to expectations and maintains quality standards. Tests are a crucial factor to avoid errors in production that could negatively affect the application or reduce its quality.
Despite the time constraints we sometimes face in the tech industry, testing represents a highly valuable investment. In the long run, they simplify the maintenance process and ensure that the code meets our requirements.
There are several reasons for this:
- Early Error Detection: Tests prevent costly errors by identifying them in the early stages of development.
- Facilitation of Rapid Changes: Tests help developers understand the code and implement changes quickly, especially in recent projects.
- Valuable Documentation: Unit tests in software development act as a form of documentation. They explain the purpose and functioning of each component, prevent errors, encourage collaboration, and reduce the time needed to become familiar with the code. Additionally, they identify reusable components and keep documentation up to date, ultimately improving project efficiency and quality.
Environment Setup
Before starting, make sure you have Node.js and npm (or yarn) installed on your system. Then, create a new React project with TypeScript (Optional, depending on the project):
npx create-react-app mi-proyecto --template typescript
cd mi-proyecto
Already created or in the project you are working on, you will need to install Jest and React Testing Library:
Create a jest.config.js file at the root of your project. You can configure Jest according to your needs. Here is an example of a basic configuration with TypeScript:
const config = {
 verbose: true,
 preset: 'ts-jest',
 testEnvironment: 'node',
 moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
 testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)Keep in mind that this configuration is a basic configuration. It can be customized according to the specific needs of the project.
You should integrate the configuration for jest into package.json:
"scripts": {
"test": "jest --watch",
"test:coverage": "jest --coverage",
},
"test": "jest --watch": Runs tests while you work and shows you results instantly.
"test:coverage": "jest --coverage": can help you identify which parts of your code need more tests.
Once installed, to run the test tests we use the following command, it can be alone and it will run all the tests but you can also specify the name of the file you want to run to test them one by one
Jest will automatically look for files matching the pattern *.test.tsx and run the tests defined in them, in this case it is .tsx because we are using TypeScript.
npx test
Jest will automatically look for files matching the pattern *.test.tsx and run the tests defined in them, in this case it is .tsx because we are using TypeScript.
npx test  mi-test.test.tsx
Comparison Methods in Jest and React Testing Library
When writing tests for our React code using Jest and React Testing Library, it is important to know how to compare expected results with the values obtained during testing. In this document, we will see how to make these comparisons effectively.
- .toBe(): This method is used to test exact equality between two values. It uses the Object.is algorithm to determine if two values are equal.
- .toEqual(): Unlike .toBe(), this method performs a recursive comparison of each field in an object or array. It is useful when working with more complex data structures.
Types of queries in Testing library (screen)
- getBy: The getBy function is used to find an element in the rendered DOM component. If the element that meets the criteria is not found, this function will throw an error and the test will fail.
const element = screen.getByText('Text in the element');
- queryBy: The queryBy function is also used to find elements in the DOM. However, unlike getBy, if the element is not found, instead of throwing an error, queryBy simply returns null.
const element = screen.queryByText('Text in the element');
- findBy: The findBy function is used to find an element in the DOM, but with an important difference: it can handle asynchronous searches, such as when waiting for an element to appear after an asynchronous action, like an HTTP request.
const element = await screen.findByText('Text in the element');
Main queries
- getByRole: This query can be used to find all elements that are exposed in the accessibility tree. With the name option, you can filter the returned elements by their accessible name. It should be your primary preference for almost everything.
- getByLabelText: It is very useful for form fields. Users navigate forms using labels. This query emulates that behavior and should be your first choice for form fields.
- getByPlaceholderText: A placeholder does not replace a label, but if that is all you have, it is better than other alternatives.
- getByText: Outside of forms, text content is the main way users find elements. It can be used to find non-interactive elements like divs, spans, and paragraphs.
- getByDisplayValue: The current value of a form element can be useful when navigating a page with filled values.
Semantic Queries
- getByAltText: If the element is one that supports alternative text (img, area, input, and any custom element), this can be used to find that element.
- getByTitle: The title attribute is not consistently read by screen readers and is not visible by default to users with sight.
Test IDs
- getByTestId: The user cannot see them (nor hear them), so it is only recommended in cases where you cannot match by role or text, or it does not make sense (for example, the text is dynamic).
đź’ˇTo better understand these concepts, it is recommended to review the official documentation and watch a video that, in my opinion, was very helpful for understanding unit tests. https://testing-library.com/docs/queries/about/
How to know when a test is async?
In JavaScript, and in the context of unit testing, such as Jest and React Testing Library, a test is considered asynchronous (async) when it involves operations that do not execute immediately, such as calls to functions that return Promises, timers (e.g., setTimeout), or interactions with asynchronous APIs (like network requests).
Synchronous Test (Non-Async):
- Does not use async or await functions.
- Does not make asynchronous calls to APIs, services, or databases.
- Does not use setTimeout or setInterval.
test('Renders Container correctly', () => {
render( );
const userList = screen.getByTestId('user-list');
expect(userList).toBeInTheDocument();
});
Asynchronous Test (Async):
- Uses the async keyword before the test function (async () => {...}).
- Uses await when calling asynchronous functions or waiting for promises.
- Involves calls to functions that return promises.
test('Renders UserListContainer correctly asynchronously', async () => {
 render( );
```js
await waitFor(() => {
  const userList = screen.queryByTestId('user-list');
  expect(userList).toBeInTheDocument();
 }, { timeout: 1000 });
});
Test Creation
- Example 1: Suppose we have a component called Button that we want to test. First, you need to create a file called Button.test.js. Here is an example of what a simple test for the Button component might look like:
import React from 'react';
import { render, screen } from '@testing-library/react';
import Button from './Button';
test('renders button with correct text', () => {
 render();
 const buttonElement = screen.getByText(/click me/i).toBeInTheDocument();
});
In this example, we are using the render and screen.getByText functions from React Testing Library to render the Button component and check if the button element with the text "Click me" is present in the document.
- Example 2: At the start of this test, an object handlers is created containing several mock functions using jest.fn(). Mock functions are used to simulate what happens when an event handler inside the component being tested is called. Each function represents a specific action that the component is expected to perform.
đź’ˇ jest.fn(): It is a tool that allows you to create mock functions that keep track of their calls during tests. This is useful to verify if a function is called correctly or to simulate the behavior of functions in test scenarios.
const handlers = {
onChange: jest.fn(),
onSearchContainer: jest.fn(),
onClear: jest.fn(),
onCheckpointChange: jest.fn()
};
Suppose we need to test whether there is a title in the EventExceptionForm component or not; the test function can be started with test or it.
test('renders the title correctly and checks if handlers are called', () => {
 render( );
 expect(screen.getByText('Evento/Excepción')).toBeInTheDocument();
 fireEvent.click(screen.getByText('Button that calls the handler'));
 // Verify that the handlers have been called
 expect(handlers.onChange).toHaveBeenCalled();
 expect(handlers.onSearchContainer).toHaveBeenCalled();
 expect(handlers.onClear).toHaveBeenCalled();
 expect(handlers.onCheckpointChange).toHaveBeenCalled();
});
- Renders the EventExceptionForm component with an empty list of checkpoints and the provided handlers. The use of ... in this case is to pass all the properties of the handlers object to the EventExceptionForm component in JavaScript. If the object contains event handler functions, these functions will be passed to the component so it can interact with them.
- Looks for an element on the screen that contains the text 'Evento/ExcepciĂłn'.
- expect(title).toBeInTheDocument(); uses Jest to verify if the title element is in the document (i.e., if it has rendered correctly). If it is not found, the test will fail.
Then, multiple expect assertions are used to verify that the handlers have been called. This is done with the following lines:
- expect(handlers.onChange).toHaveBeenCalled(): Checks if the onChange handler has been called at least once.
- expect(handlers.onSearchContainer).toHaveBeenCalled(): Checks if the onSearchContainer handler has been called at least once.
- expect(handlers.onClear).toHaveBeenCalled(): Checks if the onClear handler has been called at least once.
- expect(handlers.onCheckpointChange).toHaveBeenCalled(): Checks if the onCheckpointChange handler has been called at least once.
‍
- Example 3:
it('The button renders and calls the clearForm function on click', () => {
 const clearForm = jest.fn();
 render(
 Â
 );
 const buttonElement = screen.getByRole('button', { name: 'New interpinchazo process' });
 expect(buttonElement).toBeInTheDocument();
 expect(buttonElement).not.toBeDisabled();
 fireEvent.click(buttonElement);
 expect(clearForm).toHaveBeenCalledTimes(1);
});
- A mock function clearForm is created using jest.fn(). This mock function will be used as the onClick event handler for the button.
- The Button component is rendered; in this case, it is passed the text "New interpinchazo process," the type is set to "button," and clearForm is assigned as the onClick event handler.
- screen.getByRole is used to find a button element with the name "New interpinchazo process." Then it verifies that the button is present in the document and is not disabled.
- fireEvent.click(buttonElement) simulates a click on the button.
- Finally, it verifies that the clearForm function has been called exactly once (toHaveBeenCalledTimes(1)) after clicking the button.
‍
- Example 4: The modal containing this component is opened, and it has a condition depending on the state it is in. The component we are going to test is Item. It is a modal that receives the container property, for which we will have a mocked container to perform the corresponding tests. We will verify that depending on the state (currentState) the container is in, a button with different text will be shown.
const container = {
  codeContainer: '7654152698',
  currentState: 'created',
}
it('Render the correct button based on the state', () => {
  const { getByText } = render( );
const buttonText =
 container.currentState === 'sealed'
  ? 'Audit Container'
  : container.currentState === 'onaudit'
  ? 'Change state'
  : container.currentState === 'disabled'
  ? 'Go to Container'
  : 'Load Container';
  const button = getByText(buttonText);
  expect(button).toBeInTheDocument();
 });
```
The expression uses multiple ternary operators (? :) to determine the value of **buttonText** based on the container's state (container.currentState).
This code performs a test to confirm that the button displayed in the user interface matches the current state of a container. It ensures that the interface shows the appropriate button for the container's state.
Conclusion
Unit tests are an essential part of the software development process. They allow us to identify and fix issues in our components and functions before they reach production. By using Jest and React Testing Library, I can create effective unit tests for my React applications. I understand that writing solid tests not only improves the quality of my code but also facilitates the maintenance and evolution of my project.
References
‍https://www.youtube.com/watch?v=QdqIqGPsLW0
‍https://www.youtube.com/watch?v=bTGil8qPmXo
Ready to improve the quality and reliability of your frontend with effective unit tests?
At Kranio, we have testing experts who will help you implement unit testing strategies using tools like Jest and React Testing Library, ensuring your React applications work correctly and provide an excellent user experience. Contact us and discover how we can help you elevate the quality of your frontend software.
, Â transform: { Â Â '^.+\\.(ts|tsx)

