我正在为 React 应用程序编写集成测试,即一起测试许多组件的测试,并且我想模拟对外部服务的任何调用。
问题是测试似乎在异步回调执行之前执行,导致我的测试失败。
这附近还有吗?我可以以某种方式等待调用异步代码完成吗?
这里有一些糟糕的伪代码来说明我的观点。
我想测试一下,当我安装Parent时,它的Child组件渲染从外部服务返回的内容,我将模拟它。
class Parent extends component {
render() {
<div>
<Child />
</div>;
}
}
class Child extends component {
DoStuff() {
aThingThatReturnsAPromise().then((result) => {
Store.Result = result;
});
}
render() {
DoStuff();
return <div>{Store.Result}</div>;
}
}
function aThingThatReturnsAPromise() {
return new Promise((resolve) => {
eternalService.doSomething(function callback(result) {
resolve(result);
});
});
}
当我在测试中执行此操作时,它失败了,因为它在回调被触发之前执行。
jest.mock('eternalService', () => {
return jest.fn(() => {
return { doSomething: jest.fn((cb) => cb('fakeReturnValue');
});
});
describe('When rendering Parent', () => {
var parent;
beforeAll(() => {
parent = mount(<Parent />)
});
it('should display Child with response of the service', () => {
expect(parent.html()).toMatch('fakeReturnValue')
});
});
我如何测试这个?我知道 Angular 使用 zonejs 解决了这个问题,React 中是否有等效的方法?
更新了 Jest 27+
对于 jest 27+,您还可以使用 process.nextTick:
await new Promise(process.nextTick);
(感谢评论中的 Adrian Godong)
原答案
这是等待待处理的 Promise 得到解决的代码片段:
const flushPromises = () => new Promise(setImmediate);
请注意,setImmediate是一个非标准功能(预计不会成为标准)。但如果它足以满足您的测试环境,那么应该是一个很好的解决方案。它的描述:
此方法用于分解长时间运行的操作,并在浏览器完成其他操作(例如事件和显示更新)后立即运行回调函数。
这里大致介绍如何使用 async/await 来使用它:
it('is an example using flushPromises', async () => {
const wrapper = mount(<App/>);
await flushPromises();
wrapper.update(); // In my experience, Enzyme didn't always facilitate component updates based on state changes resulting from Promises -- hence this forced re-render
// make assertions
});
flushPromises
方法在某些情况下已失效。
只需使用await Promise.resolve()
即可:
const component = mount(<App/>);
component.find('<button>').simulate('click');
// State changes
await Promise.resolve();
// Assert changes that occurred on the component
aThingThatReturnsAPromise()
,然后将其导入到您的测试用例中。
由于
aThingThatReturnsAPromise()
返回一个 Promise,您可以利用 Jest 的异步测试功能。 Jest 会等待你的承诺得到解决,然后你就可以做出你的断言。
describe('When rendering Parent', () => {
var parent;
beforeAll(() => {
parent = mount(<Parent />)
});
it('should display Child with response of the service', () => {
expect.assertions(1);
return aThingThatReturnsAPromise().then( () => {
expect(parent.html()).toMatch('fakeReturnValue');
});
});
});
有关更多信息,请阅读 Jest 文档中的 Jest 如何使用 Promises 处理测试用例此处
。下面显示了包含两个测试的示例测试套件(也如引用的网址所示):
const flushPromises = require('flush-promises');
describe('Async Promise Test Suite', () => {
it('A test involving flushPromises', async () => {
const wrapper = mount(<App/>);
await flushPromises();
// more code
});
it('Will not work correctly without flushing promises', async () => {
let a;
let b;
Promise.resolve().then(() => {
a = 1;
}).then(() => {
b = 2;
})
await flushPromises();
expect(a).toBe(1);
expect(b).toBe(2);
});
});
但是,我可以通过在设置完成后调用 beforeAll() 的 @done 以类似的代码完成此操作。请参阅下面对代码的更改:
let setupComplete;
jest.mock('eternalService', () => {
return jest.fn(() => {
return { doSomething: jest.fn((cb) => { cb('fakeReturnValue'); setupComplete(); }) };
});
.
.
.
beforeAll(done => {
parent = mount(<Parent />)
setupComplete = done;
});
});
我从未使用过它们,但潜在的兴趣是 Jest 的
runAllTickscomponentWillMount()
componentDidMount()
),但测试起来会容易得多。但是,下面是我未经测试的伪代码,对您的测试代码进行了更改,请随意测试出来更新一下。希望对你有帮助!describe('When rendering Parent', () => {
it('should display Child with the response of the service', function(done) => {
const parent = mount(<Parent />);
expect(parent.html()).toMatch('fakeReturnValue');
done();
});
});
await jest.runAllTimersAsync();
将等待所有超时并承诺解决!
await Promise.resolve();
一个更通用的解决方案是用测试库中的
render
包装您的
await act
函数,如下所示:let component;
await act(()=>{
component = render(<Whatever />);
});
此后,在任何
useEffect
链中创建的所有承诺,甚至是改变状态的
useEffect
(响应 fetch
等),导致更多 useEffect
运行,从而执行更多 fetch
es 或其他 —— 所有的承诺都将得到解决并准备好断言,而无需异步机制,例如测试库的 waitFor
、findBy*
等。