单元测试之基本构成

在先后端分离大趋势的今天,经过模块的方式来管理代码彷佛比之前任什么时候候都容易。组件都是由JavaScript编写,且组件自己就是一个状态机,这为咱们编写测试带来了很多便利性。前端

然而,在如此好的环境下测试彷佛依然得不到许多前端人员的重视(包括我)。提及来也是,即使组件化已经深刻人心,但实现组件化的方式却多种多样。React, Vue, Angular这些框架各有各的哲学思想,时间都花在折腾这些工具上了,好好写测试渐渐成了奢望。为此这篇文章我但愿能从琳琅满目的前端工具中脱离出来,简单地阐述一些,关于单元测试最基础,或者说稍微本质性的东西。node

1. 关于单元测试

我的觉得,不管一个单元测试有多么复杂,它本质上应该能够划分为下面这些部件webpack

  1. 测试声明
  2. 测试断言
  3. 测试运行者
  4. 仿真(可能会有)

每个部件都有表明性的程序库,不一样的社区可能有不一样的选择(不限于JS社区),但我的以为他们之间区别并非很大。咱们以为测试复杂,很大一部分缘由脚手架致使的,为了把测试集成到项目开发流程中除了须要安装相关的测试依赖库之外还须要搭配Webpack,固然可能也会包括较为流行的前端框架React, Vue等等。git

不少时候会形成一种现象就是package.json里面的依赖包,真正生产环境中会使用的只不过有1-3个,然而开发人员所要用到的单单用于测试的依赖包就有十几二十个,怎能不让人生畏?为了排除这些干扰,我只在Node平台上面来介绍这些单元测试的基本部件。github

2. 单元测试基本构成

1) 测试框架Mocha

Mocha是目前JS开源社区用得比较多的一个测试框架,在代码组织层面上它充当了我前面所说的测试声明的角色,咱们能够用它所提供的DSL组织测试代码。另外它也包含命令行工具,在Node平台上能够执行相关的命令来运行已经定义好的测试。下面我编写一个简单的函数并测试它(原则上我应该先写测试再写函数)。web

// src/handle.js
exports.handleByCallback = (string, callback) => {
  return callback(string)
}
复制代码

这是一个很简单的函数,经过传入回调函数来处理相关的字符串参数,并返回结果。Mocha如何安装我这里就很少说了,下面是我写的简单的测试文件json

// test/handle.spec.js
const assert = require('assert');
const handle = require('../src/handle.js')

describe('Test handle module', () => {
  it('handle string by callback method', () => {
    assert.equal(typeof handle.handleByCallback, 'function')

    const callback = function c(string) {
      c.called = true
      c.callCount ++
      return String.prototype.repeat.call(string, 2)
    }
    callback.called = false
    callback.callCount = 0

    assert.equal(handle.handleByCallback("hello", callback), "hellohello")
    assert.equal(callback.called, true)
    assert.equal(callback.callCount, 1)

    assert.equal(handle.handleByCallback("World", callback), "WorldWorld")
    assert.equal(callback.called, true)
    assert.equal(callback.callCount, 2)
  })
})
复制代码

这测试彷佛有点长,试着运行一下Mocha的命令行工具并指定对应的测试文件,看看这杯摩卡好很差喝。后端

One

看到绿了我就放心了。可是为了写个测试咱们还得费心去定义一个函数,这使得咱们的测试代码有点长了。耐心看下去,接下来你会知道怎么去优化它。浏览器

2) 测试辅助工具Sinon

Sinon.js是我比较喜欢的一个测试辅助工具。我能够用它来建立仿真函数,或者API的请求,加快测试编写的进程,使得测试代码更为精炼且可读性更高。接下来我就用这个函数库来优化上面所编写的测试代码。前端框架

上面的例子中,我本身建立了一个回调函数,而且为函数设定了相关属性。测试完结以后将会确认两个事情

  1. 配合回调函数所获得的结果是否符合预期。
  2. 回调函数是否被调用,以及调用了多少次。

细想一下若是每次咱们都要手动地去定义回调函数及其相关属性的话代码将会愈来愈长,测试也将愈加麻烦。这种时候咱们可能会考虑把它封装成一个工厂函数,自动帮咱们生成这类函数。毕竟比起内部逻辑咱们更关心回调函数的返回值不是吗?这其实就是一种仿真的手段,Sinon很好地协助咱们作好了这个事情,下面是我利用Sinon优化过的测试代码

...
const sinon = require('sinon');

describe('Test handle module', () => {
  .....
  it('handle string by callback method using sinon', () => {
    assert.equal(typeof handle.handleByCallback, 'function')
    const callback = sinon.fake.returns('Hello World') // 仿真一个老是返回'Hello World'的函数

    assert.equal(handle.handleByCallback("hello", callback), "Hello World")
    assert.equal(callback.called, true)
    assert.equal(callback.callCount, 1)
    assert.equal(callback.lastArg, 'hello')

    assert.equal(handle.handleByCallback("good job", callback), "Hello World")
    assert.equal(callback.called, true)
    assert.equal(callback.callCount, 2)
    assert.equal(callback.lastArg, 'good job')
  })
  ...
})
复制代码

上述代码最值得关注的地方在于,只须要

const callback = sinon.fake.returns('Hello World')
复制代码

就可以仿真出一个总会返回"Hello World"的回调函数,做为一个测试的辅助函数,足矣。

除此以外,仿真函数里面会包含许多可用的属性,具体可参考文档。我这里只列举了几个对于当前测试比较有意义的属性called-记录函数是否被调用, callCount-函数被调用的次数, lastArg-调用函数的最后一个参数,测试效果以下

Two

固然这只是比较简单的场景,Sinon的能力还远不止如此。我以为仿真是测试里面的难点,毕竟并非全部场景都如同上述例子那般简单粗暴,这方面我本身也在慢慢克服着,与君共勉。

3) 更丰富的断言Chai

Node.js自己就有断言库,就是我上文引入的assert。然而不少时候咱们的测试代码并非在Node端运行,而是要把相关的代码加载到对应的浏览器中,如Chrome,Firefox等等。这种时候就得借助第三方库了。这里我简单介绍一下Chai断言库,它的的断言语句十分丰富,下面我用简单的expect语句来重写上面的逻辑

...
const chai = require('chai')
const { expect } = chai

describe('Test handle module', () => {
  .....
  it('handle string by callback method using sinon and chai', () => {
    expect(typeof handle.handleByCallback).to.equal('function')
    const callback = sinon.fake.returns('Hello World')

    expect(handle.handleByCallback('hello', callback)).to.equal("Hello World")
    expect(callback.called).to.be.true
    expect(callback.callCount).to.equal(1)
    expect(callback.lastArg).to.equal('hello')

    expect(handle.handleByCallback('good job', callback)).to.equal("Hello World")
    expect(callback.called).to.be.true
    expect(callback.callCount).to.equal(2)
    expect(callback.lastArg).to.equal('good job')
  })
  ...
})
复制代码

再次运行node_modules/mocha/bin/mocha test/handle.spec.js命令,结果以下

Three

测试效果跟以前同样。从语法上来看使用了Chai以后断言语句彷佛有了点Ruby范儿。最后来咱们聊聊测试Runner。

4) 测试Runner-Karma

测试Runner顾名思义就是测试的运行者,上面的例子中每次咱们都是经过Mocha的命令行程序来运行相应的测试程序,其中Mocha就充当了测试Runner的角色,然而正式的业务中测试可能会分散到多个不一样的目录下,咱们可能会在测试或者开发文件中运用较新的JS语法,或者是相关的框架的DSL,为了使这堆代码可以在浏览器端运行就少不了预编译。

前面的例子都是用Node来写,相对比较简单且容易理解,可是前端测试注定要复杂的多,我所理解的前端测试对于Runner有以下要求

  1. 编译代码(包括测试代码和源文件代码)。
  2. 识别相关的测试目录。
  3. 批量运行测试。
  4. 能够根据需求安装相关的插件。

这看起来彷佛有点难,但JS社区中有一个叫Karma的框架可以大大简化上述工做。它能够简单地与Webpack结合,并利用已有的Webpack配置来编译咱们的代码,只须要简单的配置就能够识别并运行相关的测试。它还能以服务的方式运行,在开发过程当中监测文件的改动并从新运行测试。因为篇幅有限就不对它的配置进行更多说明了,有具体需求再去查看文档便可。

PS: 虽然说Angular最近彷佛不怎么受待见,但可别由于Karma是Angular团队出的就对它视而不见啊。

3. Question & Answer

Q: 为何没有Webpack?

A: 说实话确实也计划过在文章里面添加这样一个东西,后来写着写着仍是放弃了。Webpack有丰富的插件系统,确实在某种程度上给予咱们开发人员必定的便利性。可是我的以为它是使得咱们现在前端领域变得如此混乱的“罪魁祸首”。单从语法层面来讲,Webpack有点像是Lisp系语言中的宏,咱们能够定制任何语法,可是在前端领域中这种“宏”却被无节制地使用着,不一样的开发人员就能定制出不一样的类JS语法,为了排除这种干扰,我决定直接采用了Node环境下最为“原生”的JS写法。


Q: 为何没有Webpack跟Karma的集成的相关代码示例子?

A: Karma自己的配置并非很复杂,它只是一个测试的Runner,预编译功能能够依赖Webpack来完成,加入一个叫作karma-webpack做为他们之间的桥梁便可。贴相关的代码会致使篇幅过长,且本文重点并非“配置”。

4. 尾声

这篇文章主要简单介绍了一些单元测试的基本部件,每一个部件中我都列举了JS 社区中较为经常使用的对应的软件库。或许他们会是比较好的选择但却并非惟一的选择。好比测试框架咱们还能够选择Jasmine,断言库咱们能够选择expect.js。至于选择什么纯粹是我的喜爱的问题,在我看来区别并非很大。

Happy Coding and Writing!!

相关文章
相关标签/搜索