目录:网上冲浪指南

如何为 Angular 应用编写时间相关的单元测试

2019/10/21

当业务逻辑依赖系统当前时间的时候,如何为这部分逻辑编写单元测试?当代码中使用了 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

fakeAsynctick

除了 Jasmin Clock 之外,由 Angular 提供的 fakeAsynctick 也可以用来非常方便的控制测试过程中定时器:

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 有几点需要注意:

  1. 每个 fakeAsync 的作用域仅限于回调函数之中,我们没法在一个 fakeAsync 中控制另一个 fakeAsync 中调用的异步任务

  2. fakeAsync 不能用作 describe 的参数,因为 describe 只是用来对测试进行分组的 API

  3. 确保在每个测试执行结束前,没有等待运行的定时器或者 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 回调函数

以上就是这篇文章全部的内容,虽然这并不是在测试中用来控制时间变量的全部手段,但是我相信这些方法肯定可以帮你解决绝大多数的问题。