原文: Unit Testing Beginners Guide - Part 2 - Spying and fake timers
作者:jstweetster
在我们开始之前,如果你对Jest测试的基础不是很熟悉的话,请先确保你已经学习了这个系列文章的第一部分,关于介绍《使用Jest测试函数》。
这一部分是前一篇文章的扩展,请确保你已经创建了 unit-testing-functions
目录,而且你已经安装了所有依赖(NodeJS & Jest)。
到目前为止我们看到的例子都非常基础:一个函数接收一些参数,计算结果并且返回结果。这使得单元测试变得轻而易举,因为您只需要调用它并返回它。
不幸的是,在现实生活中,事情没有那么的简单。有很多函数可以执行所谓的副作用(side-effects),这使得单元测试过程变得更加复杂:函数设置定时器,调用HTTP,DOM访问,写入磁盘等。
幸运的是,几乎所有这些案例都有适用的技术。
我们将会创建一个以秒为单位开始倒计时的定时函数。每一步将会调用一个 progressCallback
回调函数。当倒计时结束时,doneCallback
回调函数将会在最后被调用。
让我们创建 time.js
文件,添加如下代码:
function countdown(time, progressCallback, doneCallback) {
progressCallback(time);
setTimeout(function() {
if( time > 1) {
countdown(time-1, progressCallback, doneCallback);
} else {
doneCallback();
}
}, 1000);
}
module.exports = countdown;
怎样测试这段代码呢?让我们来尝试一下吧。
首先创建 timer.spec.js
文件,内容如下:
const countdown = require('../src/timer.js');
describe('timer suite', function() {
test('Should call the done callback when the timer has finished counting', function() {
countdown(1, function(currentTime) {
console.log('Progress callback invoked with time '+currentTime);
}, function() {
console.log('Done callback invoked');
});
});
});
我们仅仅只是调用 countdown
函数,而且在调用 progressCallback
和调用 doneCallback
的时候在控制台打印日志。
运行测试 npm run test
,你将会看到如下输出:
PASS __tests__/timer.spec.js
● Console
console.log __tests__/timer.spec.js:6
Progress callback invoked with time 1
测试显然是通过了,有什么需要大惊小怪呢?
好吧,如果你仔细看看,请注意下面两件事:
doneCallback
函数没有被调用(没有日志输出)它实际发生的情况是,因为测试中没有断言,所以没有可以验证函数行为并在错误的情况下抛出错误的情况出现。由于没有抛出任何错误,Jest认为测试成功。
不一定是这种情况,更改代码如下:
function countdown(time, progressCallback, doneCallback) {
progressCallback(time);
setTimeout(function() {
if( time > 1) {
// countdown(time-1, progressCallback, doneCallback);
} else {
// doneCallback();
}
}, 1000);
}
module.exports = countdown;
重新运行测试...测试依然通过。
所以看来我们的测试,就目前的形式而言,完全缺乏测试并不是更有用。并且它也不是测试运行器故障:即使您使用Mocha,Jasmine,Ava或其他任何测试运行器,它也不可能在没有断言的情况下验证行为。
在我的开发生涯中,我发现很多次我们是开发人员,包括我,被这些行为所欺骗:他们认为他们对某个区域进行了大量的测试,事实上,他们中的许多人都没有进行任何测试。
小建议
每当编写测试时,通过更改测试中的代码,验证它确实实际上做了应该做的事情。
稍微修改它以便测试失败并将其更改回来并确保它通过。
现在,恢复注释代码,让我们从一个基本问题开始:
我们应该验证(断言)有关此代码的什么内容?
倒计时函数的描述如下:
progressCallback
回调函数每秒钟会被调用一次doneCallback
回调函数在最后会被调用有了这个了解,我们该如果断言呢?
我们使用spies来“spy”(窥探)一个函数的行为。
Jest文档对于spies的解释:
Mock函数也称为“spies”,因为它们让你窥探一些由其他代码间接调用的函数的行为,而不仅仅是测试输出。你可以通过使用
jest.fn()
创建一个mock函数。
简单来说,一个spy是另一个内置的能够记录对其调用细节的函数:调用它的次数,使用什么参数。
这对我们来说非常方便,因为我们需要做出的两个断言都必须验证是否调用了2个回调函数。
让我们使用“spy”来更改 timer.spec.js
中的测试方法:
const countdown = require('../src/timer.js');
describe('timer suite', function() {
test('Should call the done callback when the timer has finished counting', function() {
const progressCallbackSpy = jest.fn();
const doneCallbackSpy = jest.fn();
countdown(1, progressCallbackSpy, doneCallbackSpy);
});
});
我们刚刚做的就是我们创建两个“自我纪录”(self-recording)的spy函数。它们是不做任何事情的函数,但知道如何记录对自己的调用(如果有的话)。
重新运行测试,测试结果依旧是通过的...
这是因为Jest不知道我们正在处理异步测试,而且 countdown
函数执行一个随时间异步跨越调用的函数。
在这些情况下,我们可以暗示Jest我们正在处理异步行为,让它知道它必须等待一段时间才能完成测试,然后再继续并执行 下一个测试。
更改 timer.spec.js
代码:
const countdown = require('../src/timer.js');
describe('timer suite', function() {
test('Should call the done callback when the timer has finished counting', function(done) {
const progressCallbackSpy = jest.fn();
const doneCallbackSpy = jest.fn();
countdown(1, progressCallbackSpy, doneCallbackSpy);
});
});
注意 function(done)
这部分,这里是我们告诉Jest它正在处理一个异步的测试。此时运行测试,等待几秒,我将会看到:
FAIL __tests__/timer.spec.js (6.844s)
● timer suite › Should call the done callback when the timer has finished counting
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.
2 |
3 | describe('timer suite', function() {
> 4 | test('Should call the done callback when the timer has finished counting', function(done) {
| ^
5 | const progressCallbackSpy = jest.fn();
6 | const doneCallbackSpy = jest.fn();
7 |
at new Spec (node_modules/jest-jasmine2/build/jasmine/Spec.js:85:20)
at Suite.test (__tests__/timer.spec.js:4:3)
at Object.describe (__tests__/timer.spec.js:3:1)
最后,得到了一个测试失败。但是这并不是我们期望的失败。这个测试错误说:
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
超时 - 在jasmine.DEFAULT_TIMEOUT_INTERVAL指定的超时内未调用异步回调函数。
实际上意味着Jest希望我们调用异步回调来表示异步测试的结束,但我们却没有这样做。而且通过异步回调函数,它意味着 done
回调函数被声明为 function(donw)
回调的一部分。
那么这个 done
参数是做什么的?
done
是一个函数那怎么样才能让我们的测试通过呢?
仅仅只需要添加一个 done
函数的调用。
const countdown = require('../src/timer.js');
describe('timer suite', function() {
test('Should call the done callback when the timer has finished counting', function(done) {
const progressCallbackSpy = jest.fn();
const doneCallbackSpy = jest.fn();
countdown(1, progressCallbackSpy, doneCallbackSpy);
done() // <- When this is called, we tell Jest the test is over!
});
});
再次运行测试,我们观察到测试通过了。
但是如果我们仔细观察,我们就会最开始的情况。即使在所有的 “spy” 和异步的回调函数的情况下,单元测试依然会通过,即使它们不应该通过。
问题是我们不应该在目前正在进行的地方调用 done
函数。测试仅在经过1秒后完成,而不是在调用 countdown
函数的时候立即执行。
那么,我们如何等待1秒钟通过后然后在调用 done
函数呢?
一种方法是在 doneCallbackSpy
函数被调用的时候执行 done
函数。如果不是的话,由于错误或者其他原因,那么测试将超时并最终失败,这正是我们所期望的。
更改 timer.spec.js
代码如下:
const countdown = require('../src/timer.js');
describe('timer suite', function() {
test('Should call the done callback when the timer has finished counting', function(done) {
const progressCallbackSpy = jest.fn();
const doneCallbackSpy = jest.fn(function () {
console.log('done spy invoked');
done();
});
countdown(1, progressCallbackSpy, doneCallbackSpy);
});
});
让我们注意:
const doneCallbackSpy = jest.fn(function () {
console.log('done spy invoked');
done();
});
我已经告诉你 jest.fn()
会创建一个函数,当调用它时,它不会做任何事情。
但是当它使用像 jest.fn(replacementFunction)
这种方式时,它创建了一个函数,当调用它时,它会调用 replacementFunction
。
当然,它依然保留了 spy 的基本特征,即纪录返回函数的用法。
jest.fn(replacementFunction)
允许我们为 spy 提供一个函数,并在调用时调用 done
回调函数。
再次运行测试,测试通过。
通过修改 timer.js
中的代码,注释掉调用回调的部分来检查我们是不是在欺骗自己:
if( time > 1) {
countdown(time-1, progressCallback, doneCallback);
} else {
// doneCallback();
}
运行测试,等待几秒后,观察到测试现在失败了。这是因为 done
回调函数永远没有被调用。
因此我们正在测试一些情况的(doneCallback
回调函数)。还原注释掉的代码。
还有一件事需要测试 - progressCallback
回调函数。
因此,我们可以在 countdown
的 doneCallback
回调函数中放置另一个断言,并验证是否已调用 progressCallback
,并断言它应该被调用了多少次。
const countdown = require('../src/timer.js');
describe('timer suite', function() {
test('Should call the done callback when the timer has finished counting', function(done) {
const progressCallbackSpy = jest.fn();
const doneCallbackSpy = jest.fn(function () {
expect(progressCallbackSpy.mock.calls.length).toBe(1); // <= How many times it was called
const firstCall = progressCallbackSpy.mock.calls[0];
const firstCallArg = firstCall[0];
expect(firstCallArg).toBe(1); // <= first param, of the first call, is number 1
done();
});
countdown(1, progressCallbackSpy, doneCallbackSpy);
});
});
这里关键的部分是 mockFn.mock.calls
部分 (https://facebook.github.io/jest/docs/en/mock-function-api.html#mockfnmockcalls)。
我们断言了两件事情:
expect(progressCallbackSpy.mock.calls.length).toBe(1);
- progressCallback
回调函数只被调用一次。expect(firstCallArg).toBe(1);
- progressCallback
回调函数的参数是剩余的时间一切似乎都很好,让我们添加第二个单元测试。
在 timer.spec.js
文件中添加如下代码:
test('Should call the done callback when the timer has finished counting and the countdown is 4 secs', function(done) {
const progressCallbackSpy = jest.fn();
const doneCallbackSpy = jest.fn(function() {
expect(progressCallbackSpy.mock.calls.length).toBe(4);
done();
});
countdown(4, progressCallbackSpy, doneCallbackSpy);
});
运行测试。观察单元测试完成所花费的时间如何增加到大约4秒。
这不好......如果不是4秒,倒计时将是1000秒?
我们真正需要的是把时间放在“快进”(fast-forward)上。
因此,我们可以在Jest中使用强大的计时器模拟(timer mocks):
jest.useFakeTimers()
这将实际的 setTimeout
,setInterval
等函数替换成其他允许我们快进时间的函数。
让我们在 timer.spec.js
中首先启用伪装定时器(fake timers):
const countdown = require('../src/timer.js');
jest.useFakeTimers(); // <= This mocks out any call to setTimeout, setInterval with dummy functions
接下来,让我们使用 jest.runTimersToTime(msToRun)
更改测试和快进时间。
test('Should call the done callback when the timer has finished counting', function() {
const progressCallbackSpy = jest.fn();
const doneCallbackSpy = jest.fn();
countdown(1, progressCallbackSpy, doneCallbackSpy);
jest.runTimersToTime(1000); // <= Move the time ahead with 1 second
expect(progressCallbackSpy.mock.calls.length).toBe(1);
const firstCall = progressCallbackSpy.mock.calls[0];
const firstCallArg = firstCall[0];
expect(firstCallArg).toBe(1);
});
test('Should call the done callback when the timer has finished counting and the countdown is 4 secs', function() {
const progressCallbackSpy = jest.fn();
const doneCallbackSpy = jest.fn();
countdown(4, progressCallbackSpy, doneCallbackSpy);
jest.runTimersToTime(4000); // <= Move the time ahead with 4 seconds
expect(progressCallbackSpy.mock.calls.length).toBe(4);
});
一些说明:
done
回调函数,因为测试不在时异步的(我们通过调用 jest.useFakeTimers()
函数模拟 setTimeout
)done
spy ,一个不做任何事情的函数 const doneCallbackSpy = jest.fn();
countdown
函数,并且以1秒/4秒快进时间: jest.runTimersToTime(1000);
现在测试运行的更快并且更加可靠了!
这就结束了 "spying" 和测试时间相关的功能教程了。
请继续关注本系列的下一部分,介绍更多高级技术,以模拟和测试XHR请求和DOM访问。
我很乐意听取您关于测试此类代码的经验的评论!
扫码关注w3ctech微信公众号
共收到0条回复