Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
980 views
in Technique[技术] by (71.8m points)

reactjs - How to test useEffect with async function and setState inside

I have set up a github project to understand how to better test react (v 16.8.0) useEffect hook. I make an api call to fetch data inside useEffect and I set the received data as state component element. My component receives the query as a prop and make the api call if the query prop string is not empty. I would like to test that with a no-empty query prop the api call is made and the component set its state right.

I know that the problem to be faced testing useEffect is that the effects related to useEffect don't block the browser from updating the screen so the tests come to their end before the useEffect makes its job. I read from React documentation that there is an API from react-test-utils called act that is thought to wrap the code rendering the component and performing updates on it. Even if I tried to use it I keep on having the same problems with my code.

This is the component I am trying to test:

const DisplayData = ({ query, onQueryChange }) => {
    const [data, setData] = useState({ hits: [] });

    useEffect(() => {
        const fetchData = async () => {
            const result = await axios.get(
                `http://hn.algolia.com/api/v1/search?query=${query}`,
            );
            setData(result.data);
        };
        if (!!query) fetchData();
    }, [query]);

    return (
        <ul>
            {data.hits.map(item => (
                <li key={item.objectID}>
                    <a href={item.url}>{item.title}</a>
                </li>
            ))}
        </ul>
    );
};

and this is the test I wrote for it:

it("should show new entries when query is set", () => {
    const el = document.createElement("div");
    document.body.appendChild(el);
    axios.get.mockResolvedValue({ data: { hits: FAKE_HITS } });
    act(() => {
        render(<DisplayData query='pippo' />, el);
    });
    const liCounts = el.querySelectorAll("li");
    expect(liCounts.length).toBe(2);
});

I keep on receiving a warning telling me that

An update to DisplayData inside a test was not wrapped in act(...)

and my test failed because the liCounts received is_0_ instead of the expected 2.

Inserting same console messages to debug the application, I trust that the problem is that useEffect is launched after the test execution but I don't know any more how to proceed.

UPDATE Thanks to @jonrsharpe I solved my issue using React version 16.9.0-alpha.0 that has an async version of act api.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Here is the unit test solution:

We use jest.spyOn(axios, 'get') to mock axios.get method and its resolved/rejected value without hitting the real network. This allows our unit tests to run in an environment that has no side effects and is isolated from the system environment, network environment, etc.

We use act() helper to make sure the fetched data rendered and UI has been updated.

When writing UI tests, tasks like rendering, user events, or data fetching can be considered as “units” of interaction with a user interface. react-dom/test-utils provides a helper called act() that makes sure all updates related to these “units” have been processed and applied to the DOM before you make any assertions:

In the end, We assert whether the axios.get method is called, and through snapshot testing, assert whether data is rendered correctly

index.tsx:

import React, { useState, useEffect } from 'react';
import axios from 'axios';

export const DisplayData = ({ query, onQueryChange }) => {
  const [data, setData] = useState<any>({ hits: [] });

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios.get(`http://hn.algolia.com/api/v1/search?query=${query}`);
      setData(result.data);
    };
    if (!!query) fetchData();
  }, [query]);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
};

index.spec.tsx:

import React from 'react';
import { DisplayData } from './';
import axios from 'axios';
import renderer, { act } from 'react-test-renderer';

describe('DisplayData', () => {
  it('should show new entries when query is set', async () => {
    const mProps = {
      query: 'pippo',
      onQueryChange: jest.fn()
    };
    const FAKE_HITS = [{ objectID: 1, url: 'haha.com', title: 'haha' }];
    const axiosGetSpy = jest.spyOn(axios, 'get').mockResolvedValueOnce({ data: { hits: FAKE_HITS } });
    let component;
    await act(async () => {
      component = renderer.create(<DisplayData {...mProps}></DisplayData>);
    });
    expect(axiosGetSpy).toBeCalledWith('http://hn.algolia.com/api/v1/search?query=pippo');
    expect(component.toJSON()).toMatchSnapshot();
    axiosGetSpy.mockRestore();
  });

  it('should not fetch data when query is empty string', async () => {
    const mProps = {
      query: '',
      onQueryChange: jest.fn()
    };
    const axiosGetSpy = jest.spyOn(axios, 'get');
    let component;
    await act(async () => {
      component = renderer.create(<DisplayData {...mProps}></DisplayData>);
    });
    expect(axiosGetSpy).not.toBeCalled();
    expect(component.toJSON()).toMatchSnapshot();
    axiosGetSpy.mockRestore();
  });
});

Unit test result with 100% coverage:

 PASS  src/stackoverflow/56410688/index.spec.tsx
  DisplayData
    ? should show new entries when query is set (28ms)
    ? should not fetch data when query is empty string (5ms)

-----------|----------|----------|----------|----------|-------------------|
File       |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files  |      100 |      100 |      100 |      100 |                   |
 index.tsx |      100 |      100 |      100 |      100 |                   |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   2 passed, 2 total
Time:        3.666s

index.spec.tsx.snap:

// Jest Snapshot v1, 

exports[`DisplayData should not fetch data when query is empty string 1`] = `<ul />`;

exports[`DisplayData should show new entries when query is set 1`] = `
<ul>
  <li>
    <a
      href="haha.com"
    >
      haha
    </a>
  </li>
</ul>
`;

Dependencies versions:

"jest": "^24.9.0",
"react-test-renderer": "^16.11.0",
"react": "^16.11.0",
"axios": "^0.19.0",

Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/56410688


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...