web-dev-qa-db-fra.com

Comment tester useEffect avec la fonction asynchrone et setState à l'intérieur

J'ai mis en place un projet github pour comprendre comment mieux tester React (v 16.8.0) useEffect hook. Je fais un appel api pour récupérer des données à l'intérieur useEffect et je définis les données reçues comme élément de composant d'état. Mon composant reçoit la requête en tant que prop et effectue l'appel api si la chaîne de prop de requête n'est pas vide. Je voudrais tester qu'avec un accessoire de requête non vide, l'appel api est effectué et le composant définit son état correctement.

Je sais que le problème à résoudre lors du test useEffect est que les effets liés à useEffect n'empêche pas le navigateur de mettre à jour l'écran afin que les tests prennent fin avant que useEffect ne fasse son travail. J'ai lu dans la documentation React qu'il existe une API de react-test-utils appelée agir qui est censé envelopper le code en rendant le composant et en effectuant des mises à jour dessus. Même si j'essayais de l'utiliser, je continue d'avoir les mêmes problèmes avec mon code.

Voici le composant que j'essaie de tester:

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>
    );
};

et voici le test que j'ai écrit pour cela:

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);
});

Je continue de recevoir un avertissement me disant que

Une mise à jour de DisplayData dans un test n'était pas encapsulée dans l'acte (...)

et mon test a échoué car les liCounts reçus is_0_ au lieu des attendus 2 .

En insérant les mêmes messages de console pour déboguer l'application, j'espère que le problème est que useEffect est lancé après l'exécution du test mais je ne sais plus comment procéder.

UPDATE Grâce à @jonrsharpe, j'ai résolu mon problème en utilisant React version 16.9.0-alpha.0 qui a une version asynchrone de act api.

12
delca85

Voici la solution de test unitaire:

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();
  });
});

Résultat du test unitaire avec une couverture à 100%:

 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>
`;

Versions des dépendances:

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

Code source: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/56410688

5
slideshowp2