Complete Guide to Unit Testing in React with TypeScript: Improve the Quality of Your Frontend

introduction

Unit tests are a fundamental part of developing applications to ensure their 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 should be noted 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, executing, 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 components. This encourages more realistic, user-focused testing.

Jest and React Testing Library they complement each other perfectly. Jest offers the general capabilities for running tests, while the React Testing Library focuses on interaction and the user experience when testing React components. Together, these tools allow for a comprehensive approach to ensuring that a React application works properly both in terms of functionality and usability.

What is the Test Trophy?

The concept of”Test Trophy“can be applied to different levels of testing in software development, from static testing to end-to-end testing. Each level of testing contributes to earning 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 the investment of time and the return of trust in the software application. This model focuses on writing enough tests, but not necessarily achieving 100% coverage.

Let's see how the “Test Trophy” is applied to the different levels:

Static Tests

Static testing is similar to checking a book for errors before reading it. Before running the software, the code is analyzed for errors and ensures that it is well written and documented. This is essential to ensure the quality, reliability, and maintainability of the software.

  • In static tests, code is evaluated without executing it to identify possible programming errors.
  • The code is checked for compliance with coding standards and good practices.
  • That it is clearly documented for easy understanding.

For static tests, you can use tools such as language-specific linters, such as ESLint for JavaScript or TSLint for TypeScript, to 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 for isolating and testing specific units of code to determine the effectiveness of each component.

As for the technologies used in Typescript, they are Jest, Mocha, Chai, as for Java JUnit, Mockito, TestNGand we have React, React Testing Library, Jest, Enzyme.

Integration Tests

Integration testing focuses on verifying how different components or software modules interact when combined. The main objective is to ensure that the individual parts of the software work properly when integrated to form a larger system.

Winning the “Test Trophy” in integration tests involves:

  • Identify key components and define integration scenarios.
  • Develop and automate scenario-specific test cases.
  • Run tests, manage problems and continuously improve tests for effective integration.

As for the technologies used in Typescript, they are Supertest, Cypress, as for Java Spring Boot Test, REST Assured, TestContainers and we have React, Cypress, Puppeteer.

End-to-End Testing

The tests, from end to end, evaluate the software as a whole, as an end user would. They ensure that all parts of the system are integrated and working properly from the beginning to the end of the process.

In order to win the test trophy, the following characteristics must be considered:

  • Test the entire flow of the application, simulating user actions.
  • Verify that all components are properly integrated and are working as expected.
  • It should be checked that the application responds appropriately to different real-world situations.

For web applications, tools such as Cypress, Puppeteer or Selenium are widely used for end-to-end testing.

👉 Winning the “Test Trophy” at each level increases the team's confidence in the quality of the software. Testing, including static, unit, integration, and end-to-end testing, ensures that the code remains robust and resistant 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, no matter how it connects to the rest of the code. This helps to find errors early in the development process and to create more reliable and better quality software.

Characteristics of unit tests:

  • Focus on Unity: Unit tests test the smallest part of our code in isolation, such as a function, method, or component of the user interface. Each unit is evaluated separately to ensure that it works properly 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, allowing specific problems to be detected with precision without affecting other elements of the system. In addition, by focusing on small units, they are quick 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 more coverage can provide greater trust, it doesn't 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 the quality of the tests, we have to ensure that the code works properly. In addition, unit tests help to 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 do you have to do unit tests?

Testing is essential, as it ensures that our code works according to expectations and maintains quality standards. Testing is a crucial factor in avoiding errors in production that could negatively affect the application or decrease its quality.

Despite the time constraints we sometimes face in the technology industry, testing represents an extremely valuable investment. In the long term, they simplify the maintenance process and ensure that the code meets our requirements.

There are several reasons for this:

  • Early Error Detection: Testing prevents costly errors by identifying them in the early stages of development.
  • Facilitating Quick Changes: Testing helps developers understand the code and implement changes in an agile way, especially in recent projects.
  • Valuable Documentation: Unit tests in software development work as a form of documentation. They explain the purpose and operation of each component, prevent errors, encourage collaboration, and reduce the time needed to familiarize yourself with the code. In addition, they identify reusable components and keep documentation up to date, ultimately improving project efficiency and quality.

Preparing the Environment

Before you begin, 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:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom @types/jes

Create a file jest.config.js at the root of your project. You can configure Jest according to your needs. Here's an example of 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?)$',
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
};

export default config;


You have to have Note that this configuration is a basic configuration. It can be customized to the specific needs of the project.

It must be integrated into the package.json the configuration for jest:


"scripts": {
		"test": "jest --watch",
		"test:coverage": "jest --coverage",
	},


“test”: “jest --watch”: Run tests while you work and show you results instantly.

“test:coverage”: “jest --coverage”: It can help you identify which parts of your code need more testing.

Once it is installed, in order to execute the test tests we use the following command, it can be a single one and it will execute all the tests but you can also enter the name of the file that we want to execute to test one by one

Jest will automatically search for files with the pattern *.test.tsx and it will run the tests defined in them, in this case it's .tsx because we're using typescript.


npx test


Jest will automatically search for files with the pattern *.test.tsx and it will run the tests defined in them, in this case it's .tsx because we're using typescript.


npx test  mi-test.test.tsx


Comparison Methods in Jest and React Testing Library

When we write tests for our React code using Jest and React Testing Library, it's important to know how to compare the expected results with the values obtained during the tests. In this document, we'll look at how to effectively make these comparisons.

  • .toBe (): This method is used to test the exact equality between two values. Use the algorithm Object.is to determine if two values are the same.
  • .toEqual (): Unlike .toBe (), this method performs a recursive comparison of each field in an object or array. It's useful when working with more complex data structures.

Types of queries in Testing library (screen)

  • GetBy: The function GetBy is used to search for an element in the rendered DOM component. If the element that meets the criteria is not found, this function will generate an error and the test will fail.

const element = screen.getByText('Texto en el elemento');

  • QueryBy: The function QueryBy it is also used to search for elements in the DOM. However, unlike GetBy, if the element is not found, instead of generating an error, QueryBy It just returns Null.

const element = screen.queryByText('Texto en el elemento');

  • FindBy: The function FindBy is used to search for an element in the DOM, but with one important difference: it can handle asynchronous searches, such as when an element is expected to appear after an asynchronous action, such as an HTTP request.

const element = await screen.findByText('Texto en el elemento');

Main queries

  1. GetByRole: This query can be used to search for all the items that are exposed in the accessibility tree. With the option Name, you can filter the returned items by their accessible name. It should be your primary preference for just about everything.
  2. GetByLabelText: It's very useful for form fields. Users navigate forms using tags. This query emulates that behavior and should be your first choice for form fields.
  3. getByplaceholderText: A placeholder doesn't replace a label, but if that's all you have, it's better than other alternatives.
  4. GetByText: Outside of forms, text content is the primary way users find items. It can be used to find non-interactive elements such as divs, spans, and paragraphs.
  5. GetByDisplayValue: The current value of a form element can be useful when navigating a page with completed values.

Semantic Queries

  1. GetByAltText: If the element is one that supports alternative text (img, area, input, and any custom element), you can use this to find that element.
  2. GetByTitle: The attribute Title it is not consistently read by screen readers and is not visible by default to users with a view.

Test IDs

  1. GetBytesId: The user can't see them (or hear them), so it's only recommended in cases where you can't match by role or text, or it doesn't 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 useful 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 tests, such as Jest and React Testing Library, a test is considered asynchronous (async) when it involves operations that don't execute immediately, such as calls to functions that return Promises, timers (for example, setTimeout), or interactions with asynchronous APIs (such as network requests).

Synchronous Test (No-Async):

  • Doesn't use functions async or Await.
  • It doesn't make asynchronous calls to APIs, services, or databases.
  • Does not use setTimeout or setInterval.

test('Renderiza Container correctamente', () => {
	render();
	const userList = screen.getByTestId('user-list');
	expect(userList).toBeInTheDocument();
});

Asynchronous Test (Async):

  • Use the keyword async before the test function (async () => {...}).
  • Utilize Await when calling asynchronous functions or waiting for promises.
  • It involves calls to functions that return promises.

test('Renderiza UserListContainer correctamente de manera asincrónica', async () => {

  render();

  await waitFor(() => {
    const userList = screen.queryByTestId('user-list');
    expect(userList).toBeInTheDocument();
  }, { timeout: 1000 });
});

Creating Tests

  • Example 1: Suppose we have a component called Button that you want to test. First, you need to create a file called Button.test.js. Here's an example of what a simple test for the component might look like Button:

import React from 'react';
import { render, screen } from '@testing-library/react';
import Button from './Button';

test('renders button with correct text', () => {
  render(

In this example, we are using the functions Render and Screen.getByText from the React Testing Library to render the component Button and check if the button element with the text “Click me” is present in the document.

  • Example 2: As a start of this test, an object is being created Handlers which contains several simulated functions using jest.fn (). Simulated functions are used to mimic what happens when an event handler is called inside the component being tested. Each function represents a specific action that the component is expected to perform.

💡 jest.fn (): It is a tool that allows you to create simulated functions that store information about your calls during tests. This is useful for verifying if a function is called correctly or for mimicking the behavior of functions in test situations.


const handlers = {
	onChange: jest.fn(),
	onSearchContainer: jest.fn(),
	onClear: jest.fn(),
	onCheckpointChange: jest.fn()
};


Suppose we should test whether or not there is a title in the component EventExceptionForm You can start the test function 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('Botón que llama al handler'));

  // Verifica que los controladores se hayan llamado
  expect(handlers.onChange).toHaveBeenCalled();
  expect(handlers.onSearchContainer).toHaveBeenCalled();
  expect(handlers.onClear).toHaveBeenCalled();
  expect(handlers.onCheckpointChange).toHaveBeenCalled();
});


  • Render the EventExceptionForm component with an empty list of checkpoints and the provided handlers. The use of ... in this case it is to pass all the properties of the object Handlers To the component EventExceptionForm in JavaScript. If the object contains functions that handle events, these functions will be passed to the component so that it can interact with them.
  • Search for an item on the screen that contains the text 'Event/Exception'.
  • expect (title) .toBeInTheDocument (); use Jest to check if the title element is in the document (that is, if it has been rendered correctly). If it is not found, the test will fail.

Then, multiple statements are used Expect to verify that the controllers have been called. This is done with the following lines:

  • expect (handlers.onChange) .toHaveBeenCalled (): Check if the OnChange handler has been called at least once.
  • expect (handlers.onsearchContainer) .toHaveBeenCalled (): Check if the OnSearchContainer handler has been called at least once.
  • expect (handlers.onClear) .toHaveBeenCalled (): Check if the OnClear driver has been called at least once.
  • expect (handlers.onCheckpointChange) .toHaveBeenCalled (): Check 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: 'Nuevo proceso de interpinchazo' });
  expect(buttonElement).toBeInTheDocument();
  expect(buttonElement).not.toBeDisabled();

  fireEvent.click(buttonElement);
  expect(clearForm).toHaveBeenCalledTimes(1);
});


  • A simulated function is created ClearForm utilizing jest.fn (). This simulated function will be used as the event handler OnClick of the button.
  • The component is rendered Button , in this case, the text “New interpuncture process” is passed to it, the type is set to “button”, and it is assigned ClearForm as the event handler OnClick.
  • It is used Screen.getByRole to search for a button element that has the name “New Puncture Process”. It is then verified that the button is present in the document and is not disabled.
  • FireEvent.click (ButtonElement) simulates a click on the button.
  • Finally, it is verified that the function ClearForm has been called exactly once (To Havebe in Called Times (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 the Item. It is a modal that receives the container property, which we will have a mocked container to be able to carry out the corresponding tests. We will verify that depending on the state (CurrentState) where the container is located, a button with a different text will be displayed.


const container = {
    codeContainer: '7654152698',
    currentState: 'created',
}

it('Render the correct button based on the state', () => {
    const { getByText } = render();

 const buttonText =
  container.currentState === 'sealed'
    ? 'Auditar Contenedor'
    : container.currentState === 'onaudit'
    ? 'Cambiar estado'
    : container.currentState === 'disabled'
    ? 'Ir al Contenedor'
    : 'Cargar Contenedor';

    const button = getByText(buttonText);

    expect(button).toBeInTheDocument();
  });


The expression uses multiple ternary operators (? :) to determine the value of **ButtonText** based on the state of the container (Container.currentState).

This code performs a test to confirm that the button displayed in the user interface matches the current state of a container. Ensures that the interface shows the appropriate button for the container's status.

Conclusion

Unit testing is an essential part of the software development process. They allow us to identify and fix problems in our components and functions before they reach production. By using the 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 it also makes it easier to maintain and evolve my project.

References

https://medium.com/@angelygranados/cómo-empezar-a-hacer-unit-testing-con-jest-guía-básica-ca6d9654672

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 such as Jest and React Testing Library, ensuring that your React applications work properly and provide an excellent user experience. Contact us and discover how we can help you raise the quality of your frontend software.

Karla Cabañas

September 16, 2024