文章研究了四个问题:什么是自动化测试、为何要自动化测试、什么项目适合自动化测试、自动化测试具体要怎么作。在寻找这四个问题答案的过程当中,梳理了一套完整的前端自动化测试方案,包括:单元测试、接口测试、功能测试、基准测试。javascript
维基百科是这样定义的html
在软件测试中,测试自动化(英语:Test automation)是一种测试方法,使用特定的软件,去控制测试流程,并比较实际的结果与预期结果之间的差别。经过将测试自动化,可让正式的测试过程当中的必要测试,能够反复进行;经过这种方法,也能够将难以手动进行的测试,交由软件来作。前端
测试自动化的最大优点就是能够快速并且反复的进行测试。java
总结一下:自动化测试指软件测试的自动化,让软件代替人工测试,能够快速、反复进行。node
关于自动化测试有一个金字塔理论,把测试从上到下分为UI(用户界面测试)/Service(服务测试) /Unit(单元测试 )。如图所示,越往金字塔底层,测试的效率越高,测试质量保障程度越高,测试的成本越低。怎么理解这句话呢?前端项目一般UI变化频繁,一旦发生变化,UI测试用例就没法执行且难以维护,因此UI自动化测试的成本高,收益小;相比UI测试,Service测试更加简单直接且变化不会很频繁;单元测试主要对公共函数、方法进行测试,测试用例复用度高且更能保证代码质量。react
测试最重要的目的是验证代码正确性,确保项目质量。举个例子,某一天我写了一个逻辑复杂的函数,这个函数被不少地方调用,过了一个月以后,我可能忘记这里面的具体逻辑了,出于某种缘由须要为这个函数增长一些功能,修改这个函数的代码,那我要怎么作才能保证修改代码后不影响其余的调用者呢,或者说,我要怎么作,才能快速的知道哪些地方受影响,哪些地方不受影响呢?答案就是实施自动化测试,跑测试用例。git
若是不进行自动化测试,咱们会如何验证代码的正确性?一般FE使用的方法是手动测试:console、alert、打断点、点点点。但手动测试是一次性的,若是下次有人对代码功能作了修改,咱们不得再也不次重复手动测试的工做,而且很难保证测试的全覆盖。但若是编写测试用例进行自动化测试,第一次写完的测试用例是能够重复使用的,一次编写,屡次运行。若是测试用例写的完善、语义化,开发人员还能够经过看测试用例快速了解项目需求。实施自动化测试能够驱动开发人员在代码的设计中作更好的抽象,写可测试的代码,以测试公用方法为例,要确保被测试的方法无反作用,既对外部变量没有依赖,也不会改变全局本来的状态。github
总结一下,实施自动化测试有四个好处:web
能够验证代码正确性,保证项目质量正则表达式
测试用例能够复用,一次编写,屡次运行
经过看测试用例能够快速了解需求
驱动开发,指导设计,保证写的代码可测试
自动化测试如此优秀,那是否是全部项目都适合进行自动化测试?答案是否认的,由于有成本。在实施自动化测试以前须要对软件开发过程进行分析,基于投入产出来判断是否适合实施自动化测试。实施自动化测试的项目一般须要同时知足如下条件:
若是需求变更过于频繁,维护测试脚本的成本过高;若是项目周期比较短,没有足够的时间去支持自动化测试的过程;若是测试脚本重复使用率低,耗费的精力大于创造的价值,不值得;若是代码不规范,可测试性差,那自动化测试实施起来会比较困难。
5.1 原始的测试方法
举个例子,如今有一个方法sum
const sum = (a, b) => { return a + b }
复制代码
如何证实sum
方法的正确性?咱们一般会使用以下代码进行测试
// test/util.test.js
const sum = (a, b) => { return a + b }
if(sum(1,1)===2){
console.log('sum(1,1)===2,测试结果符合预期,方法正确')
}else{
console.log('sum(1,1)===2,测试结果不符合预期,方法出错')
}
复制代码
执行测试代码后控制台输出结果以下
测试结果正确。假设如今把sum
方法改成+1
const sum = (a, b) => { return a + b + 1 }
复制代码
执行测试代码后控制台输出结果以下
这个输出虽然显示了方法出错的提示,可是对结果正确与错误没有作明显的区分,测试结论不够直观,咱们把测试代码修改一下
// test/util.test.js
const sum = (a, b) => { return a + b + 1 }
if (sum(1, 1) === 2) {
console.log(' sum(1,1)===2,测试结果符合预期,方法正确')
} else {
throw new Error('sum(1,1)===2,测试结果不符合预期,方法出错')
}
复制代码
这段代码执行后,一旦方法执行的结果不符合预期就主动抛出错误
这样就能更直观的看出测试结论。咱们进一步优化,使用nodejs提供的断言模块来书写测试用例
const sum = (a, b) => { return a + b + 1 }
const assert = require('assert')
assert.equal(sum(1, 1), 2)
复制代码
执行测试代码后控制台结果以下
输出信息与刚才的效果相似:执行结果不符合预期就主动抛出错误。使用assert
达到了相同的效果,但代码量减少了,而且更加语义化。
5.2 使用测试框架
上面的方法能够帮助咱们完成代码测试,那有没有更好的方式呢?咱们开发项目时一般会选择使用框架和库,使用框架的好处是约束咱们代码的风格,保证代码的可维护性和扩展性,使用工具库能够提升开发效率。同理,在实施自动化测试时咱们也会选择使用测试框架和库。目前市面上比较流行的前端测试框架有Mocha、QUnit、Jasmine、Jest等,以下作个简单介绍
框架能够为咱们输出更加直观的测试报告,好比像下面这样,正确和错误的测试结果都给咱们展现
还能够输出文档结构的测试报告,好比下面这样
5.3 测试方案技术选型
本文讨论的自动化测试方案技术选型以下:
选择Mocha是由于它:
下面咱们经过一段测试用例来看一下Mocha
有什么能力:
能够看到Mocha最核心的四项能力
代码中describe
块称为“测试套件”,表示一组相关的测试,它是一个函数,第一个参数是测试套件的名称("测试 sum 方法"),第二个参数是实际执行的函数,分组让测试用例代码结构化,易于维护。it
块称为"测试用例",表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称("1 加 1 应该等于 2"),第二个参数是实际执行的函数。
选择chai做为断言库是由于它提供了两种风格的断言:BDD风格(行为驱动开发)和TDD风格(测试驱动开发),其中BDD风格更接近天然语言。使用它能够自由、灵活的与Mocha搭配,下图是chai官网展现的两种断言风格。
5.4 测试方案代码
下面开始梳理完整的自动化测试方案,总体目录结构以下:
5.4.1 单元测试
(1)对以下方法进行单元测试
// /src/client/common/js/testUtil.js
export const sum = (a, b) => {
return a + b
}
复制代码
编写好测试用例
import { sum } from '../../src/client/common/js/testUtil.js'
const { expect } = require('chai')
describe('单元测试: sum (a, b)', function () {
it('1+1 应该等于 2', function () {
expect(sum(1, 1)).to.be.equal(2)
})
})
// skip能够指定跳过某个分组
describe.skip('单元测试:金额按千分位逗号分隔的方法 formatMoney (s, type)', function () {...})
复制代码
而后使用mocha执行测试用例,输出结果以下
能够看到两个测试分组有一个测试经过,一个被咱们主动跳过。使用mocha执行测试用例时,由于咱们指定了测试报告格式--reporter
参数为mochawesome
,测试报告会被输出为以下的html格式
为了分析当前测试用例对源代码的覆盖状况,咱们使用Istanbul
生成测试覆盖率报告
分别对应上图的Statements、Branches、Functions、Lines,点击左侧连接能够查看源码测试详情,绿色部分表示已被测试覆盖
关于测试覆盖率,须要强调的是,咱们不该该把测试覆盖率的高低做为检验项目质量的标准,只能做为参考。代码覆盖率真正的意义在于帮助开发者找到代码设计的问题,帮助咱们发现为何有的代码没有被测试覆盖到,是代码设计有问题,仍是加入了无用代码,它能够指导咱们在代码设计中作更好的抽象,写可测试的代码。
(2)React组件测试
如今有以下的React组件
// /src/client/components/Empty/index.jsx'
import React, { Component } from 'react'
import { Icon } from 'antd'
const Empty = (props) => {
const placeholder = props.placeholder
return (
<div> <Icon type='meh-o' /> <span>{placeholder || '数据为空'}</span> </div> ) } module.exports = Empty 复制代码
编写测试用例对它进行测试
import React from 'react'
import { expect } from 'chai'
import Enzyme, { mount, render, shallow } from 'enzyme'
import Adapter from 'enzyme-adapter-react-15.4' // 根据React的版本安装适配器
import Empty from '../../src/client/components/Empty/index.jsx'
import { spy } from 'sinon' // 对原有的函数进行封装并进行监听
Enzyme.configure({ adapter: new Adapter() }) // 使用Enzyme 先适配React对应的版本
describe('测试React组件: <Empty />', () => {
it('不传入属性时,组件中span的文本为"数据为空"', () => {
const wrapper = render(<Empty />)
expect(wrapper.find('span').text()).to.equal('数据为空')
})
it('传入属性"我是占位文本"时,组件中span的文本为"我是占位文本"', () => {
const wrapper = render(<Empty placeholder='我是占位文本' />)
expect(wrapper.find('span').text()).to.equal('我是占位文本')
})
})
复制代码
使用mocha执行测试用例会生成以下测试报告,测试经过
测试覆盖率报告以下
5.4.2 接口测试
编写测试用例,使用supertest
实施接口测试
const request = require('supertest')
const { expect } = require('chai')
const BASE_URL = 'http://127.0.0.1:1990'
describe('接口测试:商户登陆测试用例', function () {
it('登陆接口 /api/user/login', function (done) {
request(BASE_URL)
.post('/api/user/login')
.set('Content-Type', 'application/json') // set header内容
.send({ // send body内容
user_code: 666666,
password: 666666
})
.expect(200) // 断言但愿获得返回http状态码
.end(function (err, res) {
// console.info(res.body) // 返回结果
expect(res.body).to.be.an('object')
expect(res.body.data.user_name).to.equal('商户AAAAA')
done()
})
})
})
复制代码
执行接口测试用例生成以下测试报告
5.4.3 e2e测试
编写e2e测试用例,使用selenium-webdriver
驱动浏览器进行功能测试
const { expect } = require('chai')
const { Builder, By, Key, until } = require('selenium-webdriver')
const chromeDriver = require('selenium-webdriver/chrome')
const assert = require('assert')
describe('e2e测试:商户系统端到端测试用例', () => {
let driver
before(function () {
// 在本区块的全部测试用例以前执行
driver = new Builder()
.forBrowser('chrome')
// 设置无界面测试
// .setChromeOptions(new chromeDriver.Options().addArguments(['headless']))
.build()
})
describe.skip('登陆相关传统用例-跳过', function () {...})
describe('登陆商户系统', function () {
this.timeout(50000)
it('登陆跳转', async () => {
await driver.get('http://dev.company.home.ke.com:1990/login') // 打开商户登陆页面
await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/ul/li[1]/input')).sendKeys(666666) // 输入用户名
await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/ul/li[2]/input')).sendKeys(666666) // 输入密码
await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/div/button')).click() // 点击登陆按钮
const currentTitle = await driver.getTitle()
await driver.sleep(2000)
expect(currentTitle).to.equal('商户管理系统')
})
})
after(() => {
// 在本区块的全部测试用例以后执行
driver.quit()
})
})
复制代码
使用mocha执行e2e测试用例生成以下测试报告
下图是selenium-webdriver
驱动chrome浏览器自动运行,进行功能测试
5.4.4 基准测试
假设当前须要测试正则表达式的test
方法和字符串的indexOf
方法的性能,咱们一般会采用以下方法进行测试:让两个方法分别执行1000次,比较哪一个耗时长。
// 判断某个字符串中是否存在特定字符,比较reg.test和str.indexOf性能
const testPerf = (count) => {
var now = new Date() - 1
var i = count
while (i--) {
/o/.test('Hello World!')
}
console.log(`test方法执行${count}次用时`, new Date() - 1 - now)
}
const indexOfPerf = (count) => {
var now = new Date() - 1
var i = count
while (i--) {
'Hello World!'.indexOf('o') > -1
}
console.log(`indexOf方法执行${count}次用时`, new Date() - 1 - now)
}
testPerf(1000)
indexOfPerf(1000)
复制代码
测试结果以下,由于代码执行较快,两个方法执行1000次的时间都为零,没法准确判断代码执行效率
科学的统计方法是须要屡次执行,对大量的执行结果进行采样,咱们可使用工具帮咱们完成这件事,以下使用benchmark
进行测试
// 判断某个字符串中是否存在特定字符,比较reg.test和str.indexOf性能
const Benchmark = require('benchmark')
const suite = new Benchmark.Suite()
// add test
suite.add('正则表达式test方法', function () {
/o/.test('Hello World!')
})
.add('字符串indexOf方法', function () {
'Hello World!'.indexOf('o') > -1
})
// add listeners
.on('cycle', function (event) {
console.log(String(event.target))
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'))
})
// run async
.run({ 'async': true })
复制代码
执行测试代码,结果以下,indexOf
每秒执行的次数比test
每秒执行的次数超出了一个数量级,因此indexOf
性能更好
梳理完单元测试、接口测试、功能测试、基准测试的具体实施方案后,结合自动化测试的特色咱们能够得出如下结论:
前端要不要进行自动化测试,须要根据具体的项目特色进行判断,对于知足如下条件的代码能够进行自动化测试:
最后,要强调一点,咱们的目标是保证代码健壮、可维护,提升开发效率,自动化测试只是一种手段。