在多人合作的大项目中,如果出现了bug,其他模块也受影响,去定位bug的位置和责任人需要花费非常多的时间,修改某个模块,可能引起整体主流程出现问题,对于组件,更需要单元测试,并且要达到一定程度的测试覆盖率
前端自动化测试产生的背景及原理
单元测试定义
- 单元测试:对软件中的最小可测试单元进行测试
- 最小可测试单元:方法(组成整体功能和整个系统的最小单元)
为什么要进行单元测试
- 分模块开发的大项目,方便定位到哪个模块出了问题
- 保证代码质量,各个开发对自己写的方法进行单元测试,降低出问题的概率
- 由测试驱动开发,先写单元测试,再根据怎么测驱动开发
单元测试分类
- TDD test driven development
- 测试驱动开发,从需求角度出发
- 需求分析(分析需要哪些方法) => 编写单元测试 => 编写功能代码使单元测试通过 => 重构
- 当测试用例都通过后,相应的功能也就都做完了
- 测试重点在于代码
- 一般结合单元测试使用,是白盒测试
- 优点
- 很好的诠释了代码即文档
- 清晰地了解软件的需求
- 代码质量更好(组织,可维护性)
- 测试覆盖率高
- 错误测试代码不容易出现
- TDD适合于对系统代码质量和测试覆盖率有要求的开发主体,比如函数和组件库
- BDD behavior driven development
- 行为驱动开发,从具体功能角度出发
- 业务角度定义目标 => 找到实现目标的方法 => 编写单元测试 => 实现行为 => 检查产品
- 在功能开发完成后,以用户的操作为指导编写测试代码
- 测试重点在于UI(DOM)
- 一般结合集成测试使用,是黑盒测试
- 优点
- 保证程序实现效果和用户需求一致
-
BDD的模式适用于平时的业务代码开发,因为业务的需求有可能变更频繁
-
测试原则
- 根据需求变更及时修改和维护
- code review
- 只测单一的点,单一方法(不要有其它方法影响)
单元测试核心内容
- 测试框架
- 断言库
- 用来判断结果
- Mock库
- 屏蔽掉方法中其它方法的影响
- Test runner
- 跑单元测试的环境
- 覆盖率工具
单元测试常用库
常用测试框架
- Jest
- 基于jasmine,对react友好
- Jasmine
- BDD风格,已集成assert(断言库)和mock库
- 全部集成
- Mocha
- 支持TDD和BDD
- 不带assert和mock,需要自己搭配,可自定义
- 全面适合node和浏览器两个端
- Qunit
- 出自jquery,后来独立出来(较古老)
常用的是 jest 和 mocha+karma,最大区别在于是否全部集成了断言库和mocha库
断言库
- Chai
- 支持所有风格——全面
- Assert
- node环境下直接使用
- Should
- expect
最常用的是 Chai
Mock
- sinon
Test runner
- karma
- 模拟的浏览器:无头浏览器,可以模拟用户在真实浏览器上的操作( puppeteer)
覆盖率工具
- istanbu
在vue中使用单元测试
单元测试demo
- 单元测试是针对最小可测试单元,如果复杂模块,需要进行拆分成单一函数再进行测试
function a() {
return 2
}
function b() {
div1.innerHTML = '123'
}
/**
* @description: 对某个项目进行单元测试(总的)
* @param {
* param1:描述某个组件
* param2:测试用例的回调
* }
* @return {type}
*/
describe('this is a test for X', () => {
/**
* @description: it语法新建一个测试用例
* @param {
* param1:对这个测试用例的描述
* param2:测试用例执行方法的回调
* }
* @return {type}
*/
it('test function a', () => {
expect(a()).to.equal(2)
})
// 测试没有返回值的情况
it('test function b', () => {
b()
expect(div.innerHTML).to.equal('123')
})
})
vue项目中编写单元测试
vue中的测试,在vue-cli搭建工具中推荐的两种测试是e2e(端到端测试)和unit test(单元测试)
- e2e测试 end to end
- 认为整个系统都是黑箱,只有UI会暴露给用户,一般是测试来测,与前端关系不大
- 单元测试
- 对一个模块,一个函数或者一个类来进行正确性检验的测试工作tzr
vue项目中选择karma+mocha
初始化后,运行npm run test,运行结果如下
import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'
import { expect } from 'chai'
import axios from 'axios'
// 引入axios必需要引入的Promise polyfill
import Promise from 'es6-promise'
Promise.polyfill()
describe('HelloWorld.vue', () => {
// Vue.extend拿到HelloWorld的构造函数
const Constructor = Vue.extend(HelloWorld)
// 新建这个组件的构造函数,也就是组件里的this
const vm = new Constructor().$mount()
// mocha的钩子函数
// 所有用例执行前
before(() => {
console.log(1)
})
// 每个用例执行前
beforeEach(() => {
console.log(2)
})
// 所有用例执行后
after(() => {
console.log(3)
})
it('should render correct contents', () => {
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App')
})
it('is m1 add two arguments', () => {
const m1 = vm.m1
expect(m1(1, 2)).to.equal(3)
})
// 异步函数的用例,用回调
it('async m2 should return', () => {
const m2 = vm.m2
m2(2, 2, (data) => {
expect(data).to.equal(4)
})
})
// 异步接口请求的用例
it('接口请求', () => {
const m3 = vm.m3
// 3种方式
// spy间谍函数,获取到调用信息,不会屏蔽
// stub 屏蔽掉请求的方法
// 先屏蔽掉实际请求
let callback = sinon.stub(axios, 'get')
// 再通过spy新建一个请求,模拟他的返回值
let axiosSpy = sinon.spy(() => {
return 4
})
// 检测拿到的结果是否正确
expect(m3(axiosSpy)).to.equal(4)
callback.restore()
})
})
vue项目中选择jest
初始化后,运行npm run test,会报错,需要先在 jest.conf.js 文件中注释掉以下行
然后还有报错,需要安装依赖
npm i -D @vue/test-utils babel-jest jest jest-serializer-vue vue-jest
然后还有报错,网上查询解决方案
stackoverflow.com/questions/5…
npm install --save-dev babel-jest@22.4.3
jest中的匹配器
jest测试异步代码
接收回调的异步
// fetchData.js
export const fetchData = (fn) => {
axios.get('http://www.dell-lee.com/react/api/demo.json').then(res => {
fn(response.data)
})
}
对应的单元测试
test('fetchData 返回结果为{success: true}', (done) => {
fetchData().then(data => {
expect(data).toEqual({
success: true
})
done() // 必须要有done才会测试这个异步函数的返回值,不然走完fetchData就算是测试通过了9
})
})
return promise的异步
// fetchData.js
export const fetchData = () => {
return axios.get('http://www.dell-lee.com/react/api/demo.json')
}
对应的单元测试
// 方式1
test('fetchData 返回结果为404', () => {
expect.assertions(1) // 至少执行一次(如果不加这行,就不会进入catch, 这个次数对应的是catch里的expect个数)
return fetchData().catch(e => {
expect(e.toString().indexOf('404') > -1).toBe(true)
})
})
// 方式2
test('fetchData 返回结果为404', async () => {
// 除了async,await还可以用return
await expect(fetchData()).resolves.toMatchObject({
data: {
success: true
}
})
})
// 方式3 对于异常
test('fetchData 测试异常', () => {
return expect(fetchData()).rejects.toThrow()
})
jest中的钩子函数
一个describe内部的钩子函数对内部所有函数都有效
- beforeAll 所有测试用例执行之前
- afterAll 所有测试用例执行完成之后
- beforeEach
- afterEach
- describe 可以通过 describe 块来将测试分组。 当 before 和 after 的块在 describe 块内部时,则其只适用于该 describe 块内的测试
单元测试编写原则
前端自动化测试中,不会发送真正的请求,需要用mock请求