我们的开发人员非常喜欢单元测试,其用途显而易见。为了使这些测试真正有用,并且不会带来痛苦,有必要确保其稳定性。
我们公司开发了Wasaby接口框架,并销售在此基础上构建的产品,即云和桌面应用程序。释放周期紧紧地固定在日历上,并设置了连续的积分过程以控制产品的质量。我们将Jenkins用于装配体,并将Mocha与Chai assert一起使用用于单元测试JavaScript代码。最近,我们面临着这样一种情况,即程序集监视开始显示,所有跌倒案例中约有一半是由于不稳定的JavaScript单元测试引起的。症状是相同的:从集中进行单独的测试要么没有时间运行,要么按预期返回了错误的结果。对案例的分析几乎总是揭示出这样一个事实,即包含对setTimeout或setInterval函数的调用的测试本身或在测试的代码中崩溃。关于在这种情况下该怎么办,我们将进一步讨论。
为什么会发生?
一切都很简单-在开发人员正在测试的代码内部,发生了一个异步调用,在此之后,环境状态以某种方式发生了变化。测试旨在验证此条件是否符合预期。但是由于 由于我们没有生活在理想的世界中,因此碰巧会为已经在战斗中工作的代码编写测试,而该代码最初并未为此类测试做好准备。开发人员必须决定如何测试这样的代码。
考虑一个简单的示例,测试代码是一个可异步更改实例状态的类方法。而且它写得并不完美:
export class Foo {
propToTest: boolean = false;
methodToTest(): void {
setTimeout(() => {
this.propToTest = true;
}, 100);
}
}
? , setTimeout . c , (, 101 100 ). :
it('should set propToTest to true', (done: Function) => {
const inst = new Foo();
inst.methodToTest();
setTimeout(() => {
assert.isTrue(inst.propToTest);
done();
}, 101);
});
? , , .. , callback, setTimeout, . — , 2 , . , . -, . , , - , — -.
setInterval.
, . , . , , .
- callback- , setTimeout/setInterval (" 1001 101, "). .
- setTimeout() (" event loop, "). .
- ( 1 2) — , , .
?
, , . , , : . .
— .. .
, , :
export function setProperState(inst: Foo) {
inst.propToTest = true;
}
export class Foo {
propToTest: boolean = false;
methodToTest(): void {
setTimeout(() => {
setProperState(this);
}, 100);
}
}
:
it('should set propToTest to true', () => {
const inst = new Foo();
assert.isFalse(inst.propToTest);
setProperState(inst);
assert.isTrue(inst.propToTest);
});
, setProperState methodToTest ( ). , : , . , , .
, , .
, setTimeout :
export class Foo {
propToTest: boolean = false;
constructor(readonly awaiter: Function = setTimeout) {
}
methodToTest(): void {
this.awaiter(() => {
this.propToTest = true;
}, 100);
}
}
-, :
it('should set propToTest to true', () => {
const awaiter = (callback) => callback();
const inst = new Foo(awaiter);
inst.methodToTest();
assert.isTrue(inst.propToTest);
});
, Promise.
, — , . . . :
export class Foo {
propToTest: boolean = false;
methodToTest(): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
this.propToTest = true;
resolve();
}, 100);
});
}
}
setTimeout:
it('should set propToTest to true', () => {
const inst = newFoo();
return inst.methodToTest().then(() => {
assert.isTrue(inst.propToTest);
});
});
, , , " ". setTimeout , .
export class Foo {
propToTest: boolean = false;
constructor(readonly asyncTimeout: number = 100) {
}
methodToTest(): void {
return new Promise((resolve) => {
setTimeout(() => {
this.propToTest = true;
resolve();
}, this.asyncTimeout);
});
}
}
.
fake timers.
Sinon.JS , . - ( ), setTimeout .
:
export class Foo {
propToTest: boolean = false;
methodToTest(): void {
setTimeout(() => {
this.propToTest = true;
}, 100);
}
}
fake timer, (!):
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
it('should set propToTest to true', () => {
const inst = newFoo();
inst.methodToTest();
clock.tick(101);
assert.isTrue(inst.propToTest);
});
" " clock.restore(), .
- , . - , . , , .