Table of Contents
Start React testing
Option 1: Unit Testing
Option 2: Integration Testing
So, what is required for unit testing?
Other benefits
Clear waitFor block
In-line it comments
Next steps for the team
Home Web Front-end CSS Tutorial React Integration Testing: Greater Coverage, Fewer Tests

React Integration Testing: Greater Coverage, Fewer Tests

Apr 07, 2025 am 09:20 AM

React Integration Testing: Greater Coverage, Fewer Tests

For interactive websites like those built with React, integration testing is a natural choice. They validate how users interact with applications without the additional overhead of end-to-end testing.

This article illustrates it with an exercise that starts with a simple website, uses unit testing and integration testing to verify behaviors, and demonstrates how integration testing can achieve greater value with fewer lines of code. This article assumes that you are familiar with testing in React and JavaScript. Familiarity with Jest and React Testing Library can be helpful, but not required.

There are three types of tests:

  • Unit tests independently verify a piece of code. They are easy to write, but may ignore the big picture.
  • End-to-end testing (E2E) Use an automated framework such as Cypress or Selenium to interact with your website like users: loading pages, filling in forms, clicking buttons, and more. They are usually written and run slower, but are very close to the real user experience.
  • Integration testing is somewhere in between. They verify how multiple units of an application work together, but are lighter than E2E tests. For example, Jest comes with some built-in utilities to facilitate integration testing; Jest uses jsdom in the background to simulate common browser APIs, with less overhead than automation, and its powerful mocking tools can simulate external API calls.

Another thing to note: In React applications, unit tests and integration tests are written in the same way, and the tools are used .

Start React testing

I created a simple React application (available on GitHub) with a login form. I connected it to reqres.in, which is a handy API I found to use to test front-end projects.

You can log in successfully:

...or encounter an error message from the API:

The code structure is as follows:

 <code>LoginModule/ ├── components/ │ ├── Login.js // 渲染LoginForm、错误消息和登录确认│ └── LoginForm.js // 渲染登录表单字段和按钮├── hooks/ │ └── useLogin.js // 连接到API 并管理状态└── index.js // 将所有内容整合在一起</code>
Copy after login

Option 1: Unit Testing

If you like writing tests like me—maybe wearing headphones and playing nice music on Spotify—then you may be unable to resist writing unit tests for each file.

Even if you are not a test enthusiast, you may be working on a project that "try to do a good job of testing" without a clear strategy, and the test method is "I think every file should have its own test?"

This looks like this (I added unit in the test file name for clarity):

 <code>LoginModule/ ├── components/ │  ├── Login.js │  ├── Login.unit.test.js │  ├── LoginForm.js │  └── LoginForm.unit.test.js ├── hooks/ │  ├── useLogin.js │  └── useLogin.unit.test.js ├── index.js └── index.unit.test.js</code>
Copy after login

I completed the exercise on GitHub to add all these unit tests and created a test:coverage:unit script to generate coverage reports (a built-in feature of Jest). We can achieve 100% coverage through four unit test files:

A 100% coverage is usually overwhelming, but it is possible for such a simple code base.

Let's dig into one of the unit tests created for the onLogin React hook. If you are not familiar with React hooks or how to test them, don't worry.

 test('successful login flow', async () => {
 // Simulate successful API response jest
  .spyOn(window, 'fetch')
  .mockResolvedValue({ json: () => ({ token: '123' }) });

 const { result, waitForNextUpdate } = renderHook(() => useLogin());

 act(() => {
  result.current.onSubmit({
   email: '[email protected]',
   password: 'password',
  });
 });

 // Set the status to pending
 expect(result.current.state).toEqual({
  status: 'pending',
  user: null,
  error: null,
 });

 await waitForNextUpdate();

 // Set the status to resolved and store the email address expect(result.current.state).toEqual({
  status: 'resolved',
  user: {
   email: '[email protected]',
  },
  error: null,
 });
});
Copy after login

This test is interesting to write (because the React Hooks Testing Library makes testing hooks a breeze), but it has some problems.

First, the test validation internal state changes from 'pending' to 'resolved'; this implementation details are not exposed to the user, so it may not be a good thing to test. If we refactor the application, we will have to update this test, even if nothing changes from the user's perspective.

Also, as a unit test, this is just a part of it. If we want to verify other features of the login process, such as changing the submission button text to "Loading", we will have to do it in a different test file.

Option 2: Integration Testing

Let's consider adding an alternative to the integration test to validate this process:

 <code>LoginModule/ ├── components/ │  ├── Login.js │  └── LoginForm.js ├── hooks/ │  └── useLogin.js ├── index.js └── index.integration.test.js</code>
Copy after login

I implemented this test and a test:coverage:integration script to generate coverage reports. Just like unit tests, we can reach 100% coverage, but this time it's all in one file and requires fewer lines of code.

Here are the integration tests covering the successful login process:

 test('successful login', async () => {
  jest
    .spyOn(window, 'fetch')
    .mockResolvedValue({ json: () => ({ token: '123' }) });

  render(<loginmodule></loginmodule> );

  const emailField = screen.getByRole('textbox', { name: 'Email' });
  const passwordField = screen.getByLabelText('Password');
  const button = screen.getByRole('button');

  // Fill in and submit the form fireEvent.change(emailField, { target: { value: '[email protected]' } });
  fireEvent.change(passwordField, { target: { value: 'password' } });
  fireEvent.click(button);

  // It sets the load state expect(button).toBeDisabled();
  expect(button).toHaveTextContent('Loading...');

  await waitFor(() => {
    // It hides the form element expect(button).not.toBeInTheDocument();
    expect(emailField).not.toBeInTheDocument();
    expect(passwordField).not.toBeInTheDocument();

    // It displays the success text and email address const loggedInText = screen.getByText('Logged in as');
    expect(loggedInText).toBeInTheDocument();
    const emailAddressText = screen.getByText('[email protected]');
    expect(emailAddressText).toBeInTheDocument();
  });
});
Copy after login

I really like this test because it verifies the entire login process from a user's perspective: forms, load status and successful confirmation messages. Integration testing is great for React applications, precisely because of this use case; user experience is what we want to test, and this almost always involves multiple different snippets of code working together .

This test doesn't understand the components or hooks that make the expected behavior work, which is nice. As long as the user experience remains the same, we can rewrite and refactor these implementation details without breaking the test.

I won't dig into the initial state of the login process and other integration tests for error handling, but I encourage you to view them on GitHub.

So, what is required for unit testing?

Rather than considering unit testing versus integration testing, let's take a step back and think about how we decide what we need to test in the first place. LoginModule needs to be tested because it is an entity that we want users (other files in the application) to be able to use with confidence.

On the other hand, there is no need to test the onLogin hook, as it is just the implementation details of LoginModule. However, if our requirements change and onLogin has use cases elsewhere, we will need to add our own (unit) tests to verify its functionality as a reusable utility. (We also need to move the file as it is no longer specific to LoginModule.)

Unit testing still has many use cases, such as the need to verify reusable selectors, hooks, and normal functions. When developing your code, you may also find it helpful to use unit test -driven development , even if you move that logic up to integration testing later.

Additionally, unit testing does a great job of thorough testing for multiple inputs and use cases. For example, if my form needs to show inline validation for various scenarios (e.g. invalid email, missing password, too short password), I will cover a representative case in the integration test and then dig into the specific case in the unit test.

Other benefits

Now that we're here, I want to talk about some syntax tips that help keep my integration tests clear and orderly.

Clear waitFor block

Our test needs to consider the latency between the loading state and the successful state of the LoginModule:

 const button = screen.getByRole('button');
fireEvent.click(button);

expect(button).not.toBeInTheDocument(); // Too fast, the button is still there!
Copy after login

We can do this using the waitFor helper function of the DOM Testing Library:

 const button = screen.getByRole('button');
fireEvent.click(button);

await waitFor(() => {
 expect(button).not.toBeInTheDocument(); // Ah, it's much better});
Copy after login

But what if we want to test some other projects? There aren't many good examples on the internet about how to handle this, and in past projects I've put other projects outside of waitFor:

 // Wait button await waitFor(() => {
 expect(button).not.toBeInTheDocument();
});

// Then test the confirmation message const confirmationText = getByText('Logged in as [email protected]');
expect(confirmationText).toBeInTheDocument();
Copy after login

This works, but I don't like it because it makes the button condition look special, even if we can easily switch the order of these statements:

 // Wait for confirmation message await waitFor(() => {
 const confirmationText = getByText('Logged in as [email protected]');
 expect(confirmationText).toBeInTheDocument();
});

// Then test the button expect(button).not.toBeInTheDocument();
Copy after login

It seems to me that it is much better to group everything related to the same update into the waitFor callback:

 await waitFor(() => {
 expect(button).not.toBeInTheDocument();

 const confirmationText = screen.getByText('Logged in as [email protected]');
 expect(confirmationText).toBeInTheDocument();
});
Copy after login

I really like this technique for simple assertions like this, but in some cases it can slow down the tests, waiting for a failure that happens immediately outside waitFor. For this example, see "Multiple assertions in a single waitFor callback" in the React Testing Library common error.

For tests containing several steps, we can use multiple waitFor blocks in succession:

 const button = screen.getByRole('button');
const emailField = screen.getByRole('textbox', { name: 'Email' });

// Fill in the form fireEvent.change(emailField, { target: { value: '[email protected]' } });

await waitFor(() => {
 // Check whether the button is enabled expect(button).not.toBeDisabled();
  expect(button).toHaveTextContent('Submit');
});

// Submit the form fireEvent.click(button);

await waitFor(() => {
 // Check whether the button no longer exists expect(button).not.toBeInTheDocument();
});
Copy after login

If you are waiting for only one item to appear, you can use a findBy query instead. It uses waitFor in the background.

In-line it comments

Another testing best practice is to write fewer, longer tests; this allows you to correlate test cases with important user processes while keeping tests isolated to avoid unexpected behavior. I agree with this approach, but it can pose a challenge in keeping the code organized and documenting the required behavior. We need future developers to be able to return to the test and understand what it is doing, why it fails, etc.

For example, suppose one of these expectations starts to fail:

 it('handles a successful login flow', async () => {
 // Hide the beginning of the test for clarity

  expect(button).toBeDisabled();
  expect(button).toHaveTextContent('Loading...');


 await waitFor(() => {
  expect(button).not.toBeInTheDocument();
  expect(emailField).not.toBeInTheDocument();
  expect(passwordField).not.toBeInTheDocument();


  const confirmationText = screen.getByText('Logged in as [email protected]');
  expect(confirmationText).toBeInTheDocument();
 });
});
Copy after login

Developers looking at this content cannot easily determine what is being tested, and it may be difficult to determine if the failure is a bug (which means we should fix the code) or a behavior change (which means we should fix the test).

My favorite solution is to use the little-known test syntax for each test and add an inline-style comment describing each key behavior being tested:

 test('successful login', async () => {
 // Hide the beginning of the test for clarity

 // It sets the loading status expect(button).toBeDisabled();
  expect(button).toHaveTextContent('Loading...');


 await waitFor(() => {
  // It hides the form element expect(button).not.toBeInTheDocument();
  expect(emailField).not.toBeInTheDocument();
  expect(passwordField).not.toBeInTheDocument();


  // It displays the success text and email address const confirmationText = screen.getByText('Logged in as [email protected]');
  expect(confirmationText).toBeInTheDocument();
 });
});
Copy after login

These comments don't magically integrate with Jest, so if you encounter a failure, the failed test name will correspond to the parameters you passed to the test tag, in this case "successful login". However, Jest's error messages contain the surrounding code, so these it comments still help to identify failed behavior. When I remove not from an expectation, I get the following error message:

To get more explicit errors, there is a package called jest-expect-message that allows you to define error messages for each expectation:

 expect(button, 'button is still in document').not.toBeInTheDocument();
Copy after login

Some developers prefer this approach, but I find it a bit too granular in most cases, as a single it usually involves multiple expectations.

Next steps for the team

Sometimes I wish we could make linter rules for humans. If so, we can set a prefer-integration-tests rule for our team and it ends.

But, alas, we need to find a more similar solution to encourage developers to choose integration testing in some cases, such as the LoginModule example we introduced earlier. Like most things, it comes down to the team discussing your testing strategy, agreeing on what makes sense for the project, and—hopefully—documenting it in ADR.

When developing a test plan, we should avoid a culture that forces developers to write tests for each file. Developers need to be able to make informed testing decisions with confidence without worrying about their “undertest”. Jest's coverage report can help solve this problem by providing a sanity check, even if the tests are merged at the integration level.

I still don't consider myself an integration testing expert, but doing this exercise helped me break down a use case where integration testing offers more value than unit testing. I hope that sharing this with your team, or doing similar exercises on your code base will help guide you incorporating integration testing into your workflow.

The above is the detailed content of React Integration Testing: Greater Coverage, Fewer Tests. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Vue 3 Vue 3 Apr 02, 2025 pm 06:32 PM

It&#039;s out! Congrats to the Vue team for getting it done, I know it was a massive effort and a long time coming. All new docs, as well.

Building an Ethereum app using Redwood.js and Fauna Building an Ethereum app using Redwood.js and Fauna Mar 28, 2025 am 09:18 AM

With the recent climb of Bitcoin’s price over 20k $USD, and to it recently breaking 30k, I thought it’s worth taking a deep dive back into creating Ethereum

Can you get valid CSS property values from the browser? Can you get valid CSS property values from the browser? Apr 02, 2025 pm 06:17 PM

I had someone write in with this very legit question. Lea just blogged about how you can get valid CSS properties themselves from the browser. That&#039;s like this.

Stacked Cards with Sticky Positioning and a Dash of Sass Stacked Cards with Sticky Positioning and a Dash of Sass Apr 03, 2025 am 10:30 AM

The other day, I spotted this particularly lovely bit from Corey Ginnivan’s website where a collection of cards stack on top of one another as you scroll.

A bit on ci/cd A bit on ci/cd Apr 02, 2025 pm 06:21 PM

I&#039;d say "website" fits better than "mobile app" but I like this framing from Max Lynch:

Comparing Browsers for Responsive Design Comparing Browsers for Responsive Design Apr 02, 2025 pm 06:25 PM

There are a number of these desktop apps where the goal is showing your site at different dimensions all at the same time. So you can, for example, be writing

Using Markdown and Localization in the WordPress Block Editor Using Markdown and Localization in the WordPress Block Editor Apr 02, 2025 am 04:27 AM

If we need to show documentation to the user directly in the WordPress editor, what is the best way to do it?

Why are the purple slashed areas in the Flex layout mistakenly considered 'overflow space'? Why are the purple slashed areas in the Flex layout mistakenly considered 'overflow space'? Apr 05, 2025 pm 05:51 PM

Questions about purple slash areas in Flex layouts When using Flex layouts, you may encounter some confusing phenomena, such as in the developer tools (d...

See all articles