26

我的 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此外,将它们放入内部不会改变警告。

4

5 回答 5

16

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

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

await act( async () => mount(<App />));
于 2020-01-21T10:53:20.703 回答
4

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

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

于 2019-04-10T09:20:40.263 回答
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;
  }
}
于 2019-06-23T13:53:00.730 回答
2

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

于 2019-03-19T22:03:04.437 回答
-2

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

  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. 包装 mount 和其他可能导致行为内部状态变化的动作。从简单的 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);
     });  
    });
    

希望这可以帮助。

于 2019-08-23T07:59:24.070 回答