2017 年的 JavaScript Testing 盘点

3 个月前

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

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

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

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

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

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

因为一般来说 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-jsdom

node-jsdom.c6df6e611df7.jpg

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

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

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

istanbul

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

PhantomJS

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

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

karma-runner/karma

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

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

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

chaijs/chai

chaijs.c6aad8c11b0c.jpg Chai 是目前最受欢迎的断言测试库。

unexpected/unexpected

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

Sinon.JS

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

testdouble.js

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

wallaby

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

选择合适的框架

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

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

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

jasmine/jasmine

jasmine.96bd0813e17a.jpg

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 支持度相当好

mocha

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

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

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

  • 社区 - 提供了各种特殊场景可用的插件或扩展

  • 可扩展性 - 插件、扩展还有第三方库比如 Sinon,可以提供 Jasmine 没有的特性。

  • Globals - 默认创建全局的测试结构,不过和 Jasmine 一样,断言、spies 和 mocks 这些不是全局的。有人对这样的前后不一致表示惊讶。

Jest

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

阅读了大量文章,我发现在 2016年有许多开发者对 Jest 的速度和方便程度的表示赞叹。

  • 性能 - 首先 Jest 基于并行测试多文件,所以在大项目中的运行速度相当快(我们在这一点上深有体会,你可以访问这里这里这里这里了解更多)。

  • UI - 清晰且操作简单

  • 快照测试 - Jest 快照功能由 FB 开发和维护,它还可以平移到别的框架上作为插件使用。

  • 更强大的模块级 mocking 功能 - Jest 允许开发者用非常简单的方法 mock 很重的库,达到提高测试效率的目的。

  • 代码覆盖检查 - 内置了一个基于 Istanbul 的代码覆盖工具,功能强大且性能高。

  • 支持性 - Jest 在2016年末2017年初发布了大版本,各方面都有了很大提升。

  • 开发 - Jest 仅仅更新被修改的文件,所以在监控模式 (watch mode) 下它的运行速度非常快。

Ava

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

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

Tape

substack.31af2c8240ed.jpg 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/selenium

seleniumhq.581db02c7855.jpg 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/protractor

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

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

WebdriverIO

webdriverio.4a076d837f33.jpg WebdriveIO 有自己的 Selenium WebDriver 实现。

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

Nightwatch.js

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

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

casperjs/casperjs

casperjs.baccdc39675f.jpg

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

1
推荐阅读