前端译趣我爱编程

2018年JavaScript测试方案一览

2018-05-30  本文已影响163人  linc2046
2018年JavaScript测试方案一览

引言

本文旨在帮助读者紧跟2018年最重要的JavaScript测试名词、测试工具和方法。

本文结合众多文末链接的文章精粹,也融入了我们Welldone Software公司的多年来多种产品测试实践经验。

任何阅读和理解本文的读者保证可以了解web开发社区2018年JavaScript测试一览,这也是向同事、朋友分享此文的重要原因:)。

你的任务是从中筛选,找到最合适自己场景的方案。

简介

Facebook出品的Jest测试框架,其口号是提供无痛的JavaScript测试体验,但项目评论区下有人留言世上根本没有无痛测试

的确,Facebook有充分理由实践这句话。但总体而言,网站测试并不好做

JS测试受很多因素限制,很难实现,很慢,而且有时很耗时。

然而,采用合适的策略和正确工具集,接近完整覆盖率也能实现,而且测试结构组织良好,会容易操作,速度相对而言也很快。

测试类型

你可以在下面的链接中深入阅读不同的测试类型。

什么是单元测试、集成测试、冒烟测试、回归测试?

JavaScript单元测试、功能测试、集成测试

什么是单元测试、集成测试、功能测试?

总体上,网站最重要的测试类型有

测试输入独立函数或类,确保输出达到预期

测试组件或流程运行是否达到预期,包括产生的副作用

通过控制浏览器或网站,测试产品本身的多种场景,不关注内部结构是否满足预期行为

测试工具类型

测试工具可以分成下面的功能,有些提供一项功能,有些提供组合功能。

一般为了获得比较灵活的功能,即使某些工具功能类似,也会选择工具组合。

这里解释下上面提到的术语:

describe('calculator', function() {
  // describes a module with nested "describe" functions
  describe('add', function() {
    // specify the expected behavior
    it('should add 2 numbers', function() {
       //Use assertion functions to test the expected behavior
       ...  
    })
  })
})
// Chai expect (popular)
expect(foo).to.be.a('string')
expect(foo).to.equal('bar')

// Jasmine expect (popular)
expect(foo).toBeString()
expect(foo).toEqual('bar')

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

// Unexpected expect
expect(foo, 'to be a', 'string')
expect(foo, 'to be', 'bar')

一般用于集成测试中确保流程的副作用达到预期。

例如, 监视某些流程中计算函数的调用次数

it('should call method once with the argument 3', () => {
  
  // create a sinon spy to spy on object.method
  const spy = sinon.spy(object, 'method')
  
  // call the method with the argument "3"
  object.method(3)

  // make sure the object.method was called once, with the right arguments
  assert(spy.withArgs(3).calledOnce)
  
})
 

如果你在测试不同的组件时,想要确保测试过程中user.isValid总是返回真,如下例:

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

// Jasmine stubs are actually spies with stubbing functionallity
spyOn(user, 'isValid').andReturns(true)

也可以和Promise搭配使用:

it('resolves with the right name', done => {
  
  // make sure User.fetch "responds" with our own value "David"
  const stub = sinon
    .stub(User.prototype, 'fetch')
    .resolves({ name: 'David' })
  
  User.fetch()
    .then(user => {
      expect(user.name).toBe('David')
      done()
    })
})

Sinon能够模拟服务端,实现流程测试时预期响应保持离线,快速。

it('returns an object containing all users', done => {
  
  // create and configure the fake server to replace the native network call
  const server = sinon.createFakeServer()
  server.respondWith('GET', '/users', [
    200,
    { 'Content-Type': 'application/json' },
    '[{ "id": 1, "name": "Gwen" },  { "id": 2, "name": "John" }]'
  ])

  // call a process that includes the network request that we mocked
  Users.all()
    .done(collection => {
      const expectedCollection = [
        { id: 1, name: 'Gwen' },
        { id: 2, name: 'John' }
      ]
      expect(collection.toJSON()).to.eql(expectedCollection)
      done()
    })
  
  // respond to the request
  server.respond()
  
  // remove the fake server
  server.restore()
})

下面官方文档的例子展示Link组件的快照测试:

it('renders correctly', () => {
  
  // create an instance of the Link component with page and child text
  const linkInstance = (
    <Link page="http://www.facebook.com">Facebook</Link>
  )
  
  // create a data snapshot of the component
  const tree = renderer.create(linkInstance).toJSON()
  
  // compare the sata to the last snapshot
  expect(tree).toMatchSnapshot()
})

快照测试不会真正渲染组件并对组件取照,只是会在独立文件中保持组件内部数据:

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

执行快照测试会看到最新快照和上次不同,会提示开发者确认变化达到预期。

注意,快照测试一般用来比较组件显示数据,但也可以比较其他类型数据,如: 应用中不同部分内部结构和全局redux状态。

下面的工具可以提供浏览器或类浏览器环境:

提供window, document, body, location, cookie, 选择器和其他浏览器运行JS所需环境

一个响应速度显著提升的无UI浏览器

用于测试的真实浏览器

测试工具整合

我们建议所有测试尽可能选择相同工具。

相同的测试结构和语法,断言函数,结果报告和监听机制。

我们也同样建议创建两个不同流程。

一个用于执行单元和集成测试,另一个做UI功能测试。

因为UI功能测试需要花很长时间,特别是在不同的浏览器测试。通常会选择外部付费服务提供不同的设备和浏览器(后面会谈到),

所以UI功能测试可能做的比较少。例如, 只在特性分支合并的之前进行UI测试。

UI功能测试

应该覆盖应用所有纯功能单元,工具函数,服务和辅助。

使用断言函数测试简单和极端输入下的输出是否达到预期。

另外可以用覆盖率报告工具生成单元测试报告。

单元测试是使用函数式编程模式的原因之一,尽可能使用纯函数,应用越纯净,越容易进行测试

集成测试

以前的测试专注于测试应用中小部分功能的单元测试,整体应用流程仍然会挂。

集成测试,包括快照,从另一层面,检测许多系统内部互相影响的错误。

现实世界中需要注意一点,不完美设计和很多黑盒场景,并非所有功能都是纯净的和可测试,很多功能只能在更大的流程中测试。

集成测试应该覆盖重要的跨模块流程。

与单元测试相比,你可能会用监视来用检测副作用来替代断言结果,用打桩来模拟和修改流程中不在特殊测试中的环节。

另外,和单元测试相反,浏览器或类浏览器环境可以帮助依赖windows的场景和部分环节会渲染某些组件或是交互的场景。

组件快照测试也属于这类,在不真实渲染组件或使用浏览器或类浏览器环境的情况下,对流程如何影响选中组件进行测试。

UI功能测试

有时快速和有效的单元、集成测试还不够。UI功能测试一般在浏览器或类浏览器环境运行。

UI测试模拟用户行为,像: 点击、键盘输入、滚动等等。

确保用户视角所有场景正常。

另外这些测试环境配置起来有点难,要创建测试不同机器、设备,不同浏览器类型和版本,这也是市面上有很多第三方服务的原因。

主流测试工具列表

jsdom 是WHATWG机构DOM和HTML标准js实现。

换句话说,jsdom无需执行任何js文件,就能模拟浏览器环境。

通过jsdom模拟出来的浏览器测试环境,测试会变得很快。

缺点是并不能模拟所有功能,例如不能截图,所以使用jsdom测试会略受限。

值得一提的是JS社区已经迅速改进jsdom, 当前版本已经很接近真实浏览器。

Istanbul 能展示代码单元测试覆盖率, 它会通过百分比形式展示声明、代码行、函数和分支覆盖,帮助你明白剩余代码覆盖。

karma可以在真实浏览器和模拟浏览器环境,包括jsdom进行测试。

karma通过测试服务器的特定网页执行页面测试,可以跨多浏览器测试。

另外也可以通过远程服务像 browserstack运行测试。

Chai是最流行的断言库

Unexpected 语法上和chai略有不同,Unexpected结合其他库扩展后可以提供更高级玩法,如:unexpected-react

Sinon是一个十分强大的独立测试监视、打桩和模拟js库,可以和任何单元测试框架共用。

testdouble 和sinon功能一样,知名度略低,声称比sinon做的更好。

由于在设计、原理和特性上的不同,testdouble可以用在很多场景。

Wallaby是另一个值得一提的收费工具,但很多用户建议购买。

wallaby运行在IDE上,支持几乎所有开发工具,当代码变化时会执行相关测试,会在代码旁边实时展示失败结果。

Cucumber 使用Gherkin语法的BDD风格测试,可以分割测试和标准文件,然后根据对应文件进行测试。

框架支持多种语言编写测试,包括JS:

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
module.exports = function() {
  this.Given(/^I'm inside an article$/, function(callback) {
    // functional testing tool code
  })
  
  this.When(/^I share the article$/, function(callback) {
    // functional testing tool code
  })
    
  this.Then(/^the article should change to a "shared" state$/, function(callback) {     
    // functional testing tool code
  }) 
}

很多团队发现这种语法比TDD风格更方便。

选择单元测试和集成测试工具

首先要选择合适的测试框架和库,建议使用框架自带测试工具,特殊需要选择其他工具。

如果你需要快速上手大型项目测试,选择Jest

如果需要十分灵活可扩展的配置,选择mocha

如果你追求简单,选择Ava

如果想要更底层,选择tape

下面是主流测试工具主要特点介绍:

mocha是目前使用最多的库,不同于Jasmine, mocha使用第三方库进行断言和模拟,监视一般用Enzyme和Chai。

这意味着mocha分割成多个库,配置会有点复杂,但十分灵活,很容易扩展。

例如,需要执行特殊断言测试时,可以使用特殊断言库替代chai, 这也可以使用Jasmine实现,但mocha比较常用。

Jest是FB官方推荐的基于jasmine的测试框架,目前为止FB替换大部分的功能,也添加很多功能。

Ava很简单,可以并行执行测试。

tape是上述框架中最简单的,只用简短的API,在node环境执行js文件。

UI测试工具

市面上有许多UI测试工具,实现和设计、API各不相同。

强烈建议多花时间理解不同方案,然后在你的实际项目中进行测试。

如果想快速上手,想要一套简单的跨浏览器测试工具,选择TestCafe

如果你需要遵循流程以及良好社区支持,选择WebDriverIO

如果你不关心跨浏览器,选择Puppeteer

如果你的应用没有复杂的用户交互和图形,比如系统全是表单和导航,选择跨浏览器无框工具 Casper

Selenium自动驱动浏览器模拟用户行为。

它并不是专门为测试而写的工具,可以通过暴露服务器使用API来控制浏览器模拟用户行为。

Selenium 可以通过多种方式控制,支持多种编程语言,甚至支持零编程。

Selenium服务端由Selenium WebDriver控制,Driver作为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进行扩展、定制或封装。

另外基于webdriver进行封装会增加冗余代码,调试变得复杂。

然而独立定制会从其活跃开发产生差异。

很多开发者倾向于不直接使用WebDriver,下面是selenium操作的几种库:

Apium 提供和Selenium类似API,用来在移动设备测试网站,主要使用下面的工具:

Protractor库 基于Selenium包装,新增改进版语法,内置钩子支持Angular。

WebdriverIO 独立实现selenium WebDriver功能

Nightwatch 独立实现selenium WebDriver功能, 测试框架提供测试服务端,断言和工具。

TestCafe是类Selenium工具最佳替代,2016年底框架进行重写并开源。

框架会將自身脚本注入到网站,可以运行在任何浏览器,包括移动设备,并能完整控制js执行循环。

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!')
})

Cypress是TestCafe的直接竞争者,功能类似,都是在网站中注入测试用例,但相对更灵活、方便。

Cypress截止2017年10月,从内部测试版转成对外测试版,但已经有很多用户。

describe('My First Cypress Test', function() {
  it("Gets, types and asserts", function() {
    cy.visit('https://example.cypress.io')

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

    // Should be on a new URL which includes '/commands/actions'
    cy.url().should('include', '/commands/actions')

    // Get an input, type into it and verify that the value has been updated
    cy.get('.action-email')
      .type('fake@email.com')
      .should('have.value', 'fake@email.com')
  })
})

Puppeteer 是由谷歌开发的nodejs库,提供便捷API操作无框Chrome。

无框Chrome是开启--headless特性的59以上版本的普通Chrome浏览器。

无框模式下,chrome会提供接口控制浏览器,Puppeteer就是控制浏览器的js工具。

值得一提的是,2017年底火狐也发布无框支持。

Phantom 采用开源chromium引擎,创建可控的无框浏览器环境。

由于谷歌发布了Puppeteer,PhantomJS的作者和维护者已经不再参与项目,所以2017年中以来项目更新缓慢,但仍然有人维护。

选择Phantom而不是Puppeteer的几个原因有:

Nightmare是一个很棒的界面测试库,提供十分简单的语法。

和Phantom类似,库使用Electron,内置新版Chromium, 并且维护和开发十分活跃。项目也在考虑使用无框Chrome。

两者测试代码对比:

yield Nightmare()
  .goto('http://yahoo.com')
  .type('input[title="Search"]', 'github nightmare')
  .click('.searchsubmit')
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

Casper基于PhantomJS 和SlimerJS实现,和Phantom类似,但使用火狐Gecko引擎,提供导航,脚本和测试工具,并且抽象出很多复杂异步功能。

尽管处于试验阶段,Slimer很长时间都被广泛使用。

2017年底项目发布测试版本,使用新版无框火狐浏览器,目前正在稳定开发中,近期版本1.0.0,

將来,Casper2.0可能会从Phantom迁移到Puppeteer, 可以同时支持谷歌和火狐,敬请关注。

CodeceptJS采用另一种方式关注用户行为,提供不同的API抽象来测试交互。

测试代码示例:

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');
});

总结

至此,我们看到web开发社区最流行的测试方案,期望让你的测试过程变得更简单。

最后,就应用架构而言,理解社区通用测试方案,并结合自身开发经验,考虑应用场景特点和特殊需求之后才能做出最佳决策,制定符合需求测试方案。

译者注

上一篇 下一篇

猜你喜欢

热点阅读