前端自动化测试框架 JEST。
JEST 框架的安装使用
安装:npm install jest@24.8.0 -D
,推荐使用这个版本。
package.json 文件的 script 里面的 test 修改如下:
"scripts": {
"test": "jest --watchAll" },
|
执行测试命令: npm run test
。
expect(要测试的), .toBe(此方法期望的结果 ) 它是一个匹配器。
const math = require('./math.js') const { add, minus, multi } = math
test('测试加法 3 + 7', () => { expect(add(3, 7)).toBe(10) })
test('测试减法 3 - 3', () => { expect(minus(3, 3)).toBe(0) })
test('测试乘法 3 * 3', () => { expect(multi(3, 3)).toBe(9) })
function add(a, b) { return a + b } function minus(a, b) { return a - b } function multi(a, b) { return a * b }
try { module.exports = { add, minus, multi, } } catch (e) {}
|
JEST 的配置
配置 JEST npx jest --init
。
根据需要选择 node
或者 浏览器环境。
前三个都默认yes
会生成 jest.config.js
文件。
执行如下命令会生成测试覆盖率的申明 会生成一个 coverage
目录。
npx jest --coverage
使 JEST 识别 ES6 模块语法,需安装 babel pm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D
。
{ "presets": [ [ "@babel/preset-env", { "targets": { "node": "current" } } ] ] }
|
JEST 中的匹配器
- 测试值相等,它无法匹配对象内容的引用
test('测试加法 3 + 7', () => { expect(10).toBe(10) })
|
- 测试对象内容相等
test('测试对象内容相等', () => { const a = { one: 1 } expect(a).toEqual({ one: 1 }) })
|
- 变量和 null 进行匹配
test('null匹配器', () => { const a = null expect(a).toBeNull() })
|
- 真假有关的匹配器
test('toBeUndefined 匹配器', () => { const a = undefined expect(a).toBeUndefined() })
test('toBeDefined 匹配器', () => { const a = 1 expect(a).toBeDefined() })
test('toBeTruthy 相当于true匹配器', () => { const a = '1' expect(a).toBeTruthy() })
test('toBeFalsy 相当于false匹配器', () => { const a = 0 expect(a).toBeFalsy() })
test('not 取反匹配器', () => { const a = 1 expect(a).not.toBeFalsy() })
|
- 数字相关的 匹配器
test('toBeGreaterThan', () => { const a = 10 expect(a).toBeGreaterThan(9) })
test('toBeLessThan', () => { const a = 6 expect(a).toBeLessThan(9) })
test('toBeGreaterThanOrEqual', () => { const a = 6 expect(a).toBeGreaterThanOrEqual(6) })
test('toBeCloseTo', () => { const a = 0.1 const b = 0.2 expect(a + b).toBeCloseTo(0.3) })
|
- 字符串相关的匹配器
test('toMatch', () => { const str = 'http://www.dell-lee.com' expect(str).toMatch('dell') })
|
- 数组相关的匹配器
test('toContain', () => { const arr = ['dell', 'lee', 'imooc'] const data = new Set(arr) expect(data).toContain('dell') })
|
- 处理异常的匹配器
const throwNewErrorFunc = () => { throw new Error('this is a new error') }
test('toThrow', () => { expect(throwNewErrorFunc).toThrow() })
|
🔔 注意:如果当一个文件中测试用例过多,只针对其中一个进行测试,可以添加.only。
test.only('测试 Counter 中的 addOne 方法', () => { counter.addOne() expect(counter.number).toBe(1) })
|
JEST 命令工具的使用
当执行测试命令 npm run test 测试代码的时候会提示 w
切换。
f : f 的意思是 只对之前当前文件测试失败的代码进行测试 退出在 f。
o : 当有多个测试文件测试的时候,需要使用它 或者改 watchAll 为 watch。
t : 过滤模式 需要针对 pattern 输入对应要检测的模式。
q : 退出模式。
p : 它的作用是类似正则一样,检测比如 输入 matchers 它就会去找类似的文件。
异步代码的测试方法
.js文件 import axios from 'axios'; const axios = require('axios')
export const fetchData = (fn) => { axios.get('http://www.dell-lee.com/react/api/demo.json') .then((res) => { fn(res.data) }) }
.test.js文件 import { fetchData } from './fetchDate';
test('fetchData 返回结果为 { success: true }', (done) => { fetchData((data) => { expect(data).toEqual({ success: true }) done() }) })
.js 文件 import axios from 'axios';
export const fetchData = () => { return axios.get('http://www.dell-lee.com/react/api/demo.json') }
.test.js 文件 test('fetchData 返回结果为 { success: true }', () => { return fetchData().then(res => { expect(res.data).toEqual({ success: true }) }) })
.test.js test('fetchData 返回结果为 404', () => { expect.assertions(1) return fetchData().catch(e => { expect(e.toString().indexOf('404') > 1).toBe(true) }) })
|
另一种处理异步代码的测试方法
test('fetchData 返回结果为 { success: true }', () => { return expect(fetchData()).resolves.toMatchObject({ data: { success: true, }, }) })
test('fetchData 返回结果为 404', async () => { expect.assertions(1) await fetchData().catch((e) => { expect(e.toString().indexOf('404') > 1).toBe(true) }) })
test('fetchData 返回结果为 { success: true }', () => { return expect(fetchData()).rejects.toThrow() })
|
推荐写法
test('fetchData 返回结果为 { success: true }', async () => { const res = await fetchData() expect(res.data).toEqual({ success: true, }) })
test('fetchData 返回结果为 404', async () => { expect.assertions(1) try { await fetchData() } catch (e) { expect(e.toString()).toEqual('Error: Request failed with status code 404') } })
|
JEST 中的钩子函数
不使用钩子函数的情况,会有一些问题:
.js 文件 export default class Counter { constructor() { this.number = 0 } addOne() { this.number += 1 } minusOne() { this.number -= 1 } }
.test.js 文件
import Counter from './Counter'
let counter = new Counter
test('测试 Counter 中的 addOne 方法', () => { counter.addOne() expect(counter.number).toBe(1) })
test('测试 Counter 中的 minusOne 方法', () => { counter.minusOne() expect(counter.number).toBe(0) })
|
如果要在测试之前对一些公共信初始化 推荐使用钩子函数 beforeEach
。
beforeAll( ()=>{} )
第一个运行的钩子函数。befaoreEach( ()=>{} )
当每个测试用例执行之前,都先执行下 beforeEach
钩子函数。afterEach( ()=>{} )
当每个测试用例执行之后 都会执行这个钩子函数。afterAll( ()=>{} )
等待所有的测试用例结束后执行的钩子函数。
import Counter from './Counter'
let counter = null
beforeEach(() => { counter = new Counter() })
test('测试 Counter 中的 addOne 方法', () => { counter.addOne() expect(counter.number).toBe(1) })
test('测试 Counter 中的 minusOne 方法', () => { counter.minusOne() expect(counter.number).toBe(-1) })
|
分组钩子:
import Counter from './Counter'
let counter = null
beforeEach(() => { counter = new Counter() })
describe('测试跟加相关的代码', () => { test('测试 Counter 中的 addOne 方法', () => { counter.addOne() expect(counter.number).toBe(1) })
test('测试 Counter 中的 addTwo 方法', () => { counter.addTwo() expect(counter.number).toBe(2) }) })
describe('测试跟减相关的代码', () => { test('测试 Counter 中的 minusOne 方法', () => { counter.minusOne() expect(counter.number).toBe(-1) })
test('测试 Counter 中的 minusTwo 方法', () => { counter.minusTwo() expect(counter.number).toBe(-2) }) })
|
钩子函数的作用域:
钩子函数的作用域跟作用域链的基本差不多,每一个 describe
里面都可以有自己的钩子函数,最外层的 describe
里面的钩子函数可以作用域内部 describe
的测试用例。
JEST 中的 Mock
作用:
- 捕获函数的调用和返回结果,以及 this 和调用顺序。
- 它可以自由的设置返回结果。
- 改变内部函数的实现,比如下面的 测试 axios 请求。
export const runCallback = (callback) => { return callback() }
import { runCallback } from './demo' test('测试 runCallbaack', () => { const func = jest.fn() runCallback(func) expect(func).toBeCalled() })
import { runCallback } from './demo'
test('测试 runCallbaack', () => { const func = jest.fn() runCallback(func) runCallback(func) expect(func.mock.calls.length).toBe(2) console.log(func.mock) })
export const runCallback = (callback) => { return callback('abc'); } .test.js文件 import { runCallback } from './demo';
test('测试 runCallbaack', () => { const func = jest.fn(); runCallback(func); console.log(func.mock.calls[0]); expect(func.mock.calls[0]).toEqual(['abc']) })
import { runCallback } from './demo';
test('测试 runCallbaack', () => { const func = jest.fn(); func.mockReturnValueOnce('Dell') .mockReturnValueOnce('Lee') .mockReturnValueOnce('Hello') runCallback(func); runCallback(func); runCallback(func); console.log(func.mock) })
|
测试 axios 请求:
export const getData = () => { return axios.get('/api').then(res => res.data) }
.test.js 文件 import { runCallback, createObject, getData } from './demo'; import axios from 'axios'; jest.mock('axios'); test.only('测试 getDate', async () => { axios.get.mockResolvedValue({ data: 'hello' }) await getData().then((data) => { expect(data).toBe('hello'); }) })
|
如果要返回 this
,可以使用 func.mockReturnThis()
,了解即可!
mock 的深入学习:
import axios from 'axios'
export const fetchData = () => { return axios.get('/').then((res) => res.data) }
jest.mock('./demo') import { fetchData } from './demo'
test('fetchDate 测试', () => { return fetchData().then((data) => { expect(eval(data)).toEqual('123') }) })
export const fetchData = () => { return new Promise((resolve, reject) => { resolve("(function(){return '123'}())") }) }
|
如果即有需要 mock
的 axios
请求 又有非 axios
数据 就要这么写,不模拟真实就要用 jest.requireActual
。
import axios from 'axios'
export const fetchData = () => { return axios.get('/').then((res) => res.data) }
export const getNumber = () => { return 123 }
jest.mock('./demo') import { fetchData } from './demo'
const { getNumber } = jest.requireActual('./demo')
test('fetchDate 测试', () => { return fetchData().then((data) => { expect(eval(data)).toEqual('123') }) })
test(`getNumber 测试`, () => { expect(getNumber()).toEqual(123) })
|
mock timers
export default (callback) => { setTimeout(() => { callback() setTimeout(() => { callback() }, 3000) }, 3000) }
import timer from './timer'
jest.useFakeTimers()
test('timer 测试', () => { const fn = jest.fn() timer(fn) jest.runAllTimers() expect(fn).toHaveBeenCalledTimes(2) })
import timer from './timer' jest.useFakeTimers() test('timer 测试', () => { const fn = jest.fn() timer(fn) jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(1) jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(2) })
import timer from './timer'
beforeEach(() => { jest.useFakeTimers() })
test('timer 测试', () => { const fn = jest.fn() timer(fn) jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(1) jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(2) })
|
JEST 中的 Snapshop 快照测试
export const generateConfig = () => { return { server: 'http://localhost', post: 8080, domain: 'localhost', } }
import { generateConfig } from './demo' test('测试 generateConfig', () => { expect(generateConfig()).toMatchSnapshot() })
|
当配置文件是变化的情况下:
export const generateConfig = () => { return { server: 'http://localhost', post: 8080, domain: 'localhost', time: new Date(), } }
export const generateConfig1 = () => { return { server: 'http://localhost', post: 8086, domain: 'localhost', time: new Date(), } }
import { generateConfig, generateConfig1 } from './demo'
test('测试 generateConfig', () => { expect(generateConfig()).toMatchSnapshot({ time: expect.any(Date), }) })
test('测试 generateConfig1', () => { expect(generateConfig1()).toMatchSnapshot({ time: expect.any(Date), }) })
|
行内快照 toMatchInlineSnapshot
安装模块 npm install prettier@1.18.2 --save
栗如下:放在到测试用例下面
test('测试 generateConfig1', () => { expect(generateConfig1()).toMatchInlineSnapshot( { time: expect.any(Date), }, ` // 行内快照 放到了测试用例下面 Object { "domain": "localhost", "post": 8086, "server": "http://localhost", "time": Any<Date>, } ` ) })
|
ES6 中 类的测试 (单元测试)
单元测试就是利用 mock
来提升性能的测试。
class Util { a() { }
b() { } }
export default Util
import Util from './util' let util = null
beforeAll(() => { util = new Util() })
test('测试 a 方法', () => { })
import Util from './util';
const testFunction = (a, b) => { const util = new Util(); util.a(a); util.b(b);
}
export default testFunction;
jest.mock('./util');
import Util from './util'; import testFunction from './test';
test('测试 testFunction', () => { testFunction(); expect(Util).toHaveBeenCalled(); expect(Util.mock.instances[0].a).toHaveBeenCalled(); expect(Util.mock.instances[0].b).toHaveBeenCalled(); })
|
JEST 中对 DOM 进行测试
import $ from 'jquery'
const addDivToBody = () => { $('body').append('<div></div>') }
export default addDivToBody
import $ from 'jquery' import addDivToBody from './demo'
test('测试 addDivToBody', () => { addDivToBody() expect($('body').find('div').length).toBe(1) })
|
Vue 中的 TDD
Test Driven Development (TDD) 测试驱动开发
TDD 开发流程 (Red-Green) 从红到绿的一种开发模式,因为前期测试代码一片红。
- 编写测试用例
- 运行测试用例,测试用例无法通过测试
- 编写代码,使测试用例通过
- 优化代码,完成开发
- 重复上述步骤
TDD 的优势:
- 长期减少回归 bug (编写代码过程中,测试代码及时提示)。
- 代码质量更好(组织,可维护性)。
- 测试覆盖率高。
- 错误的测试代码不容易出现。
Vue 环境中配置 JEST
package.json
中 test:unit
尾部添加 --watch
。
@vue/test-utils
的配置及使用。
testMatch: [ '**/tests/unit/**/*.(spec|test).(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' ],
|
用例:
import { shallowMount } from '@vue/test-utils'
const wrapper = shallowMount(HelloWorld, { propsData: { msg } })
wrapper.text() 可以得到当前组件的文本信息 wrapper.props('msg') 也可以拿到对应的 props 信息 wrapper.setProps({msg: 'hello'}) wrapper.find() 可以查找它的类名等信息 wrapper.findAll() 同上 查询多个
expect(wrapper).toMatchSnapshot() 快照测试
import { mount } from '@vue/test-utils'
|
常用的一些 API
const input = wrapper.find('[data-test="input"]')
自定义 input 属性 判断节点是否存在。expect(input.exists()).toBe(true)
判断 input
是否存在。wrapper.vm.$data.
方式拿到 vue
里面组件的 data
实例的属性。input.setValue('')
设置 input
的属性。input.trigger('keyup.enter')
触发一个方法,模拟用户输入回车。expect(wrapper.emitted().add).toBeFalsy()
判断是否有触发这个事件。wrapper.vm.
方法名 方式拿到 vue
里面组件的 methods
的方法。
实例:
import { shallowMount } from '@vue/test-utils'
import Header from '../../components/Header'
it('Header 包含 input 框', () => { const wrapper = shallowMount(Header) const input = wrapper.find('[data-test="input"]') expect(input.exists()).toBe(true) })
it('Header 中 input 框初始内容为空', () => { const wrapper = shallowMount(Header) const inputValue = wrapper.vm.$data.inputValue expect(inputValue).toBe('') })
it('Header 中 input 框输入回车 无内容时,无反应', () => { const wrapper = shallowMount(Header) const input = wrapper.find('[data-test="input"]') input.setValue('') input.trigger('keyup.enter') expect(wrapper.emitted().add).toBeFalsy() })
it('Header 中 input 框输入回车 有内容时,向外触发事件', () => { const wrapper = shallowMount(Header) const input = wrapper.find('[data-test="input"]') input.setValue('dell lee') input.trigger('keyup.enter') expect(wrapper.emitted().add).toBeTruthy() })
it('Header 中 input 框输入回车 有内容时,向外触发事件 同时清空 inputValue', () => { const wrapper = shallowMount(Header) const input = wrapper.find('[data-test="input"]') input.setValue('dell lee') input.trigger('keyup.enter') expect(wrapper.vm.$data.inputValue).toBe('') })
|
npm run lint --fix
会自动把不规范的代码 按照 lint
自动规范化代码。