网络埋伏纪事

Node Hero - 9. Node.js 单元测试

网络埋伏纪事 · 2016-11-21翻译 · 973阅读 原文链接

本教程将会学习 Node.js 中的单元测试是什么,以及如何正确地测试你的应用程序。

测试 Node.js 应用程序

你可以把测试当作你创建的应用程序的保障措施。他们将不仅运行在你的本机上,还会在 CI 服务上,这样失败的构建就不会推送到产品系统中。

你也许会问:我的应用程序中该测试什么?我应该有多少测试?

答案因情而异,但是根据经验,你可以遵循测试金字塔制定的准则。

Test Pyramid for Node.js Unit Testing

基本上,测试金字塔描述你应该编写单元测试集成测试端到端测试。集成测试要比端到端测试多,单元测试甚至要更多一些。

下面我们来看看如何为应用程序添加单元测试!

请注意,这里我们不打算讨论集成测试和端到端测试,因为它们远远超出了本教程的范畴。


Node.js 应用程序单元测试

编写单元测试,是为了看看给定的模块(单元)是否工作。所有依赖都被剔除了,意味着我们要为模块提供伪依赖。

应该为指定模块暴露的方法,而不是内部操作提供测试。

单元测试剖析

每个单元测试有如下结构:

  1. 测试设置
  2. 调用被测试的方法
  3. 断言

每个单元测试应该只测试一个关注点(当然,这不意味着你可以只添加一个断言)

用于 Node.js 单元测试的模块

对于单元测试,我们打算用如下模块:

  • 测试运行器: mocha,或者 tape
  • 断言库: chai, 或者 assert 模块 (用于断言)
  • 测试 spy、stub 以及 mock: sinon (用于测试设置)

Spy、stub 和 mock - 用哪一个以及什么时候用?

在动手写单元测试之前,我们先看看什么是 spy、stub 和 mock!

Spy

可以使用 spy 来获取函数调用上的信息,比如函数被调用了多少次,或者传递了什么参数给它们。

it('calls subscribers on publish', function () {  
  var callback = sinon.spy()
  PubSub.subscribe('message', callback)

  PubSub.publishSync('message')

  assertTrue(callback.called)
})
// 采用的示例来自于 sinon 文档网站: http://sinonjs.org/docs/
Stub

Stub(桩)与 spy 类似,但是它是替换目标函数。可以使用 stub 来控制一个方法的行为,从而强制一个代码路径(比如抛出异常),或者阻止对外部资源的调用(比如 HTTP API)。

it('calls all subscribers, even if there are exceptions', function (){  
  var message = 'an example message'
  var error = 'an example error message'
  var stub = sinon.stub().throws()
  var spy1 = sinon.spy()
  var spy2 = sinon.spy()

  PubSub.subscribe(message, stub)
  PubSub.subscribe(message, spy1)
  PubSub.subscribe(message, spy2)

  PubSub.publishSync(message, undefined)

  assert(spy1.called)
  assert(spy2.called)
  assert(stub.calledBefore(spy1))
})
// 采用的示例来自于 sinon 文档网站: http://sinonjs.org/docs/
Mock

mock 是带有预先编好的行为和期望值的伪方法。

it('calls all subscribers when exceptions happen', function () {  
  var myAPI = { 
    method: function () {} 
  }

  var spy = sinon.spy()
  var mock = sinon.mock(myAPI)
  mock.expects("method").once().throws()

  PubSub.subscribe("message", myAPI.method)
  PubSub.subscribe("message", spy)
  PubSub.publishSync("message", undefined)

  mock.verify()
  assert(spy.calledOnce)
// 采用的示例来自于 sinon 文档网站: http://sinonjs.org/docs/
})

如你所见,对于 mock,你必须预先定义好期望的值。


假设要测试如下的模块:

const fs = require('fs')  
const request = require('request')

function saveWebpage (url, filePath) {  
  return getWebpage(url, filePath)
    .then(writeFile)
}

function getWebpage (url) {  
  return new Promise (function (resolve, reject) {
    request.get(url, function (err, response, body) {
      if (err) {
        return reject(err)
      }

      resolve(body)
    })
  })
}

function writeFile (fileContent) {  
  let filePath = 'page'
  return new Promise (function (resolve, reject) {
    fs.writeFile(filePath, fileContent, function (err) {
      if (err) {
        return reject(err)
      }

      resolve(filePath)
    })
  })
}

module.exports = {  
  saveWebpage
}

这个模块做一件事情:将网页(基于指定的 URL)保存为本机上的一个文件。要测试该模块,我们必须拔掉 fs 模块和 request 模块。

在我们 RisingStack 团队中,在真正开始为本模块编写单元测试前,我们通常添加一个 test-setup.spec.js 文件来做基础测试设置,比如创建 sinon 沙箱。这样可以省下每次测试后编写 sinon.sandbox.create()sinon.sandbox.restore()

// test-setup.spec.js
const sinon = require('sinon')  
const chai = require('chai')

beforeEach(function () {  
  this.sandbox = sinon.sandbox.create()
})

afterEach(function () {  
  this.sandbox.restore()
})

此外,请注意,我们总是将测试文件放在挨着实现文件的地方,所以就有了 .spec.js 这个名称。在我们的 package.json 文件中,可以找到这些行:

{
  "test-unit": "NODE_ENV=test mocha '/**/*.spec.js'",
}

有了这些设置后,就可以写测试本身了!

const fs = require('fs')  
const request = require('request')

const expect = require('chai').expect

const webpage = require('./webpage')

describe('The webpage module', function () {  
  it('saves the content', function * () {
    const url = 'google.com'
    const content = '<h1>title</h1>'
    const writeFileStub = this.sandbox.stub(fs, 'writeFile', function (filePath, fileContent, cb) {
      cb(null)
    })

    const requestStub = this.sandbox.stub(request, 'get', function (url, cb) {
      cb(null, null, content)
    })

    const result = yield webpage.saveWebpage(url)

    expect(writeFileStub).to.be.calledWith()
    expect(requestStub).to.be.calledWith(url)
    expect(result).to.eql('page')
  })
})

完整的代码库在这里找到:https://github.com/RisingStack/nodehero-testing

代码覆盖率

要了解你的代码库被测试覆盖的情况,你可以生成一个覆盖率报告。

这个报告将包含如下指标:

  • 覆盖率
  • 语句覆盖率
  • 分支覆盖率
  • 函数覆盖率

在 RisingStack 公司中,我们使用 istanbul 计算代码覆盖率。你应该将如下脚本添加到 package.json 文件中,来在 mocha 中使用 istanbul

istanbul cover _mocha $(find ./lib -name \"*.spec.js\" -not -path \"./node_modules/*\")

之后,你将得到像这样的代码覆盖率报告:

Node.js Unit Testing Code Coverage

你可以点击一下,看看带注解的源代码 - 哪些部分被测试,哪些部分没有。

下一步

测试可以省下很多麻烦 - 不过,依然不可避免要时常调试。下一章将学习如何调试 Node.js 应用程序

相关文章