【问题标题】:Testing React components that fetches data using Hooks测试使用 Hooks 获取数据的 React 组件
【发布时间】:2019-07-29 13:27:16
【问题描述】:

我的 React 应用程序有一个从远程服务器获取数据以显示的组件。在 pre-hooks 时代,componentDidMount() 是个好去处。但现在我想为此使用钩子。

const App = () => {
  const [ state, setState ] = useState(0);
  useEffect(() => {
    fetchData().then(setState);
  });
  return (
    <div>... data display ...</div>
  );
};

我使用 Jest 和 Enzyme 的测试如下所示:

import React from 'react';
import { mount } from 'enzyme';
import App from './App';
import { act } from 'react-test-renderer';

jest.mock('./api');

import { fetchData } from './api';

describe('<App />', () => {
  it('renders without crashing', (done) => {
    fetchData.mockImplementation(() => {
      return Promise.resolve(42);
    });
    act(() => mount(<App />));
    setTimeout(() => {
      // expectations here
      done();
    }, 500);
  });  
});

测试成功,但记录了一些警告:

console.error node_modules/react-dom/cjs/react-dom.development.js:506
    Warning: An update to App inside a test was not wrapped in act(...).

    When testing, code that causes React state updates should be wrapped into act(...):

    act(() => {
    /* fire events that update state */
    });
    /* assert on the output */

    This ensures that you're testing the behavior the user would see in the browser. Learn more at (redacted)
        in App (created by WrapperComponent)
        in WrapperComponent

App 组件的唯一更新发生在 Promise 回调中。我如何确保这发生在 act 块内?文档明确建议在act外部进行断言。此外,将它们放在里面不会改变警告。

【问题讨论】:

  • 此代码将调用fetchData 两次,如果fetchData 返回不同的数据则进入无限循环。您应该将[] 作为第二个参数传递给useEffect 以模拟componentDidMount。否则每次渲染都会调用useEffect。首先fetchData 导致重新渲染。每当setState 获得新值时,它都会导致额外的渲染。
  • 我不确定,但github.com/threepointone/react-act-examples 看起来很有希望
  • 感谢@UjinT34 的评论。事实上,我有 [] 作为部门,但认为它与这个特定问题无关。事实上,它应该在那里防止过于频繁地调用fetchData。尽管如此,关于不使用act 的警告仍然存在:(
  • 感谢@skyboyer 的建议。您提到的回购的重点是他们使用“手动”模拟,他们可以手动解决 - 在 act 语句中。我更喜欢使用 Jests 模拟可能性。我的感觉是,从 Jest 模拟中解决 Promise 发生在 act 之外,从而触发了警告。但我不知道如何解决。

标签: reactjs jestjs enzyme react-hooks react-test-renderer


【解决方案1】:

我遇到了同样的问题,最后编写了一个库,通过模拟所有标准 React Hooks 来解决这个问题。

基本上,act() 是一个同步函数,就像useEffect,但useEffect 执行一个异步函数。 act() 不可能“等待”它执行。开火即忘!

文章在这里:https://medium.com/@jantoine/another-take-on-testing-custom-react-hooks-4461458935d4

这里的库:https://github.com/antoinejaussoin/jooks

要测试您的代码,您首先需要将您的逻辑(获取等)提取到一个单独的自定义挂钩中:类似于:

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

然后,使用 Jooks,您的测试将如下所示:

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

【讨论】:

    【解决方案2】:

    Enzyme 不支持挂钩,因为它是一个相对较新的功能: https://github.com/airbnb/enzyme/issues/2011

    也许你可以同时使用简单的 Jest? 也不要担心警告,它应该会在 React 16.9.0 发布时消失(请参阅此拉取请求 https://github.com/facebook/react/pull/14853

    【讨论】:

    • 请注意,后半部分提到的 PR 描述了 act 的异步版本,您必须采用它才能使消息“消失”。简单的玩笑也不会解决它。
    • @erich2k8 你是对的。无论如何,当我升级到 React 16.9.0 时,警告消失了。
    【解决方案3】:

    我已经创建了测试异步钩子的示例。

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

    【讨论】:

      【解决方案4】:

      我使用以下步骤解决了这个问题

      1. 将 react 和 react-dom 更新到 16.9.0 版本。
      2. 安装再生器运行时
      3. 在安装文件中导入 regenerator-runtime。

        import "regenerator-runtime/runtime";
        import { configure } from "enzyme";
        import Adapter from "enzyme-adapter-react-16";
        
        configure({
         adapter: new Adapter()
        });
        
      4. 包装挂载和其他可能导致动作内部状态变化的动作。从简单的 react-dom/test-utils、async 和 await 导入 act,如下所示。

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

      希望这会有所帮助。

      【讨论】:

        【解决方案5】:

        这个问题是由组件内部的许多更新引起的。

        我遇到了同样的问题,这将解决问题。

        await act( async () => mount(<App />));
        

        【讨论】:

        • 我收到Warning: Do not await the result of calling TestRenderer.act(...), it is not a Promise.
        猜你喜欢
        • 2019-07-09
        • 2021-06-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-20
        • 2019-08-26
        • 2019-09-30
        • 2020-03-24
        相关资源
        最近更新 更多