[Jest]单元测试初学者指南 - 第二部分 - Spying and fake timers

栏目: 编程工具 · 发布时间: 7年前

内容简介:原文:作者:jstweetster

原文: 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

我们使用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 回调函数。

因此,我们可以在 countdowndoneCallback 回调函数中放置另一个断言,并验证是否已调用 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 )。

我们断言了两件事情:

  1. expect(progressCallbackSpy.mock.calls.length).toBe(1); - progressCallback 回调函数只被调用一次。
  2. 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()

这将实际的 setTimeoutsetInterval 等函数替换成其他允许我们快进时间的函数。

让我们在 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访问。

我很乐意听取您关于测试此类代码的经验的评论!

[Jest]单元测试初学者指南 - 第二部分 - Spying and fake timers

扫码关注w3ctech微信公众号


以上所述就是小编给大家介绍的《[Jest]单元测试初学者指南 - 第二部分 - Spying and fake timers》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming Collective Intelligence

Programming Collective Intelligence

Toby Segaran / O'Reilly Media / 2007-8-26 / USD 39.99

Want to tap the power behind search rankings, product recommendations, social bookmarking, and online matchmaking? This fascinating book demonstrates how you can build Web 2.0 applications to mine the......一起来看看 《Programming Collective Intelligence》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试