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

时间:2019-03-07 15:35:59

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

我的React应用程序具有一个组件,该组件从远程服务器获取要显示的数据。在预钩时代,==1040== 624 bytes in 1 blocks are possibly lost in loss record 15 of 15 ==1040== at 0x4C2B955: calloc (vg_replace_malloc.c:711) ==1040== by 0x40126B4: _dl_allocate_tls (in /usr/lib64/ld-2.17.so) ==1040== by 0x50B97AB: pthread_create@@GLIBC_2.2.5 (in /usr/lib64/libpthread-2.17.so) ==1040== by 0xA7381C9: boost::thread::start_thread_noexcept() (in boostLocation/lib/libboost_thread.so.1.67.0) ==1040== by 0x56336CA: start_thread (thread.hpp:179) ==1040== by 0x56336CA: boost::thread::thread<void (*)(<arguments>) (thread.hpp:435) ==1040== by 0x5630FA8: <my code, allocating then freeing a new boost::thread> 是一个理想的选择。但是现在我想为此使用钩子。

componentDidMount()

我使用Jest和Enzyme进行的测试如下:

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

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

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

App组件的唯一更新发生在Promise回调中。如何确保在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 块内 中发生这种情况?文档明确建议断言发生在act块的外部处。此外,将它们放在里面不会改变警告。

5 个答案:

答案 0 :(得分:10)

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

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

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

答案 1 :(得分:1)

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

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

答案 2 :(得分:0)

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

答案 3 :(得分:0)

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

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 :(得分:0)

我通过以下步骤解决了此问题

  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. 自动换行和其他可能导致状态更改的动作。从简单的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);
     });  
    });
    

希望这会有所帮助。