维护你和测试大哥的情谊,使用vue单元测试:Jest

维护你和测试大哥的情谊:使用vue单元测试

单元测试

背景

随着工作中项目的日趋复杂,前端的伙伴贡献了超级多的代码,慢慢得,工程质量开始出现隐患:

  • 最开始手生,写不好业务代码,代码逻辑不够清楚,后续难以维护
  • 人员流动造成部分业务模块无人看管
  • 只靠测试大哥肉眼手工验收,双方都累

让机器来辅助测试吧,也就是Web自动化测试

概念

常见的机器测试分两种:

  • 单元测试Uint testing。对单一功能进行测试
  • E2E 端对端测试。使用浏览器来模拟用户行为,记录页面的反馈结果,达到测试的目的。这个以后再说。

如果程序员每写完一个功能或组件,就写测试进行验证。一开始没啥用,当功能变多,模块变多之后,测试的效果就有了,代码边界处理,功能联动等。每次引入新功能,增加测试环节,避免误修改,影响之前的代码。

再配合持续集成,每次提交或者发版都执行一次测试,更可以降低bug出现的风险。

只不过,前端这边节奏比较快,对单元测试也陌生,一直没做好。

Vue 里的单元测试方案

我们的项目是 Vue2.x + TS 技术栈。vue项目如何进行单元测试,vue官方文档已经进行了指导: Vue Test Utils

我看测试框架有两类:

  • 纯测试框架,自己配断言库,框架 Mocha 和 AVA。断言库chai, expect
  • 大而全的集成测试框架: Jest 等

看官方文档这意思,推荐两种选择:

  • Jest 比较方便,开箱即用。内置了 expect断言库。但使用的 Vue-jest 来解析vue单文件,好像达不到 Vue-loader 100% 的功能。
  • Mocha+webpack 配置项比较多,走的是 Vue-loader 原汁原味。

总之:简单的不全面,全面的不简单。

这篇文章主要讲 Jest,另一篇vue单元测试文章再关注 mocha+webpack

单元测试长什么样

说一下单元测试的写法,这个是 expect 的典型用法

1
2
3
4
5
6
7
8
9
describe("测试组",()=>{
it("用例1",()=>{
expect(xx).toBe(true)
})

it("用例2",()=>{
expect(xx).toBe(true)
})
})

expect 库

jest内置了expect断言库,这里记录一下常用的api,就几个。

  • 判断相等
    • 万能的 toBe(xxx) 里面可以是任意类型
    • toEqual 判断该内容是否相等
  • 类型判断,不太常用,可以用万能的 toBe来代替
    • toBeNull
    • toBeUndefined
    • toBeTruthy
    • toBeFalsy
  • 数字比较
    • 大于小于 大于等于 toBeGreaterThan toBeLessThan toBeGreatThanOrEqual
    • 因为小数加减有问题,可以使用 toBeCloseTo
  • 字符串判断
    • toMatch(reg) 字符串判断
  • 数组判断
    • toContain
  • 错误异常
    • toThrow()

Jest

刚才jest说达不到 Vue-loader 的完全内容,有缺点。到底是啥?

不支持 自定义块,不支持样式加载。不支持webpack里的代码分割。

其实问题不大。开箱即用,省心省力。通过cli脚手架工具启用 Unit Test - Jest

已存在的项目如果需要添加jest,两个选择

  • 使用vue封装好了的预设文件。如非精通,建议走这个 Vue add @Vue/unit-jest
  • 手动启用,完全自定义。

接下来说手动如何启用。

安装npm包

1
2
3
4
5
6
7
8
9
npm i -D jest @Vue/test-utils Vue-jest babel-jest
npx jest

#
如果是第一次用
npx jest --init
#后续生成覆盖率报告
npx jest --coverage
# 动态监听变化
npx jest --watchAll

配置

通过Vue add @Vue/unit-jest 会安装@Vue/cli-plugin-unit-jest 这个插件里内置了四套预设:

  • @Vue/cli-plugin-unit-jest 常见的默认预设
  • @Vue/cli-plugin-unit-jest/presets/typescript-and-babel 具有 ts+tsx+babel的预设
  • 其他

Vue真的是封装简单,具体配置细节可以看这里,官方git仓库

内部实现细节如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const deepmerge = require('deepmerge')
const defaultTsPreset = require('../typescript/jest-preset')

module.exports = deepmerge(
defaultTsPreset,
{
globals: {
'ts-jest': {
babelConfig: true
}
}
}
)

完全手动配置 jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"jest": {
"moduleFileExtensions": ["js","json","Vue"], // jest处理文件
"transform": {
".*\\.(Vue)$": 'Vue-jest', // 用 Vue-jest 处理 *.Vue 文件
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
transformIgnorePatterns: [
'/node_modules/'
],
"moduleNameMapper":{
"^@/(.*)$": "<rootDir>/src/$1" // @ 别名匹配
}
}
}

// 注意testMatch: ['**/tests/unit/**/*.(spec|test).(js|Vue|)']

因为是node模拟的浏览器环境,node对es6+支持得好,可以指定node环境,从而跳过一些babel编译。jest会自动检测是不是 babel-jest 环境,从而去看 .babelrc文件。.babelrc:

1
2
3
4
5
6
7
8
{
"presets": [["env", { "modules": false }]],
"env": {
"test": {
"presets": [['env',{"targets":{"node":"current"}}]]
}
}
}

unit/.eslintrc.js

1
2
3
4
5
module.exports = {
env: {
jest: true
}
}

测试覆盖率。jest.config.js 或 jest 配置

1
2
3
4
5
6
7
{
"jest": {
"collectCoverage": true,
"collectCoverageFrom": ["**/*.{js,Vue}", "!**/node_modules/**"],
"coverageReporters": ["html", "text-summary"]
}
}

技术细节

mock

网络请求

模拟 axios 发送请求。

要么使用 done()回调,要么使用 async await ,显然是使用后者。

jest.fn() 可以捕获函数调用,可以追溯调用。也可以模拟函数返回

功能:

  • 可以捕获函数的调用和返回结果,以及this
  • 自由设置函数返回结果 jest.fn().mockReturnValue(‘xxx’)
  • 改变函数的内部实现

真实的项目里,一般不测试真正的接口。接口的测试属于接口自动化测试放范畴。

1
2
3
4
5
6
jest.mock('axios');
test.only('test axios', async()=>{
exprect.assertion(1) // 下面语法必须执行一次。
axios.get.mockResolvedValue({data:'hello'})
await getData().then()
})

为了统一管理,可以设置 __mocks__ 文件夹,不拦截axios了,改为mock整个请求

这个也和配置项中 automock 参数

  • /__mocks__/demo.js 手动设计 promise.resolve 对象
  • test里 jset.mock(./demo) 这里会自动匹配demo.js ,然后返回结果

异步的mock,同步的不mock

1
2
3
jest.mock('./demo')
import { fetch } frmo './demo'
const { a} = jest.requireActural('./demo')

定时器

如果js里 setTimeout 10000 等待10s如何验证?

1
2
3
4
5
6
7
8
9
10
import timer from './timer'

jest.useFakeTimers()
test('',()=>{
const fn = jest.fn()
timer(fn)
//jest.runAllTimers()
jest.advancedTimesByTime(3000) // 任意快进时间
expect(fn).toHaveBeenCalledTimer(1)
})

###钩子函数

  • beforeAll 比如 实例化
  • afterAll
  • beforeEach
  • afterEach

snapshot

expect().toMatchSnapshot()快照。

防止组件内容发生变化,UI测试

第一次执行,会自动创建快照。以后配置项有变化会提示有变化,可以手动去更新配置。

@Vue/test-units

配合我的思维导图使用。说了不少,可以看 demo 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { mount } from '@Vue/test-utils'
import Component from './component'

describe('Component', () => {
test('is a Vue instance', () => {
const wrapper = mount(Component) // 挂载
expect(wrapper.isVueInstance()).toBeTruthy() // 判断是否实例化
})

it('props',()=>{
cosnt prop='xxx'
const box = shallowMount(Compoent,{
propsData:{msg:prop }
})
// box.setProps()
expect(box.text().toMatch(msg))
// 创建快照
expect(box).toMatchSnapshot()
})

it('点击按钮获取数据', done => {
const wrapper = shallowMount(Foo)
wrapper.find('button').trigger('click')
wrapper.vm.$nextTick(() => { // 异步记得使用 done 和 nextTick
expect(wrapper.vm.value).toBe('value')
done()
})
})
})

配合 Vue-router

测试环境不要use router,因为会全局添加 $route$router 只读属性

我看,是使用 createLocalVue 再 use(vuerouter)

官网demo

1
2
3
4
5
6
7
8
9
10
11
import { shallowMount, createLocalVue } from '@Vue/test-utils'
import VueRouter from 'Vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()

shallowMount(Component, {
localVue,
router
})

这一块不太懂

配合 Vuex

先略

参考文档

  1. Vue Test Utils 官方文档

请我喝杯咖啡吧~