2018年JavaScript测试方案一览
2018年JavaScript测试方案一览
引言
本文旨在帮助读者紧跟2018年最重要的JavaScript测试名词、测试工具和方法。
本文结合众多文末链接的文章精粹,也融入了我们Welldone Software公司的多年来多种产品测试实践经验。
任何阅读和理解本文的读者保证可以了解web开发社区2018年JavaScript测试一览,这也是向同事、朋友分享此文的重要原因:)。
-
编写此文之前,我做了严格的调研。如果读者发现任何遗漏或错误,请及时评论,我会第一时间修复。
-
注意文章底部链接,阅读这些文章让你掌握整个蓝图,甚至理论上你能成为专家
-
实践本文的最好方式是选择需要的测试类型和合适的测试工具开始测试。现在工具和实践已经满天飞。
你的任务是从中筛选,找到最合适自己场景的方案。
简介
Facebook出品的Jest测试框架,其口号是提供无痛的JavaScript测试体验,但项目评论区下有人留言世上根本没有无痛测试。
的确,Facebook有充分理由实践这句话。但总体而言,网站测试并不好做。
JS测试受很多因素限制,很难实现,很慢,而且有时很耗时。
然而,采用合适的策略和正确工具集,接近完整覆盖率也能实现,而且测试结构组织良好,会容易操作,速度相对而言也很快。
测试类型
你可以在下面的链接中深入阅读不同的测试类型。
总体上,网站最重要的测试类型有
- 单元测试
测试输入独立函数或类,确保输出达到预期
- 集成测试
测试组件或流程运行是否达到预期,包括产生的副作用
- UI测试,也称作功能测试
通过控制浏览器或网站,测试产品本身的多种场景,不关注内部结构是否满足预期行为
测试工具类型
测试工具可以分成下面的功能,有些提供一项功能,有些提供组合功能。
一般为了获得比较灵活的功能,即使某些工具功能类似,也会选择工具组合。
- 提供测试结构(Mocha, Jasmine, Jest, Cucumber)
- 提供断言函数(Chai, Jasmine, Jest, Unexpected)
- 生成、显示、监控测试结果(Mocha, Jasmine, Jest, Karma)
- 生成和比较组件和数据结构的快照确保上次执行变化达到目标(Jest, Ava)
- 提供模拟、监视、存储 (Sinon, Jasmine, enzyme, Jest, testdouble)
- 生成代码覆盖率报告(Istanbul, Jest, Blanket)
- 提供浏览器或类浏览器环境并控制不同场景执行(Protractor, Nightwatch, Phantom, Casper)
这里解释下上面提到的术语:
- 测试结构指的是如何组织测试。一般采用行为驱动开发的BDD结构测试,类似这样:
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和Jasmine比较流行:
// 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状态。
下面的工具可以提供浏览器或类浏览器环境:
- jsdom,一个纯JavaScript运行环境,可以模拟真实浏览器,没有UI,也不会渲染任何界面。
提供window, document, body, location, cookie, 选择器和其他浏览器运行JS所需环境
- 无界面浏览器环境
一个响应速度显著提升的无UI浏览器
- 真实浏览器环境
用于测试的真实浏览器
测试工具整合
我们建议所有测试尽可能选择相同工具。
相同的测试结构和语法,断言函数,结果报告和监听机制。
我们也同样建议创建两个不同流程。
一个用于执行单元和集成测试,另一个做UI功能测试。
因为UI功能测试需要花很长时间,特别是在不同的浏览器测试。通常会选择外部付费服务提供不同的设备和浏览器(后面会谈到),
所以UI功能测试可能做的比较少。例如, 只在特性分支合并的之前进行UI测试。
UI功能测试
应该覆盖应用所有纯功能单元,工具函数,服务和辅助。
使用断言函数测试简单和极端输入下的输出是否达到预期。
另外可以用覆盖率报告工具生成单元测试报告。
单元测试是使用函数式编程模式的原因之一,尽可能使用纯函数,应用越纯净,越容易进行测试
集成测试
以前的测试专注于测试应用中小部分功能的单元测试,整体应用流程仍然会挂。
集成测试,包括快照,从另一层面,检测许多系统内部互相影响的错误。
现实世界中需要注意一点,不完美设计和很多黑盒场景,并非所有功能都是纯净的和可测试,很多功能只能在更大的流程中测试。
集成测试应该覆盖重要的跨模块流程。
与单元测试相比,你可能会用监视来用检测副作用来替代断言结果,用打桩来模拟和修改流程中不在特殊测试中的环节。
另外,和单元测试相反,浏览器或类浏览器环境可以帮助依赖windows的场景和部分环节会渲染某些组件或是交互的场景。
组件快照测试也属于这类,在不真实渲染组件或使用浏览器或类浏览器环境的情况下,对流程如何影响选中组件进行测试。
UI功能测试
有时快速和有效的单元、集成测试还不够。UI功能测试一般在浏览器或类浏览器环境运行。
UI测试模拟用户行为,像: 点击、键盘输入、滚动等等。
确保用户视角所有场景正常。
另外这些测试环境配置起来有点难,要创建测试不同机器、设备,不同浏览器类型和版本,这也是市面上有很多第三方服务的原因。
主流测试工具列表
- jsdom
jsdom 是WHATWG机构DOM和HTML标准js实现。
换句话说,jsdom无需执行任何js文件,就能模拟浏览器环境。
通过jsdom模拟出来的浏览器测试环境,测试会变得很快。
缺点是并不能模拟所有功能,例如不能截图,所以使用jsdom测试会略受限。
值得一提的是JS社区已经迅速改进jsdom, 当前版本已经很接近真实浏览器。
- Istanbul
Istanbul 能展示代码单元测试覆盖率, 它会通过百分比形式展示声明、代码行、函数和分支覆盖,帮助你明白剩余代码覆盖。
- Karma
karma可以在真实浏览器和模拟浏览器环境,包括jsdom进行测试。
karma通过测试服务器的特定网页执行页面测试,可以跨多浏览器测试。
另外也可以通过远程服务像 browserstack运行测试。
- Chai
Chai是最流行的断言库
- Unexpected
Unexpected 语法上和chai略有不同,Unexpected结合其他库扩展后可以提供更高级玩法,如:unexpected-react。
- Sinonjs
Sinon是一个十分强大的独立测试监视、打桩和模拟js库,可以和任何单元测试框架共用。
- testdouble.js
testdouble 和sinon功能一样,知名度略低,声称比sinon做的更好。
由于在设计、原理和特性上的不同,testdouble可以用在很多场景。
- Wallaby
Wallaby是另一个值得一提的收费工具,但很多用户建议购买。
wallaby运行在IDE上,支持几乎所有开发工具,当代码变化时会执行相关测试,会在代码旁边实时展示失败结果。
- Cucumber
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
mocha是目前使用最多的库,不同于Jasmine, mocha使用第三方库进行断言和模拟,监视一般用Enzyme和Chai。
这意味着mocha分割成多个库,配置会有点复杂,但十分灵活,很容易扩展。
例如,需要执行特殊断言测试时,可以使用特殊断言库替代chai, 这也可以使用Jasmine实现,但mocha比较常用。
-
社区提供众多插件和扩展用来测试不同场景
-
扩展性,插件、扩展和库,如Sinon等jasmine没有的功能
-
全局变量,默认创建带全局变量的测试结构。
-
Jest
Jest是FB官方推荐的基于jasmine的测试框架,目前为止FB替换大部分的功能,也添加很多功能。
-
性能,由于采用智能并行测试机制,对于多文件的大型项目使用jest测试会很快。
-
界面清爽方便
-
开箱即用,自带断言、监视、模拟功能,特殊场景依然可以引入第三方库
-
全局变量,默认创建全局配置,无需单独引用,大部分场景会简化测试,有时灵活性和控制度会降低。
-
快照测试,jest-snapshot由官方开发和维护,可以通过官方集成工具或插件形式集成至几乎任何框架
-
代码覆盖,内置基于Istanbul的高效代码覆盖率工具
-
可信赖,虽然jest库相对比较晚出现,整个2017年到现在都十分稳定,几乎所有IDE和工具都支持。
-
开发,监视模式下,jest只会更新变动文件,整体测试速度会很快
-
Jasmine
Jasmine作为老牌测试框架,已经退出很长时间,社区已经提供大量文章和工具,另外Angular仍然建议使用Jasmine。
-
开箱即用
-
全局环境,自带必要的全局测试变量
-
社区,自2009年面市,社区提供大量教程和工具
-
Angular, Angular官方文档推荐测试框架,所有版本都支持Angular
-
-
Ava
Ava很简单,可以并行执行测试。
-
开箱即用
-
全局环境
-
简单,结构和断言简单,支持很多高级功能
-
开发,监视模式更新文件
-
速度,通过多开nodejs进程并行测试
-
支持快照测试
-
tape
tape是上述框架中最简单的,只用简短的API,在node环境执行js文件。
-
简单, 比ava更简洁的断言和结构
-
全局环境
-
测试之间不共享状态,tape不建议使用beforeEach之类函数来实现测试过程的模块化和最大化用户控制
-
无需cli,可以运行在任何js环境中
UI测试工具
市面上有许多UI测试工具,实现和设计、API各不相同。
强烈建议多花时间理解不同方案,然后在你的实际项目中进行测试。
如果想快速上手,想要一套简单的跨浏览器测试工具,选择TestCafe
如果你需要遵循流程以及良好社区支持,选择WebDriverIO
如果你不关心跨浏览器,选择Puppeteer
如果你的应用没有复杂的用户交互和图形,比如系统全是表单和导航,选择跨浏览器无框工具 Casper
- Selenium
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操作的几种库:
- Appium
Apium 提供和Selenium类似API,用来在移动设备测试网站,主要使用下面的工具:
-
iOS 9.3+,使用苹果XCUITest
-
iOS 9.3以前的版本,使用苹果UIAutomation
-
Android 4.2+, 使用谷歌 UiAutomator/UiAutomator2
-
Android 2.3+, 使用谷歌 Instrumentation, Instrumentation 通过打包独立工程Selendroid提供支持
-
Windows Phone, 使用微软WinAppDriver
-
Protractor
Protractor库 基于Selenium包装,新增改进版语法,内置钩子支持Angular。
-
Angular,内置钩子支持angular,angular官方推荐使用Protractor
-
错误上报
-
支持TypeScript, 库本身由angular团队维护
- WebdriverIO
WebdriverIO 独立实现selenium WebDriver功能
-
语法简单,可读性强
-
灵活可扩展
-
社区支持,提供丰富插件和扩展
-
Nightwatch
Nightwatch 独立实现selenium WebDriver功能, 测试框架提供测试服务端,断言和工具。
-
能与其他框架集成使用,特殊场景可以单独运行功能测试
-
语法简洁,可读性极强
-
支持,不提供Typescript支持,总体较其他库支持度偏低
-
TestCafe
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
Cypress是TestCafe的直接竞争者,功能类似,都是在网站中注入测试用例,但相对更灵活、方便。
Cypress截止2017年10月,从内部测试版转成对外测试版,但已经有很多用户。
-
无跨浏览器支持,目前仅支持谷歌浏览器。
-
不支持并行测试等高级功能
-
文档清晰全面
-
易于调试和日志
-
基于Mocha构建测试结构
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
Puppeteer 是由谷歌开发的nodejs库,提供便捷API操作无框Chrome。
无框Chrome是开启--headless特性的59以上版本的普通Chrome浏览器。
无框模式下,chrome会提供接口控制浏览器,Puppeteer就是控制浏览器的js工具。
值得一提的是,2017年底火狐也发布无框支持。
-
Puppeteer相对比较新,拥有庞大使用社区和开发工具
-
由于使用原生Chrome引擎,速度很快
-
无框Chrome的缺点是不支持像Flash之类的扩展
-
PhantomJS
Phantom 采用开源chromium引擎,创建可控的无框浏览器环境。
由于谷歌发布了Puppeteer,PhantomJS的作者和维护者已经不再参与项目,所以2017年中以来项目更新缓慢,但仍然有人维护。
选择Phantom而不是Puppeteer的几个原因有:
-
Phantom更程度,提供许多教程和工具
-
被很多其他工具如CasperJS使用
-
使用旧版WebKit引擎,可以模拟旧版谷歌浏览器
-
另外Phantom支持Flash之类的扩展
-
Nightmare
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
Casper基于PhantomJS 和SlimerJS实现,和Phantom类似,但使用火狐Gecko引擎,提供导航,脚本和测试工具,并且抽象出很多复杂异步功能。
尽管处于试验阶段,Slimer很长时间都被广泛使用。
2017年底项目发布测试版本,使用新版无框火狐浏览器,目前正在稳定开发中,近期版本1.0.0,
將来,Casper2.0可能会从Phantom迁移到Puppeteer, 可以同时支持谷歌和火狐,敬请关注。
- CodeceptJS
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开发社区最流行的测试方案,期望让你的测试过程变得更简单。
最后,就应用架构而言,理解社区通用测试方案,并结合自身开发经验,考虑应用场景特点和特殊需求之后才能做出最佳决策,制定符合需求测试方案。
译者注
-
原文有删减,因译者水平有限,如有错误,欢迎留言指正交流