繁体   English   中英

测试使用 Hooks 获取数据的 React 组件

[英]Testing React components that fetches data using Hooks

我的 React 应用程序有一个组件,可以从远程服务器获取要显示的数据。 在前 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之外进行断言。 此外,将它们放入里面不会改变警告。

该问题是由 Component 内部的许多更新引起的。

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

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

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

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

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

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

我遇到了完全相同的问题,最终编写了一个库,通过模拟所有标准 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');
  });
});

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

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

     import "regenerator-runtime/runtime"; import { configure } from "enzyme"; import Adapter from "enzyme-adapter-react-16"; configure({ adapter: new Adapter() });
  4. 将 mount 和其他可能导致状态更改的操作包装在 act 中。 从简单的 react-dom/test-utils、async 和 await 导入行为,如下所示。

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

希望这可以帮助。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM