Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 561
delca85 Avatar asked Jun 01 '19 21:06

delca85


People also ask

Can I use async function in useEffect?

Either way, we're now safe to use async functions inside useEffect hooks. Now if/when you want to return a cleanup function, it will get called and we also keep useEffect nice and clean and free from race conditions. Enjoy using async functions with React's useEffect from here on out!

Can I set state inside a useEffect hook?

Can I set state inside a useEffect hook? In principle, you can set state freely where you need it - including inside useEffect and even during rendering. Just make sure to avoid infinite loops by settting Hook deps properly and/or state conditionally.

Is useEffect synchronous or asynchronous?

The useLayoutEffect function is triggered synchronously before the DOM mutations are painted. However, the useEffect function is called after the DOM mutations are painted. I chose this example to make sure the browser actually has some changes to paint when the button is clicked, hence the animation.

How do you run useEffect when state changes?

You can add the state variables you want to track to the hook's dependencies array and the logic in your useEffect hook will run every time the state variables change. Copied! import {useEffect, useState} from 'react'; const App = () => { const [count, setCount] = useState(0); useEffect(() => { console.

How do I test asynchronous functionality in react?

The majority of functionality in a React application will be asynchronous. Testing asynchronous functionality is often difficult but, fortunately, there are tools and techniques to simplify this for a React application. This guide will use Jest with both the React Testing Library and Enzyme to test two simple components.

How do I use the async and await operators in components?

The code will use the async and await operators in the components but the same techniques can be used without them. The first component accepts a function that returns a promise as its get prop. This function is called when a button is clicked and the result that it returns is displayed. The code for this component is:

Should I include setState in my dependency array?

You do not need to include the setter function setState in your dependency array because React guarantees that it will not change. Stale variables are caused when you do not include variables that change in the dependency array that are used in your useEffect function.

Is it safe to use setState within a useeffect?

No, you can safely use the setState setter within a useEffect without including it in the dependency array. Stale variables within useEffect can cause some strange, difficult to debug, bugs. Now either you have probably come across this before, or you have had warnings from eslint about stale variables.


1 Answers

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

like image 158
slideshowp2 Avatar answered Oct 20 '22 18:10

slideshowp2