Back

Integration Testing in React

Integration Testing in React

Testing in web development is a highly overlooked but crucial aspect of the web development process. It ensures an application’s reliability and functionality by detecting and removing bugs and unexpected behavior. It also increases user satisfaction and confidence in the application. This article will teach you all about integration testing so you can ensure the quality of your app.

Integration testing in React refers to testing the interactions between the different components in a React application, i.e., how these components work together to create a specific functionality or workflow. Components are the foundation of the user interface in React applications, and they frequently interact with one another to create different workflows. Integration tests ensure that these components function properly when combined to form the desired functionality. In this guide, we will delve deep into the complexities of integration testing, giving you the skills to build React applications confidently and dependably in every component’s interaction.

Writing Your First Integration Test

To perform integration testing in React, you’ll need the following two major tools:

  • React Testing Library (RTL)
  • Jest

RTL is a React library designed for testing React components. Jest is a JavaScript testing framework used for testing JavaScript codebases. Let’s create and configure a new React project called “testing” using create-react-app.

npx create-react-app testing

When you create a new React project using create-react-app, RTL and some of its dependencies are automatically installed, as shown below:

RTL_dependencies

If that is not the case, you can simply install them manually using the following commands:

npm install --save-dev @testing-library/react
npm install --save-dev @testing-library/jest-dom
npm install --save-dev @testing-library/user-event

Now that we’re all set up, let’s start testing!

We’ll create two components that interact with each other: a Parent component called MainBody.js, which renders a Child component called ChangeMainBody.js.

//ParentComponent called MainBody.js
import React, { useState } from "react";
import ChangeMainBody from "./ChangeMainBody";

function MainBody() {
  const [mainBody, setMainBody] = useState("Initial Text");

  return (
    <div>
      <h1>{mainBody}</h1>
      <ChangeMainBody setMainBody={setMainBody} />
    </div>
  );
}

export default MainBody;
//ChildComponent called ChangeMainBody.js
import React, { useState } from "react";

function ChangeMainBody({ setMainBody }) {
  const [ChangeMainBody, setChangeMainBody] = useState("Changed Text");

  function handleClick() {
    setChangeMainBody("New Text");
    setMainBody("New Text");
  }

  return (
    <div>
      <div>{ChangeMainBody}</div>
      <button onClick={handleClick}>Change Main Body</button>
    </div>
  );
}

export default ChangeMainBody;

The two components above interact with each other via props such that when the button in the ChangeMainBody component is clicked, the h1 text in the MainBody component is updated to “New Text” using the setMainBody function passed as a prop to the ChangeMainBody component.

bandicam 2024-02-10 15-02-47-038

Let’s write an integration test for this interaction using Jest and React Testing Library. To accomplish this, we will test the Parent Component(MainBody.js in this example), which houses the Child Component. Create a file named MainBody.test.js in your test folder and type in the following code to implement your first integration test.

//MainBody.test.js
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import MainBody from "../MainBody";

test("Button Click should change MainBody h1 text", () => {
  render(<MainBody />);
  const buttonElement = screen.getByText(/change main body/i);
  fireEvent.click(buttonElement);
  const headingElement = screen.getByRole("heading", { name: /new text/i }); //Check to see that h1 has been updated after click
  expect(headingElement).toBeInTheDocument();
});

Let’s break down the code sample above:

In this case, we use the render method to mount the component for testing, which is the MainBody component. We then get the button from the ChangeMainBody component using the screen.getByText method. We simulate a user clicking this button using fireEvent.click and then use the expect method to check that the h1 in the MainBody component has been updated following the button click.

After completing this test suite, type the following command in your terminal:

npm run test

Everything being equal, you should get the following output indicating the test has passed:

Integration_test1

Congratulations!! You have completed your first integration test! You realize that you could simulate the interactions between the MainBody and ChangeMainBody components to test the h1 text update functionality without having to manually perform the actions yourself. This is the core of integration testing in React.

Mocking Dependencies

Mocking is often an essential aspect of testing in React. It is defined as the practice of substituting parts of your application with simulated or controlled versions during the testing process.

Integration testing involves checking if the different components of your application work well together. Some of these components often rely on external resources, such as functions, services, or APIs, to function properly. These external resources are referred to as dependencies. Mocking enables you to simulate the behaviors of these dependencies, allowing you to test solely your component interactions without making actual calls to or relying on these dependencies.

Now you might wonder, “Isn’t it a better idea to test our components without mocking these dependencies? We’ll be testing both our frontend and backend functionalities”. However, the following points will explain why this is a very bad idea and highlight the importance of mocking in your tests.

  • Some external dependencies, such as external API services, begin charging per usage after a certain number of requests. If you are developing a large React application, you will be running tests repeatedly, and if you have components that make API requests to these charged services, you’ll make these requests every single time a test is run. Eventually, you will start being charged for each request, which is something you want to avoid. With mocks, you’ll be able to simulate the behavior and response of these external API services without making actual calls to them and as such, avoid charges for requests.
  • Real API calls may require a network request, which can be time-consuming and result in slow test execution. This means that if you have a React application with many tests, you may have to wait for extended periods for all of them to run. This may discourage you from running your tests as frequently as you should. Mocking helps speed up these test executions by providing immediate responses, allowing you to run your tests more frequently and efficiently.
  • Testing components that require external services without mocking implies you’re relying on the availability and correctness of those services during testing. If, for some reason, the external service is unavailable or returns unexpected data, your tests will fail. Whereas with mocks, you can isolate the components being tested from their dependencies by using mock versions of the services, allowing you to focus on testing only the components’ behavior and interactions with other components without worrying about the service’s availability or correctness.

Now that you understand the need for mocks in your tests let’s start mocking our dependencies. In this guide, we will mock all of our dependencies using Jest Mocks.

Mocking React Components

We’ll start by mocking React components. Suppose you have a component, AboutComponent that renders some text and an image.

import React from "react";

function AboutComponent() {
  return (
    <div>
      <h1>About</h1>
      <p>This is the about page</p>
      <img src="/image_path" alt="about_image"></img>
    </div>
  );
}

export default AboutComponent;

Fetching and displaying images will typically slow down your tests. You should mock this component to only test that it renders correctly. To mock a React component with Jest mocks, you use the jest.mock function to mock the path to the module that exports the component and return a mocked component with its custom implementation.

In your test file, type the following code:

import {render} from '@testing-library/react';
import '@testing-library/jest-dom';
import AboutComponent from './AboutComponent'  //Specify your own path

//Mock path to module
jest.mock("./AboutComponent", () => {
  return () => {
    return (
      <div>
        <mock-image data-testid="about-image" alt="mock_image" />
      </div>
    );
  };
});

test("Mocked About page is visible", async () => {
  render(<AboutComponent />);

  const fullImage = screen.getByTestId(/about-image/i);
  expect(fullImage).toBeInTheDocument();

  const aboutPage = await screen.findByAltText(/mocked_image/i);
  expect(aboutPage).toBeInTheDocument();
});

Let’s understand the code above.

We import the component we want to mock, AboutComponent into our test file. We mock this component by calling the jest.mock function. We pass as arguments to this function the path to the module containing the component and a factory function that returns the mocked component along with the mocked image.

In our test suite, we render the AboutComponent for testing. We then use the screen.findByTestId to find an element with a data-testid attribute of “about-image” and assert that it exists in the document. We also use the screen.findByAltText to find the alt text of this element and assert that its value is “mocked-image”.

When you run the test, Jest replaces the original AboutComponent with the mocked component returned by the jest.mock factory function. This causes the test to pass, as seen below:

Component_mock

Mocking External Modules and APIs

API interactions are a common feature of React applications. In this guide, we will mock API requests made with the Axios library. We will also use Jest for the API mocking. Let’s look at a simple example.

Suppose you have a file, apitest.js in which you make an axios GET request to an external server. This file contains a function fetchTodos, which handles the request like so:

import axios from "axios";

const fetchTodos = async () => {
  const res = await axios.get("https://jsonplaceholder.typicode.com/todos/1");
  if (res && res.data) {
    console.log(res.data);
    return res.data.title;
  }
};

export default fetchTodos;

In your main component, you import the fetchTodos function and use it like so:

//Main Component
import React, { useEffect } from "react";
import fetchTodos from "./apitest";

function DataComponent() {
  useEffect(() => {
    fetchTodos();
  }, []);
}

export default DataComponent;

To mock this request, you must mock axios, the external module that handles the request. In your test file, type the following code:

//DataComponent.test.js
import "@testing-library/jest-dom";
import fetchTodos from "../apitest";
import axios from "axios";

jest.mock("axios", () => {
  return {
    get: jest.fn().mockResolvedValue({ data: { title: "mocked data" } }),
  };
});

test("Testing with mocked axios", async () => {
  const result = await fetchTodos();
  expect(result).toEqual("mocked data");
});

From the code above, we import the fetchTodos function that handles the request, andaxios. We then mock axios using the jest.mock function by specifying its path and the factory function as arguments to the jest.mock function. The factory function returns an object containing a mocked implementation of the actual API call in the fetchTodos. In this mocked implementation, we replace the get method with a Jest mock function, jest.fn(), and chain with mockResolvedValue to return a resolved value called mocked-data. This means whenever axios.get is called, it returns the value “mocked-data”.

In the test, we call the fetchTodos function, which uses axios, and assert that the returned result matches the expected value “mocked-data”. When you run the test, Jest will replace the actual API request in the fetchTodos function with the mocked request in the Jest mock function, causing the test to pass.

API_Pass

This test allows you to test the functionality of the fetchTodos function without relying on the actual API call for data retrieval.

Testing Navigation and Route Changes

Routing is a fundamental aspect of React applications, so it is critical to ensure that navigation behavior and route mechanisms work as expected. Using React Testing Library, you can simulate route changes and test navigation behavior.

We will create 2 Route components: DataComponent and AboutComponent. The DataComponent will contain a link that, when clicked, navigates you to the AboutComponent.

//DataComponent.js
import React from "react";
import { Link } from "react-router-dom";

function DataComponent() {
  render(
    <div>
      <Link to="/about">About</Link>
    </div>,
  );
}

export default DataComponent;
//AboutComponent.js
import React from "react";

function AboutComponent() {
  return (
    <div>
      <h1>About</h1>
      <p>This is the about page</p>
    </div>
  );
}

export default AboutComponent;

Resulting output:

bandicam 2024-02-10 13-46-24-207

To test this navigation functionality, create a test file named DataComponent.test.js in your test folder and type in the following code:

//DataComponent.test.js
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import DataComponent from "../DataComponent";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import AboutComponent from "../AboutComponent";

//Navigation Test
test("Navigation to About page is possible", async () => {
  render(
    <Router>
      <Routes>
        <Route path="/" element={<DataComponent />} />
        <Route path="/about" element={<AboutComponent />} />
      </Routes>
    </Router>,
  );
  const aboutLink = screen.getByRole("link", { name: /about/i });
  fireEvent.click(aboutLink);
  const aboutPage = await screen.findByText(/This is the about page/i);
  expect(aboutPage).toBeInTheDocument();
});

Let’s break down the test:

We begin by rendering the components for testing via their respective routes and wrapping them with the Router component. This sets up React Router for testing by simulating the presence of the BrowserRouter in a real application. The home (’/’) route displays the DataComponent which contains the About link. We get this link using screen.getByRole and use the fireEvent.click to simulate a user clicking the link. After the simulated click, we assert that the route has changed by checking to see if the text “This is the about page” now appears on the screen, as this text is only found in the AboutComponent.

Running this test, you should get the following output indicating that it passes.

Routing_test

Conclusion

To conclude, integration testing in React is a fundamental practice that enables developing robust, reliable, and maintainable applications by ensuring that components work harmoniously together. Knowing that components work together as intended boosts the confidence of developers and teams in the overall behavior of the application. By validating that the application behaves correctly when different parts are integrated, developers can deliver a more reliable, high-quality product to end-users.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay