banner
NEWS LETTER

前端自动化测试 【JEST】

Scroll down

前端自动化测试框架 JEST。

JEST 框架的安装使用

安装:npm install jest@24.8.0 -D ,推荐使用这个版本。

package.json 文件的 script 里面的 test 修改如下:

"scripts": {
// 修改了以后 执行命令就会去找带test.js的后缀文件 并自动监听
"test": "jest --watchAll"
},

执行测试命令: npm run test

  • 单元测试:模块测试。
  • 集成测试:多模块测试。

expect(要测试的), .toBe(此方法期望的结果 ) 它是一个匹配器。

// 注意,jest 测试框架支持 CommonJS 模块化规范
// xxx.test.js文件 ===> 这是测试文件
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)
})

// 这是被测试文件 math.js
function add(a, b) {
return a + b
}
function minus(a, b) {
return a - b
}
function multi(a, b) {
return a * b
}

try {
// 使浏览器不报错 使用 try catch
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

// 配置babel 新建.babelrc 文件进行配置
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
// 这样就可以使用 ES6的模块语法

JEST 中的匹配器

  1. 测试值相等,它无法匹配对象内容的引用
test('测试加法 3 + 7', () => {
// toBe 匹配器 matchers 相当于 object.js ===
// 字符串 数值 布尔值 简单类型推荐使用
expect(10).toBe(10)
})
  1. 测试对象内容相等
test('测试对象内容相等', () => {
// toEqual 匹配器 引用类型推荐使用
const a = { one: 1 }
expect(a).toEqual({ one: 1 })
})
  1. 变量和 null 进行匹配
test('null匹配器', () => {
// toBeNull 匹配器
const a = null
expect(a).toBeNull()
})
  1. 真假有关的匹配器
test('toBeUndefined 匹配器', () => {
const a = undefined
expect(a).toBeUndefined()
})

test('toBeDefined 匹配器', () => {
const a = 1 // 定义过的才可以
expect(a).toBeDefined()
})

test('toBeTruthy 相当于true匹配器', () => {
const a = '1'
// 判断是否为真的匹配器 相当于 true 的条件才可以
expect(a).toBeTruthy()
})

test('toBeFalsy 相当于false匹配器', () => {
const a = 0
// 判断是否为假的匹配器 相当于 false 的条件才可以
expect(a).toBeFalsy()
})

test('not 取反匹配器', () => {
const a = 1
expect(a).not.toBeFalsy()
})
  1. 数字相关的 匹配器
// 大于匹配器
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)
})
  1. 字符串相关的匹配器
// 是否包含某个字符串
test('toMatch', () => {
const str = 'http://www.dell-lee.com'
expect(str).toMatch('dell') // 也可以写表达式
})
  1. 数组相关的匹配器
// 是否数组包含某个项
test('toContain', () => {
const arr = ['dell', 'lee', 'imooc']
const data = new Set(arr)
expect(data).toContain('dell')
})
  1. 处理异常的匹配器
// 异常
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)
})
}
// 使用 done 作为回调的第二个参数,否则不执行后面的语句
.test.js文件
import { fetchData } from './fetchDate';

test('fetchData 返回结果为 { success: true }', (done) => {
fetchData((data) => {
expect(data).toEqual({
success: true
})
done()
})
})
// 方法2 直接返回Promise 推荐
.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
})
})
})

// 如果需要满足404 类似错误的结果
// 比如 输错了请求地址
.test.js
test('fetchData 返回结果为 404', () => {
expect.assertions(1) // 使用 catch 一定记得用它
return fetchData().catch(e => {
expect(e.toString().indexOf('404') > 1).toBe(true)
})
})

另一种处理异步代码的测试方法

test('fetchData 返回结果为 { success: true }', () => {
// 是否包含这个子对象
return expect(fetchData()).resolves.toMatchObject({
data: {
success: true,
},
})
})

// --------- 还可以用 async await 方式写

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)
})
// 这样在测试过程中,第二个方法执行就会以第一个方法执行为标准,耦合度高
// 不是想要得到的结果,addOne + 1了 minusOne又在addOne基础上-1
// 相互之间存在依赖

如果要在测试之前对一些公共信初始化 推荐使用钩子函数 beforeEach

  • beforeAll( ()=>{} ) 第一个运行的钩子函数。
  • befaoreEach( ()=>{} ) 当每个测试用例执行之前,都先执行下 beforeEach 钩子函数。
  • afterEach( ()=>{} ) 当每个测试用例执行之后 都会执行这个钩子函数。
  • afterAll( ()=>{} ) 等待所有的测试用例结束后执行的钩子函数。
import Counter from './Counter'

// 如果要在测试之前对一些公共信初始化 推荐使用钩子函数

let counter = null

beforeEach(() => {
counter = new Counter() // 将初始化工作放入 beforeEach 中
})

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

作用:

  1. 捕获函数的调用和返回结果,以及 this 和调用顺序。
  2. 它可以自由的设置返回结果。
  3. 改变内部函数的实现,比如下面的 测试 axios 请求。
// .js 文件
export const runCallback = (callback) => {
return callback()
}

// .test.js 文件
import { runCallback } from './demo'
test('测试 runCallbaack', () => {
const func = jest.fn() // jest 提供的 mock 函数
runCallback(func)
expect(func).toBeCalled() // 判断函数是否被调用过
})

// 如果多次调用
import { runCallback } from './demo'

test('测试 runCallbaack', () => {
const func = jest.fn() // jest 提供的 mock 函数
runCallback(func)
runCallback(func)
// func.mock.calls.length 拿到调用的次数
expect(func.mock.calls.length).toBe(2) // 断言 比如上面调用了两次
// 类似于 expect(func).toBeCalledWith(['abc'])
// 区别在于 上面是第一次调用的时候参数是 abc 下面是每一次调用是 abc
console.log(func.mock)
})


// 如果要拿到传递的参数
// .js文件
export const runCallback = (callback) => {
return callback('abc');
}
.test.js文件
import { runCallback } from './demo';

test('测试 runCallbaack', () => {
const func = jest.fn(); // jest 提供的 mock 函数
runCallback(func);
console.log(func.mock.calls[0]);
// 通过 func.mock.call[0]
expect(func.mock.calls[0]).toEqual(['abc'])
})

// 如果要单独设置返回值
import { runCallback } from './demo';

test('测试 runCallbaack', () => {
const func = jest.fn(); // jest 提供的 mock 函数
// 它支持链式操作 Once 是每次设置一个 不加Once 就全部添加
func.mockReturnValueOnce('Dell')
.mockReturnValueOnce('Lee')
.mockReturnValueOnce('Hello')
// 还有种写法更底层强悍 可以写一个逻辑处理
// func.mockImplementationOnce(()=>{ // 也可以不带Once
// return 'dell'
// console.log(...) 等等逻辑
// })
runCallback(func);
runCallback(func);
runCallback(func);
console.log(func.mock)
})

测试 axios 请求:

// .js 文件
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'); // jset 对 axios 做一个模拟
test.only('测试 getDate', async () => {
axios.get.mockResolvedValue({ data: 'hello' })
await getData().then((data) => {
expect(data).toBe('hello');
})
})

如果要返回 this,可以使用 func.mockReturnThis(),了解即可!

mock 的深入学习:

// .js
import axios from 'axios'

export const fetchData = () => {
return axios.get('/').then((res) => res.data)
}

//.test.js

jest.mock('./demo') // 使用的是 __mocaks__ 里面的demo文件
import { fetchData } from './demo'

test('fetchDate 测试', () => {
return fetchData().then((data) => {
expect(eval(data)).toEqual('123')
})
})
// 注意新建 __mocks__ 下面在建.js文件
export const fetchData = () => {
return new Promise((resolve, reject) => {
resolve("(function(){return '123'}())")
})
}

// 另一种方法 修改 config.js 里面的 automocak: true

如果即有需要 mockaxios 请求 又有非 axios 数据 就要这么写,不模拟真实就要用 jest.requireActual

// .js
import axios from 'axios'

export const fetchData = () => {
return axios.get('/').then((res) => res.data)
}

export const getNumber = () => {
return 123
}

// .test.js

jest.mock('./demo')
import { fetchData } from './demo'
// 注意,使用真实的 ./demo.js 而不是 __mocks__里面的.js
const { getNumber } = jest.requireActual('./demo')

test('fetchDate 测试', () => {
return fetchData().then((data) => {
expect(eval(data)).toEqual('123')
})
})

// 下面针对进行测试
test(`getNumber 测试`, () => {
expect(getNumber()).toEqual(123)
})

mock timers

// .js 文件
export default (callback) => {
setTimeout(() => {
callback()
setTimeout(() => {
callback()
}, 3000)
}, 3000)
}

// .test.js 文件
// 解决方法一:
import timer from './timer'
// 使用这个方法
jest.useFakeTimers()

test('timer 测试', () => {
const fn = jest.fn()
timer(fn)
// 使定时器立即执行
jest.runAllTimers()
// 希望被调用几次,因为上面.js调用了两次
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 钩子函数中
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 快照测试

// .js
export const generateConfig = () => {
return {
server: 'http://localhost',
post: 8080,
domain: 'localhost',
}
}
//.test.js 文件
import { generateConfig } from './demo'
test('测试 generateConfig', () => {
expect(generateConfig()).toMatchSnapshot()
})
// 会生成对应的快照文件

当配置文件是变化的情况下:

// .js
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(),
}
}
// .test.js
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 来提升性能的测试。

// util.js
class Util {
a() {
// ...很复杂的逻辑
}

b() {
// ...很复杂的逻辑
}
}

export default Util
// util.test.js
import Util from './util'
let util = null

beforeAll(() => {
util = new Util()
})

test('测试 a 方法', () => {
// expect(util.a(1, 2)).toBe('12');
})

// test.js
import Util from './util';


const testFunction = (a, b) => {
const util = new Util();
util.a(a);
util.b(b);

}

export default testFunction;

// .test.test.js
jest.mock('./util');
// 底层会把 Util下面的a,b会变成 jest.fn.a/b
import Util from './util';
import testFunction from './test';


test('测试 testFunction', () => {
testFunction();
expect(Util).toHaveBeenCalled();
// 针对上文的类中的两个方法a/b进行测试
expect(Util.mock.instances[0].a).toHaveBeenCalled();
expect(Util.mock.instances[0].b).toHaveBeenCalled();
})

JEST 中对 DOM 进行测试

// .js
import $ from 'jquery'

const addDivToBody = () => {
$('body').append('<div></div>')
}

export default addDivToBody

//.test.js
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) 从红到绿的一种开发模式,因为前期测试代码一片红。

  1. 编写测试用例
  2. 运行测试用例,测试用例无法通过测试
  3. 编写代码,使测试用例通过
  4. 优化代码,完成开发
  5. 重复上述步骤

TDD 的优势:

  • 长期减少回归 bug (编写代码过程中,测试代码及时提示)。
  • 代码质量更好(组织,可维护性)。
  • 测试覆盖率高。
  • 错误的测试代码不容易出现。

Vue 环境中配置 JEST

package.jsontest:unit 尾部添加 --watch

@vue/test-utils 的配置及使用。

// 修改 jest.config.js 配置文件如下信息:
testMatch: [
'**/tests/unit/**/*.(spec|test).(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],

用例:

// 浅渲染,只会渲染当前组件,不会影响子组件
import { shallowMount } from '@vue/test-utils'
// 只对当前组件 HelloWorld 组件进行浅渲染
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})


// wrapper 的几个方法
wrapper.text() 可以得到当前组件的文本信息
wrapper.props('msg') 也可以拿到对应的 props 信息
wrapper.setProps({msg: 'hello'})
wrapper.find() 可以查找它的类名等信息
wrapper.findAll() 同上 查询多个
// 使用findAll拿数据要这样拿记得 at()----> expect(countElem.at(0).text()).toEqual('0')
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"]')
// input 是否存在 API 在 vue-test-utils
expect(input.exists()).toBe(true)
})

it('Header 中 input 框初始内容为空', () => {
const wrapper = shallowMount(Header)
// 找到这个 new 的实例
// 通过 wrapper.vn.$data 拿到 VUE 实例里面 data 的属性
const inputValue = wrapper.vm.$data.inputValue
expect(inputValue).toBe('')
})

it('Header 中 input 框输入回车 无内容时,无反应', () => {
const wrapper = shallowMount(Header)
const input = wrapper.find('[data-test="input"]')
// 设置 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 自动规范化代码。

我很可爱,请给我钱

其他文章