私の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コールバックから行われます。これが確実に行われるようにするにはwithinact
ブロックですか?ドキュメントはアサーションが発生することを明確に示唆していますoutsideact
ブロック。その上、それらを中に入れても警告は変わりません。
非同期フックをテストするための例を作成しました。
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フック。
基本的に、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');
});
});
この問題は、コンポーネント内の多くの更新が原因で発生します。
同じ問題が発生しました。これで問題が解決します。
await act( async () => mount(<App />));