Test async task with Angular + Jest
在前端开发中,Unit Test是很重要的一个环节,而异步任务测试又是Unit Test不可避免的一个环节。本文将介绍如何使用Angular + Jest来测试异步任务。
待测试函数返回一个Promise
这是最简单的情况了,直接使用async/await
即可。
待测函数
1 | getPromise() { |
测试代码
1 | it('test getPromise', async () => { |
在实际项目中不会有这么简单的情况,大部分都是一个async函数,里面await了其他异步操作,比如下面代码中的handleData,我们该如何测试它呢?
待测函数
1 | getData() { |
首先来分析一下,handleData
是一个async方法,而async方法一定返回一个Promise(如果函数实际返回值不是Promise,那么async方法会用Promise包裹该返回值),所以我们还是可以直接使用async/await
来测试。
测试代码
1 | it('test handle data', async () => { |
最新版的Angular推荐使用waitForAsync
来测试异步任务,所以我们也可以使用waitForAsync来测试。(注意waitForAsync
中不能使用await。)
测试代码
1 | it('test handle data', waitForAsync(() => { |
当然也可以使用fakeAsync
来测试,这个情景使用fakeAsync
来测试有点大材小用,仅作示例。
1 | it('test handle data', fakeAsync(() => { |
注意不要忘记flush()
操作,否则会产生如下错误:Error: 1 timer(s) still in the queue.
待测试函数包含异步操作,但是没有返回Promise。
以上情况还是有些简单,实际应用中,经常是一个函数中间夹杂着某些异步操作用来获取数据,然后对数据进行处理,最后可能也不返回Promise,对于这种情况,我们应该使用fakeAsync
来测试。
fakeAsync内部有三个方法可以控制异步操作
- tick: 让时间快进
- flush: 让所有异步操作立即执行
- flushMicrotasks: 让所有微任务立即执行
name | 作用 | 使用场景 |
---|---|---|
tick | 用于控制时间流逝 | 想要精细控制每个timer的执行时机,tick在执行前会清空micro task队列,如果代码中有promise,tick之后,promise都会执行完毕。 |
flush | 执行所有异步操作,不论是macro task还是micro task | 这个最常用,无脑操作,将所有异步操作执行完,比如setTimeout和promise等,flush之后就可以expect了 |
flushMicrotasks | 这个只执行micro task,对于前端来说,就是promise了,不会影响timer | 如果代码中没有用到timer,可以使用这个。 |
下面看几个列子,分别讲解一下如何使用这三个方法来进行测试。
待测试函数只包含Macro Task
待测函数
1 | updateValue() { |
测试代码
1 | it('test updateValue', fakeAsync(() => { |
来分析一下以上测试代码,首先我们调用了updateValue
方法,然后期望num
的值为10,因为updateValue
中有两个timer,所以我们需要调用两次tick
,第一次调用tick
,让时间快进100ms,这时候第一个timer会执行,num
的值变为11,然后再调用一次tick
,让时间再快进100ms,这时候第二个timer会执行,num
的值变为12。
当然了,如果你不想测试中间过程,而只想测试最终的结果,也可以使用Jest提供的useFakeTimer
方法。
测试代码
1 | it('test updateValue', () => { |
useFakeTimers
会将所有的timer替换成一个fake的timer,然后调用runAllTimers
,会让所有的timer立即执行,这样就可以直接测试最终结果了。
待测试函数只包含Micro Task
待测函数
1 | fetchData() { |
测试代码
1 | it('test updateValue', fakeAsync(() => { |
上述代码中,首先调用了updateValue
方法,然后期望num
的值为0,因为updateValue
中有一个promise,所以我们需要调用flushMicrotasks
,让所有的micro task立即执行,这时候num
的值变为10。
当然上例中的flushMicrotasks
也可以替换成flush
,因为flush
会让所有的异步操作立即执行,包括macro task和micro task。也可以使用tick,因为tick在执行之前也会先把微任务队列清空(把队列中的微任务都执行完)。
待测试函数同时包含Macro Task和Micro Task
待测函数
1 | fetchData() { |
测试代码
1 | it('test updateValue', fakeAsync(() => { |
上述代码中,fetchData()
中同时包含Macro task和Micro task,所以我们可以使用tick
或者flush
来测试。但是使用flushMicrotasks
就不行了,因为flushMicrotasks
只会让micro task立即执行,而setTimeout
是macro task,不会被执行。
Limitation: The
fakeAsync()
function won’t work if the test body makes an XMLHttpRequest (XHR) call. XHR calls within a test are rare, but if you need to call XHR, see thewaitForAsync()
section.
需要更新UI的异步测试
上面的测试用例都不涉及UI更新,如果需要测试UI更新,那么需要使用fixture.detectChanges()
来触发Angular的变更检测。
fixture.WhenStable()
这个方法也是用来处理异步任务的,可以稍后总结一下。fixture.WhenStable()通常和waitForAsync
一起配合使用。
1 | it('should show quote after getQuote (waitForAsync)', waitForAsync(() => { |
总结
- 如果函数返回一个Promise,那么直接使用
async/await
测试即可。新版的Angular推荐使用waitForAsync
来测试。 - 如果函数中间夹杂着异步操作,但是没有返回Promise,那么分为以下三种情况
- 待测试函数只包含微任务 - 使用
fakeAsync
配合flushMicrotasks
来控制异步操作。 - 待测试函数只包含宏任务 - 使用
fakeAsync
配合tick
或者flush
来控制异步操作。 - 待测试函数同时包含微任务与宏任务 - 使用
fakeAsync
配合tick
或者flush
来控制异步操作。
- 待测试函数只包含微任务 - 使用
- 能用
async/await
或者waitForAsync
测试的一定能用fakeAsync
测试,反之不成立。
最终结论: 在Angular + Jest为基础的项目中,使用fakeAsync
+ tick
/flush
能搞定所有的异步测试。