Testing React components that fetches data using Hooks

后端 未结 5 1405
不知归路
不知归路 2020-12-02 00:02

My React-application has a component that fetches data to display from a remote server. In the pre-hooks era, componentDidMount() was the place to go. But now I

相关标签:
5条回答
  • 2020-12-02 00:04

    I have created examples for testing async hooks.

    https://github.com/oshri6688/react-async-hooks-testing

    CommentWithHooks.js:

    import { getData } from "services/dataService";
    
    const CommentWithHooks = () => {
      const [data, setData] = useState(null);
      const [isLoading, setIsLoading] = useState(true);
    
      const fetchData = () => {
        setIsLoading(true);
    
        getData()
          .then(data => {
            setData(data);
          })
          .catch(err => {
            setData("No Data");
          })
          .finally(() => {
            setIsLoading(false);
          });
      };
    
      useEffect(() => {
        fetchData();
      }, []);
    
      return (
        <div>
          {isLoading ? (
            <span data-test-id="loading">Loading...</span>
          ) : (
            <span data-test-id="data">{data}</span>
          )}
    
          <button
            style={{ marginLeft: "20px" }}
            data-test-id="btn-refetch"
            onClick={fetchData}
          >
            refetch data
          </button>
        </div>
      );
    };
    

    CommentWithHooks.test.js:

    import React from "react";
    import { mount } from "enzyme";
    import { act } from "react-dom/test-utils";
    import MockPromise from "testUtils/MockPromise";
    import CommentWithHooks from "./CommentWithHooks";
    import { getData } from "services/dataService";
    
    jest.mock("services/dataService", () => ({
      getData: jest.fn(),
    }));
    
    let getDataPromise;
    
    getData.mockImplementation(() => {
      getDataPromise = new MockPromise();
    
      return getDataPromise;
    });
    
    describe("CommentWithHooks", () => {
      beforeEach(() => {
        jest.clearAllMocks();
      });
    
      it("when fetching data successed", async () => {
        const wrapper = mount(<CommentWithHooks />);
        const button = wrapper.find('[data-test-id="btn-refetch"]');
        let loadingNode = wrapper.find('[data-test-id="loading"]');
        let dataNode = wrapper.find('[data-test-id="data"]');
    
        const data = "test Data";
    
        expect(loadingNode).toHaveLength(1);
        expect(loadingNode.text()).toBe("Loading...");
    
        expect(dataNode).toHaveLength(0);
    
        expect(button).toHaveLength(1);
        expect(button.prop("onClick")).toBeInstanceOf(Function);
    
        await getDataPromise.resolve(data);
    
        wrapper.update();
    
        loadingNode = wrapper.find('[data-test-id="loading"]');
        dataNode = wrapper.find('[data-test-id="data"]');
    
        expect(loadingNode).toHaveLength(0);
    
        expect(dataNode).toHaveLength(1);
        expect(dataNode.text()).toBe(data);
      });
    

    testUtils/MockPromise.js:

    import { act } from "react-dom/test-utils";
    
    const createMockCallback = callback => (...args) => {
      let result;
    
      if (!callback) {
        return;
      }
    
      act(() => {
        result = callback(...args);
      });
    
      return result;
    };
    
    export default class MockPromise {
      constructor() {
        this.promise = new Promise((resolve, reject) => {
          this.promiseResolve = resolve;
          this.promiseReject = reject;
        });
      }
    
      resolve(...args) {
        this.promiseResolve(...args);
    
        return this;
      }
    
      reject(...args) {
        this.promiseReject(...args);
    
        return this;
      }
    
      then(...callbacks) {
        const mockCallbacks = callbacks.map(callback =>
          createMockCallback(callback)
        );
    
        this.promise = this.promise.then(...mockCallbacks);
    
        return this;
      }
    
      catch(callback) {
        const mockCallback = createMockCallback(callback);
    
        this.promise = this.promise.catch(mockCallback);
    
        return this;
      }
    
      finally(callback) {
        const mockCallback = createMockCallback(callback);
    
        this.promise = this.promise.finally(mockCallback);
    
        return this;
      }
    }
    
    0 讨论(0)
  • 2020-12-02 00:04

    I had that exact same problem, and ended up writing a library that solves this issue by mocking all the standards React Hooks.

    Basically, act() is a synchronous function, like useEffect, but useEffect executes an async function. There's no way that act() would be able to "wait" for that to execute. Fire and forget!

    Article here: https://medium.com/@jantoine/another-take-on-testing-custom-react-hooks-4461458935d4

    Library here: https://github.com/antoinejaussoin/jooks

    To test your code, you would first need to extract your logic (the fetch, etc.) into a separate custom hook: something like:

    const useFetchData = () => {
      const [ state, setState ] = useState(0);
      useEffect(() => {     
        fetchData().then(setState);
      });
      return state;
    }
    

    Then, using Jooks, your test would look like:

    import init from 'jooks';
    [...]
    describe('Testing my hook', () => {
      const jooks = init(() => useFetchData());
    
      // Mock your API call here, by returning 'some mocked value';
    
      it('Should first return 0', () => {
        const data = jooks.run();
        expect(data).toBe(0);
      });
    
      it('Then should fetch the data and return it', async () => {
        await jooks.mount(); // Fire useEffect etc.
        const data = jooks.run();
        expect(data).toBe('some mocked value');
      });
    });
    
    
    0 讨论(0)
  • 2020-12-02 00:06

    That issue is caused by many updates inside Component.

    I got the same issue, this would solve the issue.

    await act( async () => mount(<App />));
    
    0 讨论(0)
  • 2020-12-02 00:07

    Enzyme doesn't have support for hooks since it's a relatively new feature: https://github.com/airbnb/enzyme/issues/2011

    Maybe you can use plain Jest in the meantime? Also don't worry about the warning, it's supposed to go away when React 16.9.0 is released (see this pull request https://github.com/facebook/react/pull/14853)

    0 讨论(0)
  • 2020-12-02 00:20

    I resolved this issue using below steps

    1. Update react and react-dom to 16.9.0 version.
    2. Install regenerator-runtime
    3. import regenerator-runtime in setup file.

      import "regenerator-runtime/runtime";
      import { configure } from "enzyme";
      import Adapter from "enzyme-adapter-react-16";
      
      configure({
       adapter: new Adapter()
      });
      
    4. Wrap mount and other possible actions which are going to cause state changes inside act. import act from simple react-dom/test-utils, async & await like below.

      import React from 'react';
      import { mount } from 'enzyme';
      import App from './App';
      import { act } from "react-dom/test-utils";
      
      jest.mock('./api');
      
      import { fetchData } from './api';
      
      describe('<App />', () => {
      it('renders without crashing',  async (done) => {
        fetchData.mockImplementation(() => {
          return Promise.resolve(42);
        });
        await act(() => mount(<App />));
        setTimeout(() => {
          // expectations here
          done();
        }, 500);
       });  
      });
      

    Hope this helps.

    0 讨论(0)
提交回复
热议问题