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

IT技术 reactjs jestjs enzyme react-hooks react-test-renderer
2021-04-16 05:28:56

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

5个回答

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

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

await act( async () => mount(<App />));
我得到 Warning: Do not await the result of calling TestRenderer.act(...), it is not a Promise.
2021-06-07 05:28:56

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

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

请注意,上半部分提到的 PR 描述了 的异步版本act,您必须采用它才能使消息“消失”。简单的玩笑也不会解决它。
2021-05-26 05:28:56
@erich2k8 你是对的。无论如何,当我升级到 React 16.9.0 时,警告消失了。
2021-06-21 05:28:56

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

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

希望这可以帮助。