展望 2018 年 JavaScript Testing

展望 2018 年 JavaScript Testing

- 20180227 更新 -
原作者于2月9号更新了文章,主要是调整文章结构、新增加了新的框架和更新附录中的推荐阅读,并将标题更改为 'An Overview of JavaScript Testing in 2018'。译文内容也做出更新。



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

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

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


简而言之

单元测试和集成测试用 Jest 完成,UI 测试则是用 TestCafe。


这篇指南意在和开发者分享2018年 JavaScript 测试领域最值得关注的框架、工具和方法论等。综合多篇讨论了类似话题的优秀文章(相关链接见文章结尾处)的结论,融合了我们在自己的产品 Welldone Software Solutions 上实践出的经验和想法,最终写成了这篇文章。

仔细读过这篇文章的开发者,可以大胆地说自己已经对今年 JS 测试领域略知一二了。

  • 这篇文章是经过相当严谨的调研后出炉的,如果读者发现其中出现了错误或者遗漏的地方,请在评论区告诉我,我将会第一时间更正。
  • 请注意文章底部的相关链接,把它们全部读过并理解透彻了,理论上你将会从一个初学者进阶为 JS 测试大师。
  • 这篇指南最好的使用方式是任选一个或一些你需要的测试方法,找到匹配的测试工具,多做一些尝试。我们身处于一个信息爆炸工具繁多的领域。你的目标应该是根据项目情况筛选出合适的工具。


介绍

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

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

因为一般来说 JS 开发者是不太喜欢做网页测试的。JS 测试往往能力有限、搭建测试用例比较难而且效率不高。

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


测试类型

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

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

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


测试工具的类型

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

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

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

下面的例子来自 Jest 官方文档,对 Link 这个组件做快照测试:

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


它不会真的去渲染一个 Link 组件然后截图比较,而是把内部数据放在一个独立的文件中:

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;


执行测试i时如果发现前后两份文件的内容有不同,它会和开发者确认这些不同点:

注意:虽然快照测试常见于比较组件的一致性,但其实很多需要比较数据一致性的场景也可以用这种测试,比如 redux 中的 store 对象、应用内不同组件的内部结构等等。

下面三种场景任意一种,都能被称之为“浏览器或类浏览器环境”:

  • jsdom -- 一个纯净的模拟真实浏览器的 JS 环境,没有 UI 不做任何界面渲染。但是它提供了 window、document、body、location、cookies、选择器等等所有在浏览器中运行的 JavaScript 代码可能用到的对象。
  • Headless 浏览器环境 -- 为了更快地执行代码,提供了无界面的浏览器。
  • 真实的浏览器 -- 在平时我们用的浏览器中执行你的测试代码。


组合为整体

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

我们建议开发者将测试分为两块。一个执行单元和集成测试,另一个做 UI 测试。UI 测试往往需要消耗更长时间,特别是在需要跨浏览器测试的场景中还需要用到一些(可能要求付费的)外部服务,以提供不同的设备和浏览器(这一点会在后续讨论到)。所以可能开发者不会频繁进行 UI 测试,可能只需要在一些关键节点上(比如合并某个特性的分支时)再进行。


单元测试

需要覆盖到应用所有的单元。为这些单元撰写简单的边缘测试,用断言(3)检测输出是否如预期。还要生成测试覆盖报告(6),确认代码的覆盖率。

单元测试在函数式编程中相对容易测试得多。 代码中的纯函数 (pure function) 越多,越容易进行单元测试。


集成测试

在以前,常常只做单元测试。这样即使每个单元测试都通过了,但在应用整体执行的时候常常出现 bug。 集成测试(包括快照测试)可以防止开发者为了修复一个问题,又引入了新的问题而不自知的情况。 另外,要知道用户会用什么样的姿势使用你的产品是未知的,几乎无法测试到所有单元的所有应用场景。有些用例必须从更高维度的测试场景下才能覆盖到。

集成测试需要覆盖到关键的跨模块进程。和单元测试相比,你可能需要用到 Spies (5) 确保副作用在可控范围内;需要用 Stubs(5) 模拟不在本次测试中模块,以确保此次测试能顺利进行。

和单元测试不同的是,有时候我们的代码需要渲染特定组件还要与之发生交互,浏览器或类浏览器环境(7)能提供 window 对象保证这些场景能被测试到。

组件快照测试(4)也属于集成测试。它们为我们提供了当组件未被渲染的时候,代码对它们会产生的影响是什么样的。也可以在浏览器或类浏览器环境中进行这方面测试。


UI 测试

虽然单元测试和集成测试性价比颇高,但也有它们无能为力的时候。前文也提到过,UI 测试通常需要在浏览器或类浏览器环境(7)中执行。

UI 测试可以在特定环境中模拟用户行为(比如点击、打字、滚动...),而且这些模拟都是从终端用户的视角出发的。

UI 测试是所有测试中最难部署的。想象一下,你需要搭建一个环境让一个测试可以在不同的机器、不同设备和不同版本不同厂商的浏览器中运行。开发这么一个测试环境的成本极大,这也是为什么会有专门提供 UI 测试的产品


通用的测试工具


node-jsdomwww.npmjs.com图标

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

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

值得一提的是,JS 社区响应迅速,它的能力不断提升,最新版本已经很接近真正的浏览器了。


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

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


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)中就可以运行,测试是基于代码的更改,如果执行过程中出现测试不通过的情况,会有标注在代码边上。


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) {     
    // 功能测试代码
  }) 
}

很多团队认为比起 TDD,它更有用。


选择合适单元测试和集成测试框架

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

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

下面列出了一些常见的测试工具以及它们的特性:


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。Facebook 重写了大部分的功能,还加上了很多新特性。

许多开发者对 Jest 的速度和方便程度的表示赞叹。
  • 性能 - 首先 Jest 基于并行测试多文件,所以在大项目中的运行速度相当快(我们在这一点上深有体会,你可以访问这里这里这里这里了解更多)。
  • UI - 清晰且操作简单
  • Ready-To-Go - 有断言、spies、mocks,和 Sinon 能做的事差不多。和其他库的结合使用也很方便。
  • Globals - 和 Jasmine 一样,默认创建全局环境。但这一个特性确实会降低代码灵活性和健壮性,不过大部分情况下你都会庆幸 Jest 有这么一个功能:
// "describe" 已经在全局作用域中
// 不需要再这样 "require" 模块了
// import { describe } from 'jest'
// import { describe } from 'jasmine'

describe('calculator', function() {
  ...
})
  • 快照测试 - Jest 快照功能由 FB 开发和维护,它还可以平移到别的框架上作为插件使用。
  • 更强大的模块级 mocking 功能 - Jest 允许开发者用非常简单的方法 mock 很重的库,达到提高测试效率的目的。比如可以模拟一个 promise 的 resolve,而不是真的进行网络请求。
  • 代码覆盖检查 - 内置了一个基于 Istanbul 的代码覆盖工具,功能强大且性能高。
  • 支持性 - Jest 在2016年末2017年初发布了大版本,各方面都有了很大提升。大部分主流 IDE 和工具都已支持
  • 开发 - Jest 仅仅更新被修改的文件,所以在监控模式 (watch mode) 下它的运行速度非常快。


jasmine/jasminegithub.com图标

Jest 基于 Jasmine,那为什么不直接用 Jest 呢?

首先 Jasmine 历史悠久,背后有一个很成熟的社区支持。另外 Angular 更推荐 Jasmine,虽然 Jest 也完美支持 Angular,而且很多开发者确实是用 Jest 来测试他们的 Angular 代码的。

  • Ready-To-Go - 所有的预备工作都已做好,开发者可以直接动手开始写测试了。
  • Globals - 核心的测试模块都已经在全局作用域下,直接调用即可。
  • 社区 - 2009年诞生,至今社区已相当成熟,累积了大量文章、工具和前人的经验。
  • Angular -Angular 官方推荐的测试框架。


avajs/avagithub.com图标

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

  • Ready-To-Go - 除了 spies 和 dubbing 需要手动添加外(相当容易操作),其他功能都是现成的。要在 Ava 中使用断言可以这样写:
import test from 'ava'

test('arrays are equal', t => {
  t.deepEqual([1, 2], [1, 2])
})
  • Globals - 如上面代码所示,Ava 没有定义全局变量,开发者可以更灵活地编写代码。
  • 简洁 - 简单的测试结构和断言,没有复杂的 API,但也有不少高级特性。
  • 开发 - Ava 仅仅会更新被修改的文件,所以在监控模式下它的运行速度非常快。
  • 速度 - 创建一个新的 Node.js 进程,并行地运行测试。
  • 快照测试 - 基于Jest-snapshot后台运行


substack/tapegithub.com图标

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

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


UI 测试

在上文也提到了,在这里你都能找到提供 UI 测试的产品。专门做功能测试的工具数量有限,而且每个工具的实现方式差别颇大。一定要仔细斟酌慎重选择工具。

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


SeleniumHQ/seleniumgithub.com图标

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,放我们来看看都有哪些库这么做吧!


appium/appiumgithub.com图标

Apium 提供了一个和 Selenium 相似的 API,用于测试站点在不同移动设备上的支持性,用到了以下工具:

如果工具是支持 Selenium 或者是基于 Selenium,那它也是支持 Apium 的。


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,社区文化稍弱于其他几个框架。


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

TestCafe 同样基于 Selenium 。在2016年底,TestCafe 团队重构了代码,并开源项目

同时提供付费版本的工具,付费版本提供了测试记录和客服服务。

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

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

import { Selector } from 'testcafe';

fixture `Getting Started`
    .page `https://devexpress.github.io/testcafe/example`

// Own testing structure
test('My first test', async t => {
    await t
        .typeText('#developer-name', 'John Smith')
        .click('#submit-button')
        .expect(Selector('#article-header').innerText)
        .eql('Thank you, John Smith!')
})


Cypresswww.cypress.io

Cypress 算是 TestCafe 的第一竞品。它们的功能都是将测试代码注入站点,方向也都是在寻找更智能、更便捷的测试方法。

Cypress 相当年轻,2017年10月它推出 public beta 版本,但已经有了不少拥趸

  • 不支持跨浏览器 - 暂时只支持有界面版本的 Chrome,不过开发团队正在扩展这个能力
  • 特性缺乏 - 和 TestCafe 相比,目前还不支持并行测试等能力。但开发团队对此已经有了规划。
  • 文档 - 清晰明了。
  • debug 工具 - 上手容易使用方便日志友好。
  • 测试结构 - 使用 Mocha 的测试结构,使得开发者的 UI 测试代码和其他测试代码看起来结构相同。
describe('My First Cypress Test', function() {
  it("Gets, types and asserts", function() {
    cy.visit('https://example.cypress.io')

    cy.contains('type').click()

    // 跳转的 URL 中需要包含 '/commands/actions'
    cy.url().should('include', '/commands/actions')

    // 获取输入框,并输入相应内容,再验证输入合法性
    cy.get('.action-email')
      .type('fake@email.com')
      .should('have.value', 'fake@email.com')
  })
})


GoogleChrome/puppeteergithub.com图标

Puppeteer 由 Google 开发维护,是一个 Node 库,提供 API 调用 Headless Chrome

Chrome 59 以上支持 --headless 参数启动 Headless Chrome。Headless Chrome 提供了一系列接口允许开发者通过代码控制浏览器,Puppeteer 就是这么一个工具。

值得一提的是,2017年底 Firefox 也发布了自己的 Headless 版本

除了 Puppeteer 外还有很多工具支持 Headless Chrome 和 Headless Firefox,比如 TestCafe 和 Karma。

  • 相比之下 Puppeteer 尚且年轻,但有强大的社区和开发团队在支持着。
  • 使用原生的最新版 Chrome 内核,比起使用旧版 Webkit 内核的 Phantom 更快更可靠。
  • Headless Chrome 最大的问题是它不支持安装扩展(比如 flash),而且目前也没有这方面规划


PhantomJS | PhantomJSphantomjs.org

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

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

使用 Phantom 而不用 Puppeteer 的理由可能是什么呢:

  • Phantom 相当完备,有足够多的资料可供参考。
  • 不少测试工具比如 CasperJS 用到了 Phantom。
  • 旧的 Webkit 内核可以模拟早期 Chrome
  • Headless Chrome 不同的是,Phantom 支持扩展


segmentio/nightmaregithub.com图标

作为一个 UI 测试工具,Nightmare 的语法可以说是相当简单了。

基于和 Phantom 类似的 Electron,但它用的是新版的 Chromium 内核,持续开发和维护。Electron 主要功能是允许开发者使用 JavaScript、HTML 和 CSS 开发出强大的跨平台应用。

Nightmare 团队也在讨论验在 Headless Chrome 上的效果。它的代码看起来和 Phantom 很像:

nightmare-example.js:

yield Nightmare()
  .goto('http://yahoo.com')
  .type('input[title="Search"]', 'github nightmare')
  .click('.searchsubmit')

raw-phantom.js:

phantom.create(function (ph) {
  ph.createPage(function (page) {
    page.open('http://yahoo.com', function (status) {
      page.evaluate(
        function () {
          var el = document.querySelector('input[title="Search"]')
          el.value = 'github nightmare'
        },
        function (result) {
          page.evaluate(
            function () {
              var el = document.querySelector('.searchsubmit')
              var event = document.createEvent('MouseEvent')
              event.initEvent('click', true, false)
              el.dispatchEvent(event)
            },
            function (result) {
              ph.exit()
            }
          ) // page.evaluate
        }
      ) // page.evaluate
    }) // page.open
  }) // ph.createPage
}) // phantom.create


casperjs/casperjsgithub.com图标

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

开发者已经使用了未正式发布的 Slimer 一段时间,它在 2017 年底发布了 beta 版,基于 Headless Firefox。开发团队正致力于开发第一个正式稳定版。

Casper 有可能在 2.0 版本中从 Phantom 移植到 Puppeteer,届时将同时支持 Headless Chrome 和 Headless Firefox。值得大家期待。


Codeception/CodeceptJSgithub.com图标

和上文中提到的 CucumberJS 类似,Codecept 提供了另一套语法用于描述用户和产品的交互,基于这套语法编写出的代码如下:

Scenario('login with generated password', async (I) => {
  I.fillField('email', 'miles@davis.com');
  I.click('Generate Password');
  const password = await I.grabTextFrom('#password');
  I.click('Login');
  I.fillField('email', 'miles@davis.com');
  I.fillField('password', password);
  I.click('Log in!');
  I.see('Hello, Miles');
});

除了 Codecept 外,刚刚我们说到的 WebDriverIO, Protractor, Nightmare, Appium, Puppeteer 也都支持这种语法。


结论

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

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

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


测试愉快 :)

谢谢 :)


推荐阅读

首推

概览

spies 和 stubs

测试框架间的比较

Jest

Ava

Tape

UI Test

TestCafe

Cypress

编辑于 2018-02-27

文章被以下专栏收录

    关注前端前沿技术,探寻业界深邃思想。https://qianduan.group 欢迎微信/微博搜索『前端外刊评论』,关注我们。欢迎给本专栏投稿,原作译作不限,要求:质量高!如果愿意尝试从事前端技术相关的书籍的编写或翻译工作,请私信外刊君。