为什么我的测试在我的(酶模拟事件)同步事件处理程序之前完成?
Why does my test finish before my (enzyme simulated event) synchronous event handler?
我有一个基于 mocha 的测试,它在我的 React 组件的基于 jsdom 的酶测试中的 onChange 处理程序之前完成,尽管该处理程序使用 babel+ES2017 同步。如果我执行 1ms 的 setTimeout()
来放入我的 expect()
调用;测试 通过 。
只是想知道故障在哪里?我确定这里有一些我没有考虑的简单概念。我在想 jsdom 或酶不会等待事件处理程序完成?使用 fetch-mock
模拟 fetch()
的时间长度使问题复杂化(因为它通常是异步的)。
没有setTimeout()
、sinon
或lolex
是否可以解析,如果不能; simon
/ lolex
有可能吗?
明天我希望重构它以避免在测试中模拟 fetch()。
测试输出
</div>
1) flashes a nice message upon success
Success now!!
End of function now.
10 passing (4s)
1 failing
1) <Signup /> flashes a nice message upon success:
Uncaught AssertionError: expected { Object (root, unrendered, ...) } to have a length of 1 but got 0
at test/integration/jsx/components/signup.test.js:38:54
at _combinedTickCallback (internal/process/next_tick.js:67:7)
at process._tickDomainCallback (internal/process/next_tick.js:122:9)
Bootstrap
require('babel-register')();
require('babel-polyfill');
...
var jsdom = require('jsdom').jsdom;
var exposedProperties = ['window', 'navigator', 'document'];
global.document = jsdom('');
global.window = document.defaultView;
global.FormData = document.defaultView.FormData;
Object.keys(document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
exposedProperties.push(property);
global[property] = document.defaultView[property];
}
});
global.navigator = {
userAgent: 'node.js'
};
documentRef = document;
测试
import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import Signup from '../../../../assets/js/components/signup.jsx';
import fetchMock from 'fetch-mock';
import sinon from 'sinon';
import 'isomorphic-fetch';
...
it("flashes a nice message upon success", function(){
fetchMock.mock("*", {body: {}});
const wrapper = shallow(<Signup />);
wrapper.find('#email').simulate('change', {target: {id: 'email', value: validUser.email}});
const signupEvent = {preventDefault: sinon.spy()};
wrapper.find('#signupForm').simulate('submit', signupEvent);
wrapper.update();
console.log(wrapper.debug());
expect(signupEvent.preventDefault.calledOnce).to.be.true;
expect(wrapper.find('.alert-success')).to.have.length(1);
expect(wrapper.find('.alert-success').text()).to.contain('Your sign up was successful!');
fetchMock.restore();
});
组件
async handleSubmit(e) {
e.preventDefault();
this.setState({ type: 'info', message: 'Sending...', shouldValidateForm: true });
let form = new FormData(this.form);
let response;
let responseJson = {};
try {
response = await fetch("/signup", {
method: "POST",
body: form
});
responseJson = await response.json();
if(!response.ok){
throw new Error("There was a non networking error. ");
}
this.setState({ type: 'success', message: 'Your sign up was successful!' });
console.log("Success now!!");
} catch(err) {
this.setState({ type: 'danger', message: "There was a technical problem. "});
}
console.log("End of function now. ");
}
...
<form method="POST" onSubmit={this.handleSubmit} ref={(form) => {this.form = form;} } id="signupForm">
我的第一个回答集中在 simulate
的异步性质上,但从评论中可以清楚地看出酶对该方法 is not asynchronous 的实现,因为它只是同步调用点击处理程序。所以这是对我的回答的重写,着重于异步行为的其他原因。
本次测试:
expect(wrapper.find('.alert-success')).to.have.length(1);
... 失败,因为那时还没有执行以下行:
this.setState({ type: 'success', message: 'Your sign up was successful!' });
我假设此 setState
调用会将 alert-success
class 添加到消息元素。
要查看为什么尚未设置此状态,请考虑执行流程:
wrapper.find('#signupForm').simulate('submit', signupEvent);
这将触发在表单的 onsubmit
属性中指定的内容:
onSubmit={this.handleSubmit}
所以调用了handleSubmit
。然后设置一个状态:
this.setState({ type: 'info', message: 'Sending...', shouldValidateForm: true });
...但这不是您需要的状态:它不添加 alert-success
class。然后进行 Ajax 调用:
response = await fetch("/signup", {
method: "POST",
body: form
});
fetch
returns 一个承诺,await
将暂停函数的执行,直到该承诺得到解决。同时,在 调用 handleSubmit
之后 将继续执行任何代码。在这种情况下,这意味着您的测试将继续,并最终执行:
expect(wrapper.find('.alert-success')).to.have.length(1);
...失败了。表示挂起的 Ajax 请求有响应的事件可能已经到达事件队列,但只有在当前执行的代码完成后才会被处理。所以 在 测试失败后,fetch
返回的承诺得到解决。这是因为 fetch
的内部实现有一个回调通知响应已经到达,因此它解决了承诺。这使得函数 handleSubmit
"wake up",因为 await
现在解除阻塞执行。
第二个 await
用于获取 JSON,这将再次引入事件队列循环。最终(双关语不是故意的),代码将恢复并执行测试正在寻找的状态:
this.setState({ type: 'success', message: 'Your sign up was successful!' });
所以...要使测试成功,它必须实现异步回调,等待足够长的时间让 Ajax 调用获得响应。
这可以通过 setTimeout(done, ms)
完成,其中 ms
应该是足够长的毫秒数以确保 Ajax 响应可用。
在我看来,与 ReactTestUtils
(@trincot 的答案基于此)不同,酶的 simulate()
实际上是 同步。然而,我对 fetch()
的模拟调用是 异步 并且承诺在下一个事件循环中解决。将期望或断言包装在 setTimeout(()=>done(), 0)
中应该就足够了,也许比 setImmediate()
更可靠,后者似乎比 setTimeout()
对我来说具有更高的优先级(即使它们都可能在相同的事件循环)。
这是我写的一个组件和测试来演示。
测试输出
<Example />
updated asynchronously
onChangeError ran.
SUCCESS SOON: Taking a break...
Setting delayed success.
✓ has a rendered success message on the next event loop
updated synchronously
onChangeError ran.
Setting success.
✓ has a rendered success message on this loop
onChangeError ran.
onChangeError ran.
onChangeError ran.
...
onChangeError ran.
onChangeError ran.
onChangeError ran.
Setting success.
✓ has a rendered success message on this loop despite a large simulation workload (2545ms)
3 passing (6s)
组件
import React from 'react';
export default class Example extends React.Component {
constructor(props){
super(props);
this.onChangeError = this.onChangeError.bind(this);
this.onChangeSuccess = this.onChangeSuccess.bind(this);
this.onChangeDelayedSuccess = this.onChangeDelayedSuccess.bind(this);
this.state = { message: "Initial message. " };
}
onChangeError(e){
console.log("onChangeError ran. ");
this.setState({message: "Error: There was an error. "})
}
onChangeSuccess(e) {
console.log("Setting success. ");
this.setState({message: "The thing was a success!"});
};
onChangeDelayedSuccess(e){
console.log('SUCCESS SOON: Taking a break...');
setTimeout(() =>{
console.log("Setting delayed success. ");
this.setState({message: "The thing was a success!"});
}, 0);
}
render(){
return(
<div>
<p>{ this.state.message}</p>
<input type="text" id="forceError" onChange={this.onChangeError} />
<input type="text" id="forceSuccess" onChange={this.onChangeSuccess} />
<input type="text" id="forceDelayedSuccess" onChange={this.onChangeDelayedSuccess} />
</div>
);
}
}
测试
import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import Example from '../../../../assets/js/components/example.jsx';
describe("<Example />", function() {
describe("updated asynchronously", function() {
it("has a rendered success message on the next event loop ", function(done) {
const wrapper = shallow(<Example />);
wrapper.find('#forceError').simulate('change', {target: {value: ""}});
wrapper.find('#forceDelayedSuccess').simulate('change', {target: {value: ""}});
setTimeout(function(){
expect(wrapper.find('p').text()).to.contain('The thing was a success!');
done();
}, 0);
});
});
describe("updated synchronously", function(){
it("has a rendered success message on this loop", function(done) {
const wrapper = shallow(<Example />);
wrapper.find('#forceError').simulate('change', {target: {value: ""}});
wrapper.find('#forceSuccess').simulate('change', {target: {value: ""}});
expect(wrapper.find('p').text()).to.contain('The thing was a success!');
done();
});
it("has a rendered success message on this loop despite a large simulation workload", function(done) {
this.timeout(100000);
const wrapper = shallow(<Example />);
for(var i=1; i<=10000;i++){
wrapper.find('#forceError').simulate('change', {target: {value: ""}});
}
wrapper.find('#forceSuccess').simulate('change', {target: {value: ""}});
expect(wrapper.find('p').text()).to.contain('The thing was a success!');
done();
});
});
});
我有一个基于 mocha 的测试,它在我的 React 组件的基于 jsdom 的酶测试中的 onChange 处理程序之前完成,尽管该处理程序使用 babel+ES2017 同步。如果我执行 1ms 的 setTimeout()
来放入我的 expect()
调用;测试 通过 。
只是想知道故障在哪里?我确定这里有一些我没有考虑的简单概念。我在想 jsdom 或酶不会等待事件处理程序完成?使用 fetch-mock
模拟 fetch()
的时间长度使问题复杂化(因为它通常是异步的)。
没有setTimeout()
、sinon
或lolex
是否可以解析,如果不能; simon
/ lolex
有可能吗?
明天我希望重构它以避免在测试中模拟 fetch()。
测试输出
</div>
1) flashes a nice message upon success
Success now!!
End of function now.
10 passing (4s)
1 failing
1) <Signup /> flashes a nice message upon success:
Uncaught AssertionError: expected { Object (root, unrendered, ...) } to have a length of 1 but got 0
at test/integration/jsx/components/signup.test.js:38:54
at _combinedTickCallback (internal/process/next_tick.js:67:7)
at process._tickDomainCallback (internal/process/next_tick.js:122:9)
Bootstrap
require('babel-register')();
require('babel-polyfill');
...
var jsdom = require('jsdom').jsdom;
var exposedProperties = ['window', 'navigator', 'document'];
global.document = jsdom('');
global.window = document.defaultView;
global.FormData = document.defaultView.FormData;
Object.keys(document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
exposedProperties.push(property);
global[property] = document.defaultView[property];
}
});
global.navigator = {
userAgent: 'node.js'
};
documentRef = document;
测试
import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import Signup from '../../../../assets/js/components/signup.jsx';
import fetchMock from 'fetch-mock';
import sinon from 'sinon';
import 'isomorphic-fetch';
...
it("flashes a nice message upon success", function(){
fetchMock.mock("*", {body: {}});
const wrapper = shallow(<Signup />);
wrapper.find('#email').simulate('change', {target: {id: 'email', value: validUser.email}});
const signupEvent = {preventDefault: sinon.spy()};
wrapper.find('#signupForm').simulate('submit', signupEvent);
wrapper.update();
console.log(wrapper.debug());
expect(signupEvent.preventDefault.calledOnce).to.be.true;
expect(wrapper.find('.alert-success')).to.have.length(1);
expect(wrapper.find('.alert-success').text()).to.contain('Your sign up was successful!');
fetchMock.restore();
});
组件
async handleSubmit(e) {
e.preventDefault();
this.setState({ type: 'info', message: 'Sending...', shouldValidateForm: true });
let form = new FormData(this.form);
let response;
let responseJson = {};
try {
response = await fetch("/signup", {
method: "POST",
body: form
});
responseJson = await response.json();
if(!response.ok){
throw new Error("There was a non networking error. ");
}
this.setState({ type: 'success', message: 'Your sign up was successful!' });
console.log("Success now!!");
} catch(err) {
this.setState({ type: 'danger', message: "There was a technical problem. "});
}
console.log("End of function now. ");
}
...
<form method="POST" onSubmit={this.handleSubmit} ref={(form) => {this.form = form;} } id="signupForm">
我的第一个回答集中在 simulate
的异步性质上,但从评论中可以清楚地看出酶对该方法 is not asynchronous 的实现,因为它只是同步调用点击处理程序。所以这是对我的回答的重写,着重于异步行为的其他原因。
本次测试:
expect(wrapper.find('.alert-success')).to.have.length(1);
... 失败,因为那时还没有执行以下行:
this.setState({ type: 'success', message: 'Your sign up was successful!' });
我假设此 setState
调用会将 alert-success
class 添加到消息元素。
要查看为什么尚未设置此状态,请考虑执行流程:
wrapper.find('#signupForm').simulate('submit', signupEvent);
这将触发在表单的 onsubmit
属性中指定的内容:
onSubmit={this.handleSubmit}
所以调用了handleSubmit
。然后设置一个状态:
this.setState({ type: 'info', message: 'Sending...', shouldValidateForm: true });
...但这不是您需要的状态:它不添加 alert-success
class。然后进行 Ajax 调用:
response = await fetch("/signup", {
method: "POST",
body: form
});
fetch
returns 一个承诺,await
将暂停函数的执行,直到该承诺得到解决。同时,在 调用 handleSubmit
之后 将继续执行任何代码。在这种情况下,这意味着您的测试将继续,并最终执行:
expect(wrapper.find('.alert-success')).to.have.length(1);
...失败了。表示挂起的 Ajax 请求有响应的事件可能已经到达事件队列,但只有在当前执行的代码完成后才会被处理。所以 在 测试失败后,fetch
返回的承诺得到解决。这是因为 fetch
的内部实现有一个回调通知响应已经到达,因此它解决了承诺。这使得函数 handleSubmit
"wake up",因为 await
现在解除阻塞执行。
第二个 await
用于获取 JSON,这将再次引入事件队列循环。最终(双关语不是故意的),代码将恢复并执行测试正在寻找的状态:
this.setState({ type: 'success', message: 'Your sign up was successful!' });
所以...要使测试成功,它必须实现异步回调,等待足够长的时间让 Ajax 调用获得响应。
这可以通过 setTimeout(done, ms)
完成,其中 ms
应该是足够长的毫秒数以确保 Ajax 响应可用。
在我看来,与 ReactTestUtils
(@trincot 的答案基于此)不同,酶的 simulate()
实际上是 同步。然而,我对 fetch()
的模拟调用是 异步 并且承诺在下一个事件循环中解决。将期望或断言包装在 setTimeout(()=>done(), 0)
中应该就足够了,也许比 setImmediate()
更可靠,后者似乎比 setTimeout()
对我来说具有更高的优先级(即使它们都可能在相同的事件循环)。
这是我写的一个组件和测试来演示。
测试输出
<Example />
updated asynchronously
onChangeError ran.
SUCCESS SOON: Taking a break...
Setting delayed success.
✓ has a rendered success message on the next event loop
updated synchronously
onChangeError ran.
Setting success.
✓ has a rendered success message on this loop
onChangeError ran.
onChangeError ran.
onChangeError ran.
...
onChangeError ran.
onChangeError ran.
onChangeError ran.
Setting success.
✓ has a rendered success message on this loop despite a large simulation workload (2545ms)
3 passing (6s)
组件
import React from 'react';
export default class Example extends React.Component {
constructor(props){
super(props);
this.onChangeError = this.onChangeError.bind(this);
this.onChangeSuccess = this.onChangeSuccess.bind(this);
this.onChangeDelayedSuccess = this.onChangeDelayedSuccess.bind(this);
this.state = { message: "Initial message. " };
}
onChangeError(e){
console.log("onChangeError ran. ");
this.setState({message: "Error: There was an error. "})
}
onChangeSuccess(e) {
console.log("Setting success. ");
this.setState({message: "The thing was a success!"});
};
onChangeDelayedSuccess(e){
console.log('SUCCESS SOON: Taking a break...');
setTimeout(() =>{
console.log("Setting delayed success. ");
this.setState({message: "The thing was a success!"});
}, 0);
}
render(){
return(
<div>
<p>{ this.state.message}</p>
<input type="text" id="forceError" onChange={this.onChangeError} />
<input type="text" id="forceSuccess" onChange={this.onChangeSuccess} />
<input type="text" id="forceDelayedSuccess" onChange={this.onChangeDelayedSuccess} />
</div>
);
}
}
测试
import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import Example from '../../../../assets/js/components/example.jsx';
describe("<Example />", function() {
describe("updated asynchronously", function() {
it("has a rendered success message on the next event loop ", function(done) {
const wrapper = shallow(<Example />);
wrapper.find('#forceError').simulate('change', {target: {value: ""}});
wrapper.find('#forceDelayedSuccess').simulate('change', {target: {value: ""}});
setTimeout(function(){
expect(wrapper.find('p').text()).to.contain('The thing was a success!');
done();
}, 0);
});
});
describe("updated synchronously", function(){
it("has a rendered success message on this loop", function(done) {
const wrapper = shallow(<Example />);
wrapper.find('#forceError').simulate('change', {target: {value: ""}});
wrapper.find('#forceSuccess').simulate('change', {target: {value: ""}});
expect(wrapper.find('p').text()).to.contain('The thing was a success!');
done();
});
it("has a rendered success message on this loop despite a large simulation workload", function(done) {
this.timeout(100000);
const wrapper = shallow(<Example />);
for(var i=1; i<=10000;i++){
wrapper.find('#forceError').simulate('change', {target: {value: ""}});
}
wrapper.find('#forceSuccess').simulate('change', {target: {value: ""}});
expect(wrapper.find('p').text()).to.contain('The thing was a success!');
done();
});
});
});