问题
I'm trying to write a test for a React component that needs to complete an asynchronous action in its componentWillMount
method. componentWillMount
calls a function, passed as a prop, which returns a promise, and I mock this function in my test.
This works fine, but if a test fails in a call to setImmediate
or process.nextTick
, the exception isn't handled by Jest and it exits prematurely. Below, you can see I even try to catch this exception, to no avail.
How can I use something like setImmediate
or nextTick
with Jest? The accepted answer to this question is what I'm trying to implement unsuccessfully: React Enzyme - Test `componentDidMount` Async Call.
it('should render with container class after getting payload', (done) => {
let resolveGetPayload;
let getPayload = function() {
return new Promise(function (resolve, reject) {
resolveGetPayload = resolve;
});
}
const enzymeWrapper = mount(<MyComponent getPayload={getPayload} />);
resolveGetPayload({
fullname: 'Alex Paterson'
});
try {
// setImmediate(() => {
process.nextTick(() => {
expect(enzymeWrapper.hasClass('container')).not.toBe(true); // Should and does fail
done();
});
} catch (e) {
console.log(e); // Never makes it here
done(e);
}
});
Jest v18.1.0
Node v6.9.1
回答1:
Another potentially cleaner solution, using async/await and leveraging the ability of jest/mocha to detect a returned promise:
function currentEventLoopEnd() {
return new Promise(resolve => setImmediate(resolve));
}
it('should render with container class after getting payload', async () => {
let resolveGetPayload;
let getPayload = function() {
return new Promise(function (resolve, reject) {
resolveGetPayload = resolve;
});
}
const enzymeWrapper = mount(<MyComponent getPayload={getPayload} />);
resolveGetPayload({
fullname: 'Alex Paterson'
});
await currentEventLoopEnd(); // <-- clean and clear !
expect(enzymeWrapper.hasClass('container')).not.toBe(true);
});
回答2:
Overcome that issue in the next way atm (also it solves the issue with Enzyme and async call in componentDidMount and async setState):
it('should render proper number of messages based on itemsPerPortion', (done) => {
const component = shallow(<PublishedMessages itemsPerPortion={2} messagesStore={mockMessagesStore()} />);
setImmediate(() => { // <-- that solves async setState in componentDidMount
component.update();
try { // <-- that solves Jest crash
expect(component.find('.item').length).toBe(2);
} catch (e) {
return fail(e);
}
done();
});
});
(Enzyme 3.2.0, Jest 21.1.6)
UPDATE
Just figured out another, better (but still strange) solution on that using async/await (and it still solving async componentDidMount and async setState):
it('should render proper number of messages based on itemsPerPortion', async () => {
// Magic below is in "await", looks as that allows componentDidMount and async setState to complete
const component = await shallow(<PublishedMessages itemsPerPortion={2} messagesStore={mockMessagesStore()} />);
component.update(); // still needed
expect(component.find('.item').length).toBe(2);
});
Also other async-related actions should be prefixed with await
as well like
await component.find('.spec-more-btn').simulate('click');
回答3:
Some things to note;
process.nextTick
is async, so the try/catch won't be able to capture that.Promise
will also resolve/reject async even if the code you run in the Promise is sync.
Give this a try
it('should render with container class after getting payload', (done) => {
const getPayload = Promise.resolve({
fullname: 'Alex Paterson'
});
const enzymeWrapper = mount(<MyComponent getPayload={getPayload} />);
process.nextTick(() => {
try {
expect(enzymeWrapper.hasClass('container')).not.toBe(true);
} catch (e) {
return done(e);
}
done();
});
});
回答4:
Following on from Vladimir's answer + edit, here's an alternative that worked for me. Rather than await
the mount, await
the wrapper.update()
:
it('...', async () => {
let initialValue;
let mountedValue;
const wrapper = shallow(<Component {...props} />);
initialValue = wrapper.state().value;
await wrapper.update(); // componentDidMount containing async function fires
mountedValue = wrapper.state().value;
expect(mountedValue).not.toBe(initialValue);
});
回答5:
Wrapping the callback block passed to process.nextTick
or setImmediate
in a try
/catch
works, as others have shown, but this is verbose and distracting.
A cleaner approach is to flush promises using the brief line await new Promise(setImmediate);
inside an async
test callback. Here's a working example of using this to let an HTTP request in a useEffect
(equally useful for componentDidMount
) resolve and trigger a re-render before running assertions:
Component (LatestGist.js
):
import axios from "axios";
import {useState, useEffect} from "react";
export default () => {
const [gists, setGists] = useState([]);
const getGists = async () => {
const res = await axios.get("https://api.github.com/gists");
setGists(res.data);
};
useEffect(() => getGists(), []);
return (
<>
{gists.length
? <div data-test="test-latest-gist">
the latest gist was made on {gists[0].created_at} by {gists[0].owner.login}
</div>
: <div>loading...</div>}
</>
);
};
Test (LatestGist.test.js
):
import React from "react";
import {act} from "react-dom/test-utils";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({adapter: new Adapter()});
import mockAxios from "axios";
import LatestGist from "./LatestGist";
jest.mock("axios");
describe("LatestGist", () => {
beforeEach(() => jest.resetAllMocks());
it("should load the latest gist", async () => {
mockAxios.get.mockImplementationOnce(() => Promise.resolve({
data: [{owner: {login: "test name"}, created_at: "some date"}],
status: 200
}));
const wrapper = mount(<LatestGist />);
let gist = wrapper.find('[data-test="test-latest-gist"]');
expect(gist.exists()).toBe(false);
await act(() => new Promise(setImmediate));
wrapper.update();
expect(mockAxios.get).toHaveBeenCalledTimes(1);
gist = wrapper.find('[data-test="test-latest-gist"]');
expect(gist.exists()).toBe(true);
expect(gist.text()).toContain("test name");
expect(gist.text()).toContain("some date");
});
});
Forcing a failed assertion with a line like expect(gist.text()).toContain("foobar");
doesn't cause the suite to crash:
● LatestGist › should load the latest gist
expect(string).toContain(value)
Expected string:
"the latest gist was made on some date by test name"
To contain value:
"foobar"
at Object.it (src/LatestGist.test.js:30:25)
回答6:
-- Posting as an answer for one cannot format code blocks in comments. --
Building upon Vladimir's answer, note that using async/await works in a beforeEach as well:
var wrapper
beforeEach(async () => {
// Let's say Foobar's componentDidMount triggers async API call(s)
// resolved in a single Promise (use Promise.all for multiple calls).
wrapper = await shallow(<Foobar />)
})
it('does something', () => {
// no need to use an async test anymore!
expect(wrapper.state().asynchronouslyLoadedData).toEqual(…)
})
来源:https://stackoverflow.com/questions/41792927/jest-tests-cant-fail-within-setimmediate-or-process-nexttick-callback