当业务逻辑依赖系统当前时间的时候,如何为这部分逻辑编写单元测试?当代码中使用了 setTimeout
或者 setInterval
,应该怎么去验证在一定时间后指定的代码是否会运行?当你使用了 AnimationPlayer
的动画完成时回调,你又该如何在单元测试中正确的断言这一行为?如果你面对这些问题感到一头雾水、不知所措,那么就接着读下去吧。
jasmine.clock()
使用 new Date()
是编写 JS 代码最常见不过的操作了,但是这在有些时候却会给我们的测试工作带来困难,例如下面的代码:
function isTodayMonday() {
return new Date().getDay() === 1;
}
要想完整的测试这个函数的输出的话,我们就必须分别在周一以及其他非周一的时间来运行测试,这很显然是不切合实际的,利用 jasmine.clock()
我们就可以在测试中随意设置当前时间:
beforeEach(() => {
jasmine.clock().install(); (1)
});
it('should return true in 2019/8/19', () => {
const baseTime = new Date(2019, 7, 19);
jasmine.clock().mockDate(baseTime); (2)
expect(isTodayMonday()).toBe(true);
});
afterEach(() => {
jasmine.clock().uninstall(); (3)
});
1 | 在接下来的测试中 mock Date 对象 |
2 | 设置 new Date() 的返回值 |
3 | 解除对 Date 对象的 mock |
除了 mock 当前时间之外,Jasmin Clock 还可以用来控制 setTimeout
这类定时器,更多的使用说明可以看这里:Jasmin Clock
fakeAsync
与 tick
除了 Jasmin Clock
之外,由 Angular 提供的 fakeAsync
与 tick
也可以用来非常方便的控制测试过程中定时器:
it('should call timeout callback', fakeAsync(() => {
timerCallback = jasmine.createSpy("timerCallback");
setTimeout(() => {
timerCallback();
}, 1000);
tick(100); (1)
expect(timerCallback).not.toHaveBeenCalled();
tick(900); (2)
expect(timerCallback).toHaveBeenCalled();
}));
1 | 时间流逝 100 ms |
2 | 时间流逝 900 ms |
所有在 fakeAsync
回调函数中的定时器以及 MicroTask 都会被一个特殊的 zone.js 作用域捕获,所以我们才能通过 tick
函数来让控制时间的流动,除了这些定时器之外,我们还可以用 fakeAsync
来控制其他的 MacroTask 的执行,具体的操作可以参考 Async test with fakeAsync
使用 fakeAsync
有几点需要注意:
-
每个
fakeAsync
的作用域仅限于回调函数之中,我们没法在一个fakeAsync
中控制另一个fakeAsync
中调用的异步任务 -
fakeAsync
不能用作describe
的参数,因为describe
只是用来对测试进行分组的 API -
确保在每个测试执行结束前,没有等待运行的定时器或者 MicroTask,否则你就会遇到这样的错误:
Error: x timer(s) still in the queue.
不过你可能还注意到了一点,如果我们使用了 setInterval
,仅仅使用 tick
是无法在测试结束前清除所有的定时器的,这个时候你可能会遇到这个错误:
Error: x periodic timer(s) still in the queue.
这个时候只要使用 discardPeriodicTasks()
就好了。
AnimationPlayer 的回调函数
Angular4 给我们带来了灵活的 AnimationPlayer
,如果在单元测试中需要使用类似 AnimationPlayer.onDone
这种回调,NoopAnimationModule
就可以帮我们的忙。
首先需要在测试环境导入相关的模块:
TestBed.configureTestingModule({
declarations: [
// ...
],
imports: [NoopAnimationsModule],
})
导入之后,测试过程中的 player 就会被替换成 NoopAnimationPlayer
的实现,这个类并不会在播放动画的时候产生任何对定时器的调用,但是它会安排一个 MicroTask 到队列中,所以,我们只需要简单地调用 flushMicroTasks()
这个函数就可以触发动画完成时的回调函数了。
当然,如果你需要更加精细的控制,你可以使用 MockAnimaitonPlayer
:
TestBed.configureTestingModule({
declarations: [
// ...
],
imports: [NoopAnimationsModule],
providers: [
{ provide: AnimationDriver, useClass: MockAnimationDriver }
]
})
这样,我们在测试的过程中就可以通过 MockAnimationDriver
来获取当前正在运行的 AnimationPlayer
:
const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
player.finish(); (1)
1 | 手动调用 player 的 onDone 回调函数 |
以上就是这篇文章全部的内容,虽然这并不是在测试中用来控制时间变量的全部手段,但是我相信这些方法肯定可以帮你解决绝大多数的问题。