jasmine clock setTimout详解

使用jasmine作为前端框架进行单元测试,比较坑爹的是当程序中有异步处理,或者模拟、类似异步处理的时候,比较难进行控制。今天就来详细讨论一下clock和setTimeout两种不同的方法在处理不同的情况下的用法。

使用clock模拟时钟快速调用异步操作

clock是jasmine里面想象力非常赞的一个功能,它能模拟时钟,快速推进(甚至倒退)程序中的模拟时钟,从而让setTimeout、setInterval在你的模拟时钟里面按照你的想象进行。

比如你写了这样一段代码:

setTimeout(() => console.log("ok"), 30*60*1000) // 三十分钟后执行

如果在正常的浏览器时钟中执行这段代码,你肯定得等30分钟才能看到控制台输出“OK”。但是,通过模拟时间,你能使这个时间在模拟状态下进行控制(五维空间的感觉)。

it("test timeout", () => {
    let test = 0
    jasmine.clock().install() // 安装模拟时钟,会对setTimeout和setInterval进行重写
    setTimeout(() => test = 1, 30*60*1000)

    console.log(test) // 0
    jasmine.clock().tick(30*60*1000)
    console.log(test) // 1
})

上面这段代码里面,给了一个test变量,按照setTimeout的设定,test要在半个小时之后才会变成1,结果把上面这段代码放到jasmine的it测试用例中去执行,秒变1了,为什么?关键就在jasmine.clock().tick(30*60*1000)。在前面执行了jasmine.clock().install()之后,setTimeout就不是原来的setTimeout了,执行jasmine.clock().tick(30*60*1000)之后相当于时间已经过了半个小时,所以setTimeout里面的回调函数被执行了,所以test变成了1,所以下面的test输出为1.

简单的说,clock机制就是重写setTimeout,在调用tick的时候,把模拟时钟的时间按照自己的意愿推进,如果推进过程中超过了setTimeout设定的时间,setTimeout的回调函数就会被执行。为了验证这点,jasmine提供了jasmine.createSpy方法作为监控助手。你可以通过timerCallback.calls.count()来查看回调函数被执行的次数:

it("test timeout", () => {
    let timerCallback = jasmine.createSpy("timerCallback")
    let test = 0
    jasmine.clock().install() // 安装模拟时钟,会对setTimeout和setInterval进行重写
    setInterval(() => test ++, 30*60*1000)

    console.log(test) // 0
    jasmine.clock().tick(20*60*1000) // 时间没到,没有调用
    console.log(test) // 0
    console.log(timerCallback.calls.count()) // 0
    jasmine.clock().tick(30*60*1000)
    console.log(test) // 1
    console.log(timerCallback.calls.count()) // 1,显示出回调函数被执行的次数,可以重复调用tick来看执行的次数
})

那么,jasmine是如何实现clock功能的呢?我们还要去看下源码。这里是clock的源码,可以看到,jasmine确实对setTimeout进行了封装,另外,使用了一个叫delayedFunctionSchedulerFactory的模块,来实现对回调函数列队的存取和调用。认真阅读这两个模块之后,就可以了解jasmine实现clock的原理。

jasmine中的异步回调测试

从上面你可以看到,clock实际上是想把setTimeout等基于时间的几个函数转换为同步执行,也就是说它解决了一个时间问题。但是,有的时候,程序必须异步执行,比如ajax,或者异步加载一些数据(promise),甚至setTimeout(fun, 0),这些异步的动作,如果简单按照我们开发中的思路进行的话,就会出问题,测试就会失败。举一个栗子:

it("test data", () => {
    let content = ""
    $.get("your url").then(data => content = data)
    expect(content).not.toBe("")
})

我们虽然使用了ajaxMock,对jquery的ajax请求进行了模拟,但是由于使用的是deffered,所以这个请求仍然是模拟的异步操作。当expect执行的时候,$.get的then可能还没有执行,因此可能就会得到错误的测试结果。

jasmine解决异步测试的方法非常简单,就是在it的回调函数中传入参数作为标记,比如把上面的代码改为:

it("test data", done => {
    let content = ""
    $.get("your url").then(data => {
        content = data
        expect(content).not.toBe("")
        done()
    })
})

当done被传入之后,只有调用done时,jasmine才认为这个测试用例结束,否则这个测试用例将处于等待状态。

不过有一个点需要注意,就是setTimeout异步测试的时候,不能和clock一起使用,比如:

beforeEach(() => jasmine.clock().install())
it("test async", done => {
    $.get(...).then(...)
    setTimeout(() => {
        expect(...).toBe(...)
        done()
    }, 1000)
})
afterEach(() => jasmine.clock().uninstall())

上面这段代码会报错!!!原因很简单,因为clock需要通过tick来推进时间,而上面代码中时间将永远禁止。

既然如此,那加上tick不就好了吗?对不起,仍然会失败(不报错)。前文已经说明,tick其实是调用回调函数,而立即调用回调函数,$.get方法请求的数据其实还没有回来,所以你expect中的值是原始值,不会得到正确的结果。所以,如果是使用ajax,不要用timeout的方式去expect,而是应该写在ajax的回调函数中,采用done作为回调通知。

jasmine提供了一个ajax的解决方案,和clock非常像,也是对XMLHttpRequest进行重写,也采用jasmine.Ajax.install()这种形式。不过它不是jasmine-core的内容,而是以插件的形式挂载到jasmine中去。

jasmine1.3供了两个函数,runs和waitsFor来实现,但是2.0开始使用done的方式异步通知方便的多。

总结

本文总结了jasmine中clock和异步的两个问题。简单的说,clock主要用在“不想等待,直接快进时间”,而异步则主要用在“需要等待”。使用clock不能把它当setTimeout来用,实际上相反,clock是把setTimeout同步执行,是把setTimeout作为其他程序进行的依赖来处理,比如:

setTimeout(() => a = 1, 1000)
setTimeout(() => b = a + 2, 200)

上面这段代码,第二个setTimeout依赖于第一个,所以这个时候使用clock快进时间就很爽,但是绝不能把它用在异步操作中,clock无法让ajax的请求变快,ajax请求仍然是自己的那个节奏,如果你在请求还没回来的时候就直接通过clock去快进时间,得到的只是ajax还没成功时拿到的数据,比如上面提到的那个反例。

2017-02-27 |