开玩笑:测试不能在 setImmediate 或 process.nextTick 回调中失败
Posted
技术标签:
【中文标题】开玩笑:测试不能在 setImmediate 或 process.nextTick 回调中失败【英文标题】:Jest: tests can't fail within setImmediate or process.nextTick callback 【发布时间】:2017-06-07 04:05:36 【问题描述】:我正在尝试为需要在其componentWillMount
方法中完成异步操作的 React 组件编写测试。 componentWillMount
调用一个函数,作为道具传递,它返回一个承诺,我在测试中模拟了这个函数。
这可以正常工作,但如果在调用 setImmediate
或 process.nextTick
时测试失败,则 Jest 不会处理该异常并提前退出。下面,你可以看到我什至试图捕捉这个异常,但无济于事。
如何在 Jest 中使用 setImmediate
或 nextTick
之类的内容?这个问题的公认答案是我试图实现的失败: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
节点 v6.9.1
【问题讨论】:
您是否阅读过有关测试异步内容的文档 (facebook.github.io/jest/docs/tutorial-async.html#content)? 是的,我不是在测试异步函数,而是在测试一个需要等待承诺的组件。 但是你创建了一个promise,即使你在测试中解决了它也不会在测试运行中产生影响,你至少需要从测试中返回promise。 但是你创建了一个promise,即使你在测试中解决了它也不会在测试运行中产生影响,你至少需要从测试中返回promise。 您是否设法解决了这个问题@AlexPaterson?我遇到了同样的问题,唯一可行的方法是将期望包装到 try catch 中……对我的口味来说有点难看。这也可能是相关的; github.com/facebook/jest/issues/2059 【参考方案1】:另一个可能更清洁的解决方案,使用 async/await 并利用 jest/mocha 的能力来检测返回的 Promise:
// async test utility function
function currentEventLoopEnd()
return new Promise(resolve => setImmediate(resolve));
it('should render with container class after getting payload', async () =>
// mock the API call in a controllable way,
// starts out unresolved
let resolveGetPayload; // <- call this to resolve the API call
let getPayload = function()
return new Promise(function (resolve, reject)
resolveGetPayload = resolve;
);
// instanciate the component under state with the mock
const enzymeWrapper = mount(<MyComponent getPayload=getPayload />);
expect(enzymeWrapper.hasClass('container')).not.toBe(true);
resolveGetPayload(
fullname: 'Alex Paterson'
);
await currentEventLoopEnd(); // <-- clean and clear !
expect(enzymeWrapper.hasClass('container')).toBe(true);
);
【讨论】:
【参考方案2】:用 atm 的下一个方式克服这个问题(也解决了 componentDidMount 和 async setState 中 Enzyme 和异步调用的问题):
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();
);
);
(酶 3.2.0,Jest 21.1.6)
更新
刚刚想出了另一个更好(但仍然很奇怪)的解决方案,使用 async/await (它仍然解决异步 componentDidMount 和异步 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);
);
其他与异步相关的操作也应以await
为前缀,以及
await component.find('.spec-more-btn').simulate('click');
【讨论】:
我也使用await wrapper.update()
更新了代码。我注意到有些测试需要await
进行更新,而有些则不需要,即使我正在为类似的事情测试相同的组件。【参考方案3】:
一些注意事项;
process.nextTick
是异步的,所以 try/catch 将无法捕获它。
即使您在 Promise 中运行的代码是同步的,Promise
也会解析/拒绝异步。
试试这个
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();
);
);
【讨论】:
不应该是return fail(e)
而不是return done(e);
吗?
据我所知没有fail
方法,只有done
回调:facebook.github.io/jest/docs/en/asynchronous.html#callbacks
应该是done.fail(e)
。如果你这样做 done(e)
测试将通过 done()
不接受参数。 fail
函数记录在 Jasmine 页面上:done.fail function【参考方案4】:
正如其他人所展示的那样,将传递给process.nextTick
或setImmediate
的回调块包装在try
/catch
中是可行的,但这既冗长又令人分心。
更简洁的方法是在 async
测试回调中使用简短行 await new Promise(setImmediate);
刷新承诺。这是一个使用它来让useEffect
(对componentDidMount
同样有用)中的 HTTP 请求在运行断言之前解析并触发重新渲染的工作示例:
组件(LatestGist.js
):
import axios from "axios";
import React, 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>
</>
);
;
测试(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 "../src/components/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"]')
.hostNodes()
;
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"]')
.hostNodes()
;
expect(gist.exists()).toBe(true);
expect(gist.text()).toContain("test name");
expect(gist.text()).toContain("some date");
);
);
使用expect(gist.text()).toContain("foobar");
之类的行强制断言失败不会导致套件崩溃:
● 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)
这是我的依赖项:
"dependencies":
"axios": "^0.18.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
,
"devDependencies":
"enzyme": "3.9.0",
"enzyme-adapter-react-16": "1.12.1",
"jest": "24.7.1",
"jest-environment-jsdom": "24.7.1"
【讨论】:
【参考方案5】:继弗拉基米尔的回答 + 编辑之后,这里有一个对我有用的替代方案。而不是await
挂载,await
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);
);
【讨论】:
【参考方案6】:-- 作为答案发布不能格式化 cmets 中的代码块。 --
根据 Vladimir 的回答,请注意使用 async/await 也适用于 beforeEach:
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(…)
)
【讨论】:
以上是关于开玩笑:测试不能在 setImmediate 或 process.nextTick 回调中失败的主要内容,如果未能解决你的问题,请参考以下文章
`toBeInstanceOf(Number)` 不能开玩笑