2017 年的 JavaScript Testing 盘点

2017 年的 JavaScript Testing 盘点

kmokiddkmokidd
译者注:去年底看到了stateofjs 中关于测试的盘点,发现不少陌生的名字,在 medium 上找到这篇文章,虽然文章发布日期是4月,但是盘点报告里的测试框架也没有巨变。时效性还是有的 =)


原文链接:An Overview of JavaScript Testing in 2017 本译文已获原作者授权翻译

原作者:vzaidman⎝(•ω•)⎠!!JS


这篇指南意在和开发者分享这一年 JavaScript 测试领域最值得关注的框架、工具和方法论等。综合了最近多篇讨论了类似话题的优秀文章的结论,融合了我们自己的经验和想法,最终有了这篇文章。

看,下图是 Facebook 出品的测试框架 Jest 的 logo:

他们的 slogan 标榜了 Jest 是“painless(没有)” 测试,但有人认为 "没有痛点的测试是不存在的":

因为一般来说 JS 开发者是不太喜欢做网页测试的。JS 测试往往能力有限、搭建测试用例比较难而且效率不高,FaceBook 希望用这个 slogan 吸引到开发者用户。

但其实,只要用对方法,找到合适的测试框架或测试库的组合方式,是能构建出一套覆盖完整且效率很高的方案的。

值得一提的是,在撰写这篇文章的过程中,我发现很多相当优秀的库或者框架,在一些特定的测试场景中它们可能很有用。但可能现在由于各种原因没有再维护了,比如 DalekJS。如果有公司愿意参与维护,复兴这些库就好了。

之后我也许会找机专门开篇讨论这个话题。现在就先专注这篇的内容,讨论讨论圈内正火的测试库吧。


测试类型

想要了解更多关于测试类型的信息,可以访问这三个链接:链接1链接2链接3

总的来看,可以分为下面三种测试类型:

  • 单元测试 (Unit Test) - 通过模拟输入和预测输出的方式测试独立的函数或者类。
  • 集成测试 (Integration Test) - 测试多个模块间的联动是否和期望相同。
  • 功能测试 (Functional Test) - 关注点不在内部实现方式,而是测试产品在真实使用场景(比如在浏览器)中是否可以达到预想的结果。

测试工具的类型

根据功能,测试工具可以被分为下面几类,其中有些专注在一个测试类型上;有些则是像搭积木一样,开发者通过自由搭配不同的工具整合适合项目的测试方法。

即使用一个测试框架可能就能满足当前需求,但出于长远考虑,为了提高扩展性,多数开发者还是会选择自由组合各种工具。

  1. 提供测试环境Mocha, Jasmine, Jest, Karma
  2. 提供测试结构Mocha, Jasmine, Jest, Cucumber
  3. 断言测试:Chai, Jasmine, Jest, Unexpected
  4. 生成、展示和监控测试结果:Mocha, Jasmine, Jest, Karma
  5. 通过对比生成的组件和数据结构的快照,确保更改是来自前一次运行的:Jest, Ava
  6. 提供 Mocks、Spies 和 StubsSinon, Jasmine, enzyme, Jest, testdouble
  7. 生成代码覆盖报告:Istanbul, Jest
  8. 提供一个浏览器或类浏览器环境,并提供接口可以控制它们的执行场景:Protractor, Nightwatch, Phantom, Casper

现在来详细聊一聊上面提到的各种工具吧:

测试结构 (Testing Structure) 指的是开发者如何组织自己的测试逻辑。常见的 BDD(行为驱动开发 behavior-driven development) 测试结构的代码大概是这样的:

describe('calculator', function() {
  // 内嵌 describe 函数用于描述一个模块
  describe('add', function() {
    // 期望的表现行为
    it('should add 2 numbers', function() {
       // 用断言测试预期行为
    })
  })
})


断言函数:用于测试运行结果是否如预期的函数,使用人数最多的是下面代码中的前两个库(即 Chai 和 Jasmine):

// Chai 中设定 expect 值
expect(foo).to.be.a('string')
expect(foo).to.equal('bar')

// Jasmine 中设定 expect 值
expect(foo).toBeString()
expect(foo).toEqual('bar')

// Chai 的断言
assert.typeOf(foo, 'string')
assert.equal(foo, 'bar')

// Jasmine 中的 expect 值
expect(foo, 'to be a', 'string')
expect(foo, 'to be', 'bar')

TIP: 对 Jasmine 的断言的高级运用,可以看这篇文章


Spies 告诉开发者在应用中或者测试中的函数被调用多少次、在什么情况下被谁调用等信息。这个特性常用在集成测试中,特别是想要测试特定场景下函数的执行情况时。比如在使用应用的某个过程里一个计算逻辑被调用了多少次。

it('should call method once with the argument 3', () => {
  const spy = sinon.spy(object, 'method')
  spy.withArgs(3)
  object.method(3)
  assert(spy.withArgs(3).calledOnce)
})


Stubbing 也可以叫 dubbing(类似电影中「替身」的概念)的使用场景是开发者在确定某个模块一定能通过测试时,假设那些函数已经被正确的执行了,然后将某些函数替换成预期值。

如果我们希望在测试时 user.isValid() 总是返回 true,那么你可以这么写:

sinon.stub(user, 'isValid').returns(true) // Sinon
spyOn(user, 'isValid').andReturns(true) // Jasmine


也支持 promise 语法:

it('resolves with the right name', done => {
  const stub = sinon.stub(User.prototype, 'fetch')
    .resolves({ name: 'David' })

  User.fetch()
    .then(user => {
      expect(user.name).toBe('David')
      done()
    })
})


Mocks 或者被称为 Fakes 假定了某些模块或者某些行为,以确保测试是在已知输入值的情况下进行的。Sinon 就有这个功能,比如模拟服务器和客户端间的交互,保证能迅速得到预期的结果:

it('returns an object containing all users', done => {
  const server = sinon.fakeServer.create()
  server.respondWith('GET', '/users', [
    200,
    { 'Content-Type': 'application/json' },
    '[{ "id": 1, "name": "Gwen" },  { "id": 2, "name": "John" }]'
  ])
  Users.all()
    .done(collection => {
      const expectedCollection = [
        { id: 1, name: 'Gwen' },
        { id: 2, name: 'John' }
      ]
      expect(collection.toJSON()).to.eql(expectedCollection)
      done()
    })

  server.respond()
  server.restore()
});


快照测试 适用需要比较预期数据结果和实际结构的场景。比如,下面这段代码模拟了链接组件被渲染后,将结果保存为 JSON 格式以做比较。

本次快照的对比对象是前一次的运行结果。开发者可以观察前后快照是否一致,如果有不一致,能进一步确认两者之间存在的差异是否合理:

it('renders correctly', () => {
  const linkInstance = (
    <Link page="http://www.facebook.com">Facebook</Link>
  )
  const tree = renderer.create(linkInstance).toJSON()
  expect(tree).toMatchSnapshot()
})


组合为整体

我们建议尽可能在所有的场景里都用同一种测试工具,包括相同测试结构和语法(2)、断言函数(3)、测试报告和监控(4)。有时候即使只用一种环境配置(1)都可以满足两种以上的测试场景。

根据测试类型按需执行。

  • 单元测试:给每个用例提供对应的模拟输入(6),确认输出是否如预期(3),还要使用覆盖率工具(7)检查用例的覆盖情况
  • 集成测试:定义重要的跨模块的内部场景。单元测试相比,集成测试要求开发者需要用到 spies 和 stubs 预估程序的行为,而不是仅仅s是使用断言判断输出 (6)。浏览器或者类浏览器环境可以模拟在多个进程之间的集成测试,以及 UI 上的展示。
  • 功能测试:通过在浏览器或类浏览器环境中,配合 API 的调用模拟用户行为进行测试。


优秀的测试工具


node-jsdomwww.npmjs.com图标

JSDom 是超文本 DOM 规范和 HTML 标准的 JS 实现。换句话说,JSDom 是用纯 JS 模拟了浏览器环境。

在这个模拟环境下,代码执行效率极高。但 JSDom 的短板也正是无法百分之百模拟浏览器行为(比如无法用它截图),所以用 JSDom 可能会限制你的测试范围。

值得一提的是,JS 社区响应迅速,它的能力将不断提升。


A Javascript code coverage tool written in JSgotwarlost.github.io

Istabul 能够将开发者所写的测试用例的覆盖率反馈出来。它分别对声明、行、函数和分支都做了覆盖检测,在生成的报告中以百分比的形式展示,开发者可以直观地看到是那部分代码还需要进一步测试。


PhantomJS | PhantomJSphantomjs.org

Phantom 实现了一个 headless Webkit 内核的浏览器(无界面可编程的浏览器),这种浏览器介于真正的浏览器和 JSDom 之间,它的稳定性和速度自然也是在两者之间。

在笔者撰写这篇文章的时候(译者注:2017年4月份)Phantom 风头正盛。但是自从 Google 把 headless 作为特性直接加入 Chrome 后。PhantomJS 之父和主要维护者 Vitaliy Slobodin 便声明他将不再维护这个工具了。


karma-runner/karmagithub.com图标

Karma 允许测试直接运行在浏览器环境下。这个环境包括了真正的浏览器、Phantom、JSDom 甚至是非常老的浏览器(译者注:比如还需要 ActiveX 的 IE 们)。

Karma 会启动一个测试服务器,服务器发送某个特定的 web 页面到客户端,作为开发者的测试环境。这个页面将会在多个浏览器上打开。

这也意味着,通过 BrowserStack 的配合,Karma 就能远程调试。


chaijs/chaigithub.com图标

Chai 是目前最受欢迎的断言测试库。


unexpectedjs/unexpectedgithub.com图标

Unexpected 也是一个断言库,它的语法和 Chai 有一点不同。Unexpected 也有良好的扩展性,通过各种插件(比如 unexpected-react)让断言能力进一步提高,想了解更多请访问这里


Standalone test spies, stubs and mocks for JavaScript. Works with any unit testing framework.sinonjs.org图标

Sinon 是一个只做 spies、stubs 和 mocks 这三件事的 JS 库,但是非常强大,可以和任何测试框架结合。


testdouble.jsgithub.com

testdouble 是一个比较新的库,功能和 Sinon 类似。但在整体设计、测试理念和特性上和 Sinon 还是有些区别的,这些区别让它在很多场景上特别适用。如果你想进一步了解这个库,可以访问三个链接


Integrated Continuous Test Runner for JavaScriptwallabyjs.com图标

Wallaby 也值得一提。虽然这是一款收费工具,但很多开发者都大力推荐使用。在 IDE (支持绝大多数 IDE)中就可以运行,测试是基于代码的更改,如果执行过程中出现测试不通过的情况,会有标注在代码边上。


选择合适的框架

开发者要做的第一件事是选择合适的框架,找到与之配合的各种库。如果框架官网上有推荐使用的库,建议开发者直接采用官方的建议,除非有特殊需求。之后要在测试框架上增减都不难。

简而言之,如果你是想踏出测试的第一步,或者想为大型项目配备足以快速上手的框架,建议使用 Jest
想要灵活性高可扩展性好,那就用 Mocha
想再简单点,就用 Ava
想做底层的测试,用 tape

下面列出了一些常见的测试工具的优缺点:


jasmine/jasminegithub.com图标

Jasmine 是一个测试框架,基本上开发者指望在测试框架里有的功能,它都能提供:一个可运行的环境、测试结构、结果报告、断言和 mocking 工具。

  • Globals - 默认创建全局测试,不需要用 require 的方式引入:
// 已经全局定义了 "describe"
// 所以不需要用下面的 require 引入 jasmine:

// const jasmine = require('jasmine')
// const describe = jasmine.describe
describe('calculator', function() {
  ...
})
  • Ready-To-Go - 有断言、spies、mocks,和 Sinon 做的事情一样。不过在 Jasmine 中引入别的库也很容易,以防开发者想要用到某些库的特性。
  • Angular - 对 Augular 支持度相当好


mochajs/mochagithub.com图标

Mocha 应该是目前使用最广泛的库。和 Jasmine 不同的是,它需要和第三方库配合(通常是 Enzyme 和 Chai)才能有断言、mocks、spies 的功能。

这也意味着,Mocha 的学习曲线相对较陡,但这也说明了它可以提供更好的灵活性和可扩展性。

如果想要特殊的断言逻辑,你可以 fork Chai,加上你想要的特性,然后整合到自己的 Mocha 环境中。当然开发者如果用的是 Jasmine,也可以在自己的环境里按需修改代码。只是在这个场景下,Mocha 会更友好。

  • 社区 - 提供了各种特殊场景可用的插件或扩展
  • 可扩展性 - 插件、扩展还有第三方库比如 Sinon,可以提供 Jasmine 没有的特性。
  • Globals - 默认创建全局的测试结构,不过和 Jasmine 一样,断言、spies 和 mocks 这些不是全局的。有人对这样的前后不一致表示惊讶。


facebook/jestgithub.com图标

Jest 是 Facebook 推荐使用的测试框架,除了拥有 Jasmine 的全部特性外,它也有一些自己的特色。

阅读了大量文章,我发现许多开发者在2016期间对 Jest 的速度和方便程度的表示赞叹
  • 性能 - 首先 Jest 基于并行测试多文件,所以在大项目中的运行速度相当快(我们在这一点上深有体会,你可以访问这里这里这里这里了解更多)。
  • UI - 清晰且操作简单
  • 快照测试 - Jest 快照功能由 FB 开发和维护,它还可以平移到别的框架上作为插件使用。
  • 更强大的模块级 mocking 功能 - Jest 允许开发者用非常简单的方法 mock 很重的库,达到提高测试效率的目的。
  • 代码覆盖检查 - 内置了一个基于 Istanbul 的代码覆盖工具,功能强大且性能高。
  • 支持性 - Jest 在2016年末2017年初发布了大版本,各方面都有了很大提升。
  • 开发 - Jest 仅仅更新被修改的文件,所以在监控模式 (watch mode) 下它的运行速度非常快。


avajs/avagithub.com图标

Ava 是一个极简的测试框架,但也能并行地运行测试。

  • Globals - 没有定义全局变量,开发者可以任意控制你的测试代码。
  • 简单 - 简单的测试结构和断言,没有复杂的 API,但也有不少高级特性。
  • 开发 - Ava 仅仅更新被修改的文件,所以在监控模式下它的运行速度非常快。
  • 快照测试 - 基于Jest-snapshot后台运行


substack/tapegithub.com图标

Tape 算是本文谈到的库中最简单的一个了。开发者只需要用 node 执行一个 JS 脚本,直截了当地调用 API 即可。

  • 简洁 - 比 Ava 更甚,没有复杂的 API,简单到极致的结构和断言。
  • Globals - 没有定义全局变量,开发者可以任意控制你的测试代码。
  • 测试用例间没有 Shared State - 为了保证模块级的测试,和最大程度上地允许开发者控制整个测试闭环,Tape 并不鼓励开发者使用类似 beforeEach 这样的函数。
  • 没有 CLI - 能运行 JS 的环境,就能运行 Tap。

单元测试

尽可能覆盖到代码。用类似 Istanbul 这样的工具测试覆盖率,确保所有模块都被测试到。

由于这些测试都是一个个模块单独测的,出于测试速度的考虑,建议直接跑在 NodeJS 环境里,而不是用浏览器执行(比如 Karma 就是在浏览器中执行的)。


集成测试

集成测试 - 列一个 to test 清单,可以先不把测试逻辑写上,仅仅是把测试过程逐步地罗列清楚。然后再逐个填充好逻辑,可以考虑加上 UI mocking 和快照测试。

快照测试可以作为是传统的 UI 集成测试的替代方案。不再是测试局部的界面表现,而是直接为整个应用截图。

如果想要在浏览器中测试,可以考虑使用 JSDom 或者 Karma


功能测试

专门做功能测试的工具数量有限,而且每个工具的实现方式差别颇大。一定要仔细斟酌慎重选择工具,推倒重来的成本有点高。

简而言之,如果你想立刻着手在多个运行环境下尝试下功能测试,可以试试 TestCafe
如果你希望测试流程完整,还有强大的社区支持。可能就不止要写 JS 了,Selenium 是个不错的选择。
如果你的应用没有复杂的界面和交互逻辑,比如一个全是表单和导航的系统。换言之,是相对较容易测试de的场景。可以使用 headless 浏览器工具,比如 Casper,高效完成测试。


SeleniumHQ/seleniumgithub.com图标

SeleniumHq,更广为人知的名字是 Selenium,可以控制浏览器模拟用户行为。虽然这个库并非专用于测试的,但它通过调用 API 暴露了一个可以模拟用户操作浏览器行为的服务器,最终实现了操作浏览器的目的。

Selenium 有很多使用方法,支持多种编程语言,甚至在某些工具中连代码都不需要编写。

根据我们的需要,Selenium 服务器由 Selenium WebDriver 控制,Selenium WebDriver 是一个介于 NodeJS 和操作浏览器的服务器之间的中间层。

Node.js <=> WebDriver <=> Selenium Server <=> FF/Chrome/IE/Safari

WebDriver 可以被引入开发者的测试框架,通过类似下面的代码中的方法调用:

describe('login form', () => {
  before(() => {
    return driver.navigate().to('http://path.to.test.app/')
  })
  it('autocompletes the name field', () => {
    driver.findElement(By.css('.autocomplete'))
      .sendKeys('John')
    driver.wait(until.elementLocated(By.css('.suggestion')))
    driver.findElement(By.css('.suggestion')).click()
    return driver.findElement(By.css('.autocomplete'))
      .getAttribute('value')
      .then(inputValue => {
        expect(inputValue).to.equal('John Doe')
      })
  })

  after(() => {
    return driver.quit()
  })
})


可能对你来说 WebDriver 已经够用了,但是依然有人建议可以配合插件、扩展去使用,甚至修改它的代码,让这个工具更加强大。

但真的把 WebDriver 和别的工具一起使用以后,可能会出现冗余代码、debug 困难等问题。fork 后自行修改又可能会渐渐偏离 主干的发展方向

即便如此,依然有开发者倾向于不直接使用 WebDriver,放我们来看看都有哪些库这么做吧!


angular/protractorgithub.com图标

Protractor 是一个对 Selenium 做了二次封装的库。优化了语法,内置了针对 Angular 的钩子。

  • Angular - 有针对 Angular 的特殊钩子,虽然其他 JS 框架可能也有类似的功能。
  • Error reporting - 良好的报错机制。
  • 移动端 - 没有支持移动端应用的自动化测试。
  • 支持 - 支持 TypeScript ,这个库由 Angular 团队开发和维护。


WebdriverIO - WebDriver bindings for Node.jswebdriver.io图标

WebdriveIO 有自己的 Selenium WebDriver 实现。

  • 语法 - 相当简单、可读性高。
  • 灵活性 - 非常简单,甚至被用作测试,很灵活、可扩展性好的库。
  • 社区 - 良好的社区氛围,积极的开发者们贡献了很多的插件和扩展。


Nightwatch.jsnightwatchjs.org图标

Nightwatch 也开发了自己的 Selenium WebDriver。并提供了测试框架,和配套的服务器、断言等等其他工具。

  • 框架 - 可以和其他框架一起使用。适用局部的功能测试场景。
  • 语法 - 可以说是几个中最简单、可读性最佳的。
  • 支持 - 不支持 TypeScript,社区文化稍弱于其他几个框架。


casperjs/casperjsgithub.com图标

Casper 基于 PhantomSlimer (和 Phantom 类似,但用了火狐的 Gecko 内核),提供了导航、脚本、测试,降低了编写脚本的难度。

Casper 和其他 headless 浏览器类似,提供给开发者一个更快但是稳定性较差的渠道,可以在无界面浏览器中测试功能。


A node.js tool to automate end-to-end web testing | TestCafedevexpress.github.io图标

TestCafe 同样基于 Selenium 。

在2016年的10月,TestCafe 的核心库作为开源的 JS 框架开放给广大开发者。同时提供非 JS 工具(比如测试记录)和有客服服务的付费版本也持续发行。

TestCafe 从前确实是闭源的,但现在它也拥抱开源了。

TestCafe 是脚本注入型工具,不像 Selenium 那样作为浏览器插件存在。这就允许 TestCafe 可以在包括移动设备在内的任意浏览器平台上执行,也不需要在各个平台上都安装一遍工具。

TestCafe 更新、更 JS 友好也更面向测试。它的特色之一是非常有用的错误报告系统,在这个系统中开发者可以追溯没通过测试的使用路径、一个非常有用的选择器系统等等很多其他有用的特性。


cucumber/cucumber-jsgithub.com图标

Cucumber 也是在功能测试方面颇有口碑的一个库,它的自动化测试支持上面提到的很多特性,但具体的支持方式可能不大一样。

Cucumber 用的是 BDD 的测试结构。 将测试分为用 Gherkin 写出期望结果的商业运营团队和根据测试结果编写测试代码的开发团队(译者注:这个模式有点像 TDD)。Cucumber 支持多种代码,包括我们熟悉的 JS:

features/like-article.feature (gherkin 语法)

Feature: A reader can share an article to social networks
  As a reader
  I want to share articles
  So that I can notify my friends about an article I liked
  Scenario: An article was opened
    Given I'm inside an article
    When I share the article
    Then the article should change to a "shared" state
特性:读者可以在社交平台上分享一篇文章
  作为一名读者
  我想要分享文章
  这样我的朋友就知道我喜欢哪篇文章
  场景:打开一篇文章的链接
    假设我正在浏览这篇文章
    我分享这篇文章后
    该文章变成"分享" 状态


features/stepdefinitions/like-article.steps.js

module.exports = function() {
  this.Given(/^I'm inside an article$/, function(callback) {
    // 功能测试代码
  })
  this.When(/^I share the article$/, function(callback) {
    // 功能测试代码
  })
    
  this.Then(/^the article should change to a "shared" state$/, function(callback) {     
    // 功能测试代码
  }) 
}

如果团队能适应这样的测试方式,你会发现它非常有用。


贡献

看到这里,如果你发现了我遗漏了什么信息,请告诉我。希望这篇文章能越来越准确、完善,尽可能帮助更多的开发者。


结论

在这篇简短的测试指南中,我们可以看到最近的测试趋势和 JS 社区对「测试」的态度,希望看到这里的你能更轻松地上手测试你的产品。

要知道还有很多有用的工具或测试方案是这篇只能没有讲到的,可能其中有些和上面说到的工具有关,也可能会是完全不同的东西。

最后,要决定使用哪一种测试方案应该从产品本身出发,另外开发者还需要从社区活跃度、和项目的适配程度以及测试框架的特性等角度仔细考虑、比较不同的方案。选择出最适合自己项目的。
接下去就是反复不断地开发、测试、开发、测试... :)


测试愉快 :)

谢谢 :)


推荐阅读

概览

testdouble, Sinon

Unexpected.js

测试框架间的比较

Jest

Ava

Tape

Selenium

TestCafe

文章被以下专栏收录
11 条评论