文章基调
- 介绍概念及思考的过程,不提供代码(具体代码写法可参考 jest 官网)
-
延长:
- 信息大爆炸时代,各类资源很丰盛,具体教程网上有很多材料
- 具体不过官网,不反复制作雷同的信息,造成额定的心智累赘
- 大脑只是搜索引擎,晓得资源从那里找,不负责记录具体做法,节俭内存
测试的几个名称
- 视觉测试:【测试工具】前端视觉较为多变,故视觉测试的老本较大,普及性不高,但益处在于,能够测试款式信息
- 单元测试:【测试指标】最小颗粒度的测试,针对单个函数或性能,适宜函数库,根底组件库等的测试
- 集成测试:【测试指标】模仿用户操作,面向交付的最终后果,针对我的项目的流程
- TDD(Test Driven Development 测试驱动开发):【方法论】先写测试用例(提出期望值),在写具体的实现办法与函数,使用于单元测试
- BDD(Behavior Driven Development 行为驱动开发):【方法论】基于集成测试
-
本文次要介绍 jest(玩笑) 单元测试库
jest 单元测试的原理与局限性
先介绍原理,是心愿让大家晓得其性能边界,能做什么,不能做什么,理解能力范畴
-
jest 运行在 node 端,底层应用实现库是 jsdom,应用 node 模仿一套 dom 环境,模仿的范畴仅局限于 dom 层级构造及操作
-
【dom 操作】只模仿大部分 dom 通用性能,某些特定性的 dom api 并不反对,如 canvas,video 的媒体性能 api
- 如果要测试 canvas,video 的媒体 API,须要装置对应的扩大库,能够了解为在 node 端实现浏览器的性能,如图片生成,音视频播放等
- canvas 扩大,video 相干扩大临时没找到
-
【css 款式】严格而言,没有 css 款式模仿性能,css 在 jsdom 中只当做纯正的 dom 属性字符串,与 id,class 字符串没有区别
- 不反对继承,每个 dom 都是独立的个体,没有款式上继承。
- 仅反对内联款式,无奈辨认 vue 中的款式
- 不太有用的,解析外链款式的示例
- 这里有个解决方案,但没有失去官网合并
- 非内联的款式测试,须要应用视觉测试库
单元测试须要笼罩那些场景?
-
代码变动
- 间接运行单元测试即可发现,但如何防止开发者遗记了运行单元测试?
- 通过增加 cicd 流程解决,提交 merge request 申请时,触发单元测试,运行失败,则主动回绝合并申请,并执行 node 命令发送音讯揭示
- gitlab ci 相干配置会在文章开端介绍
-
新增代码
- 新增的函数或者性能,运行旧的单元测试不会笼罩到,如何揭示开发者笼罩新增的这部分代码?
- 通过配置测试覆盖率行数 100% 解决,达不到指标,则视为测试不通过,防止新增代码的脱漏。切实无奈笼罩的分支或函数,怎么解决?
- 通过配置「疏忽备注 / istanbul ignore next /」,放弃某文件的百分百覆盖率测试
-
后续有工夫也能够通过全局搜寻这些疏忽配置,来一一笼罩测试,起到标记的作用
coverageThreshold: { './src/common/js/*.js': { branches: 80, // 笼罩代码逻辑分支的百分比 functions: 80, // 笼罩函数的百分比 lines: 80, // 笼罩代码行的百分比 statements: -10 // 如果有超过 10 个未笼罩的语句,则 jest 将失败 } },
-
新增文件,是否脱漏测试
- 个别状况下,单元测试只会跑单元测试文件,新增的代码文件没有对应的测试文件,会呈现漏测的状况
-
通过
collectCoverageFrom
参数指定须要笼罩的文件夹,当该文件夹中的文件没有对应的测试用例,会当作覆盖率 0 解决,起到新文件漏测揭示作用// 从那些文件夹中生成覆盖率信息,包含未设置对其编写测试用例的文件,解决脱漏新文件的测试笼罩问题 collectCoverageFrom: ['./src/common/js/*.{js,jsx}', './src/components/**/*.{js,vue}', ],
-
非凡场景(教训的价值)
- 局部函数,在失常状况下运行是没有问题的,仅在非凡的状况下才会报错,如简略的加法运算,放在小数中就会呈现计算误差,
0.1 + 0.2 = 0.30000000000000004
- 这些非凡场景的笼罩,只能靠一线开发人员在理论工作中记录,须要工夫的积攒
- 这是程序员教训的价值,也是少有的,值得传承的局部
- 局部函数,在失常状况下运行是没有问题的,仅在非凡的状况下才会报错,如简略的加法运算,放在小数中就会呈现计算误差,
单元测试疏忽原理
jest 收集覆盖率底层应用的是 istanbul 库(istanbul:伊斯坦布尔,胜产地毯,地毯用于笼罩),以下疏忽格局都是 istanbul 库的性能
- 疏忽本文件,放在文件顶部 / istanbul ignore file /
- 疏忽一个函数, 一块分支逻辑或者一行代码,放在函数顶部 / istanbul ignore next /
- 疏忽函数参数默认值
function getWeekRange(/* istanbul ignore next */ date = new Date()) {
- 具体疏忽规定可查看 istanbul github 介绍
编写测试用例的正确姿态
以对性能的冀望及定位作为出发点,而不是代码,一开始应先思考该函数或工具库须要起到的性能,而不应该一开始就看代码
- 先列举你冀望的,该组件或者函数的性能,用文本写进去,这也是
test('检测点击事件')
中形容的作用,告知别人这个测试用例的目标 - 编写相应的测试用例
- 对不满足测试用例的代码进行批改
- 察看代码覆盖率,笼罩所有代码行
增加 jest 全局自定义函数
- 如果某测试函数的呈现频率比拟高,能够思考对齐进行复用,写成一个预加载文件,在每个测试文件执行前,加载该文件
- 如获取 dom 款式的原始代码比拟繁琐,
wrapper.element.style.height
,且 element 并没有失去官网裸露,属于外部变量 -
能够通过增加配置文件,编写 styles 全局办法,通过函数的形式获取 style 数据,与 classes 等办法放弃对立
// jest.config.js 设置前置运行文件,在每个测试文件执行前,会运行该文件,实现增加某些全局办法的作用 setupFilesAfterEnv: ['./jest.setup.js'],
// ./jest.setup.js import {shallowMount} from '@vue/test-utils' // 向全局 wrapper 挂载通用函数 styles,返回该元素的内联款式(因为 jsdom 只反对内联款式,不反对检测 class 中的款式),或某内联款式的值 function addStylesFun() { // 生成一个长期组件,获取 vueWrapper 及 domWrapper 实例,挂载 style 办法 const vueWrapper = shallowMount({template: '<div>componentForCreateWrapper</div>'}) const domWrapper = vueWrapper.find('div') vueWrapper.__proto__.styles = function(styleName) {return styleName ? this.element.style[styleName] : this.element.style } domWrapper.__proto__.styles = function(styleName) {return styleName ? this.element.style[styleName] : this.element.style } } addStylesFun()
钩子函数
相似于 vue router 外面的守卫函数,在进入前后执行钩子函数
- 解决有状态函数的数据存储问题,防止执行每一个测试用例时,反复编写代码筹备数据
-
beforeAll、afterAll
- 写在单元测试文件最内部,则代表在该函数在文件执行前、后被执行一次
- 写在测试组 describe 最外层,代表该函数在测试组执行前、后被执行一次
-
beforeEach、afterEach
- 每个测试用例(test)前后执行一次
疾速单元测试技巧
跳过已测试胜利且源码没产生过变更的用例,不再多余执行
-
第一步,jest –watchAll 测试文件产生过变动,则主动执行测试
- 只能在 package script 命令中增加该参数,在 npm 命令后执行不失效
- 源码变更,或单元测试文件变更,都会触发
-
第二步,按下 f(只执行谬误的用例)
- 毛病在于,不能监控已执行胜利的单元测试的变动,以及对应源码的变动,(即之前胜利过的都会被疏忽,不论新的变动,是否产生了谬误)
- 源码变更,或单元测试文件变更,都会触发
- 可通过重复按下 f 来切换全局遍历
-
第三步,再按下 o (只执行源码产生过变动的文件的测试用例)
- 等价于 jest –watch
- 只监听 git 中,未提交到暂存区的文件,一旦提交了 stash,则不再触发
- 即便该文件中存在失败的测试用例,也会被疏忽
- 按下 a 来跑全副文件的测试用例,即 a 与 o 的切换
- 底层是通读取 .git 文件夹的内容进行文件辨别,故依赖 git 的存在
-
按下 w 能够显示菜单,查看 watch 的选项
个别状况下,汇合 o 与 f 应用,先 o(疏忽没变动的文件,当咱们改变该文件时,将会被监听。再重复按下 f,只监听谬误的用例)
jest 报告阐明
- 鼠标悬浮对应图表,即可显示对应提醒
- 「5x」示意在测试中这条语句执行了 5 次
- 「I」是测试用例 if 条件未进入,即没有 if 为真的测试用例
-
「E」是测试用例没有测试 if 条件为 false 时的状况
- 即测试用例中 if 条件始终都是 true,得写一个 if 条件为 false 的测试用例,即不走进 if 条件外面的代码,这个 E 才会隐没
模仿函数,不是模仿数据的函数
- 只是模仿函数(Function、jest.fn()),并不是像 mockjs 一样,生成模仿数据的函数
-
作用:
- 检测该函数被执行过多少次
- 检测该函数被执行时的 this 指向
- 检测执行时的入参
- 检测执行后的返回值
-
笼罩模仿第三方函数
- 笼罩 axios 函数,防止真正发动接口,定制特定的返回值
jest.mock('axios'); axios.get.mockResolvedValue(resp);
- 外面没有魔法,也没有私下适配,只是单纯的函数重载。相当于
axios.get = ()=> resp
重写了该办法
- 笼罩 axios 函数,防止真正发动接口,定制特定的返回值
-
终极办法,笼罩整个第三方库
- 编写替身文件,在应用 import 导入时,导入的是替身文件
- 也能够通过 jest.requireActual(‘../foo-bar-baz’) 来强制设置导入的是实在的文件,不应用替身文件
计时器模仿
- 复写 setTimeout 计时器,能够跳过指定时长,缩短单元测试运行时长
测试快照
- 快照,即数据正本,即检测「以后数据」是否与「旧有数据正本」雷同,相似于
JSON.stringify()
,进行数据的序列化记录 -
利用场景
- 限度配置文件的变更
- 检测 dom 构造的比拟,某函数的变更,是否影响 dom 构造
- 总体而言,用在大数据的比拟操作,防止将数据写死在单元测试文件中
其余疑难杂症
-
别名与等价的办法
- it 是 test 的别名,两者等价
- toBeTruthy !== toBe(true)、toBeFalsy !== toBe(false),toBe(true) 更严格,toBeTruthy 是强转为 boolean 后,是否为真
- skip 跳过某测试用例,比正文更优雅
describe.skip('测试自定义指令',xxx)
`test.skip(‘ 测试自定义指令 ’,xxx)`
-
jest toBe,外部应用 Object.is 进行比拟
- 与 === 的区别是,除了 NaN,+0 和 -0 之外,其行为与三等号于运算符雷同
- 解决小数点浮点数计算误差问题
toBeCloseTo
-
异步测试,通过 .resolves / .rejects 强制校验 promise 走特定分支
test('the data is peanut butter', () => {return expect(fetchData()).resolves.toBe('peanut butter'); });
-
解决默认参数为 new Date 的笼罩问题
test('以后月,测试参数 new Date 默认值', () => { // 覆写 new Date 的值,模仿为 2022/01/23 17:13:03,解决默认参数为 new Date 时,无奈笼罩的问题 const mockDate = new Date(1642938133000) const spyDate = jest .spyOn(global, 'Date') // 即监听 global.Date 变量 .mockImplementationOnce(() => {spyDate.mockRestore() // 须要在第一次执行后,马上打消该 mock,防止后续影响后续 new Date return mockDate }) let [starTime, endTime] = getMonthRange() expect(starTime).toBe(1640966400000) // 2022/01/01 00:00:00 expect(endTime).toBe(1643644799000) // 2022/01/31 23:59:59 })
-
等价于应用原生语法写
const OriginDate = global.Date Date = jest.fn(() => { Date = OriginDate return new OriginDate(1642938133000) })
-
应用最新语法
beforeAll(() => {jest.useFakeTimers('modern') jest.setSystemTime(new Date(1466424490000)) // 因为 Vue Test Utils 中应用的 jest 是 24.9 的版本,没有该函数 }) afterEach(() => {jest.restoreAllMocks() })
-
-
匹配测试,及应用多个批次的数据,进行跑同一个测试用例
describe.each([[1, 1, 2], // 每一行是代表运行一次测试用例 [1, 2, 3], // 每一行中的参数,是运行该次测试用例是用到的数据,前两个是参数,第三个是测试期望值 [2, 1, 3], ])('.add(%i, %i)', // 设置 describe 的题目,%i 是 print 的变量参数 (a, b, expected) => {test(`returns ${expected}`, () => {expect(a + b).toBe(expected); }); });
gitlab-ci 单元测试相干配置
- 在发动 merge 合并申请时,触发 ci 执行单元测试
-
当单元测试失败,执行 node 文件,发送飞书信息,飞书信息中,包含该次 merge 申请的链接,能够点击该链接,疾速定位到单元测试 job,查看问题
stages: - merge-unit-test - merge-unit-test-fail-callback - other-test # merge 申请时执行的 job step-merge: stage: merge-unit-test # 应用的 gitlab runner tags: [front-end] # 仅在提出代码合并申请时执行 only: [merge_requests] # 排除特定分支的代码合并申请,即在特定分支的代码合并申请时,不执行该 job except: variables: - $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "qa" # 运行的命令 script: - npm install --registry=https://registry.npm.taobao.org # 装置依赖 # 2>&1 规范谬误定向到规范输入 # Linux tee 命令用于读取规范输出的数据,并将其内容输入成文件。- npm run test 2>&1 | tee ci-merge-unit-test.log # 执行单元测试,并将在控制台输入的信息,保留在 ci-merge-unit-test.log 文件中,以便后续剖析 - echo 'merge-unit-test-finish' # 定义往下一个 job 须要传递的材料 artifacts: when: on_failure # 默认状况下,只会在 success 保留,能够通过这个标识符进行配置 paths: # 定义须要传递的文件 - ci-merge-unit-test.log # merge 检测失败时执行的 node 命令 step-merge-unit-test-fail-callback: stage: merge-unit-test-fail-callback # 当上一个 job 执行失败时,才会触发 when: on_failure tags: [front-end] only: [merge_requests] except: variables: - $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "qa" script: - node ci-merge-unit-test-fail-callback.js $CI_PROJECT_NAME $CI_JOB_ID # 执行 node 脚本,进行飞书告诉,并携带对应链接,进行疾速定位
-
ci-merge-unit-test-fail-callback.js.js
const fs = require('fs') const path = require('path') const https = require('https') const projectName = process.argv[2] // 我的项目名 const jobsId = process.argv[3] // 执行的 ci 工作 id const logsMainMsg = fs.readFileSync(path.join(__dirname, 'ci-merge-unit-test.log')) .toString() .split('\n') .filter(line => line[line.length - 1] !== '|' && line.indexOf('PASS') !== 0) // 过滤不关注的信息 .join('\n') const data = JSON.stringify({ msg_type: 'post', content: { post: { zh_cn: { content: [ [ { tag: 'a', text: 'gitlab merge 单元测试', href: `https://xxx/fe-team/${projectName}/-/jobs/${Number(jobsId) - 1}` }, { tag: 'text', text: ` 运行失败 \r\n${logsMainMsg}` } ] ] } } } }) const req = https.request({ hostname: 'open.feishu.cn', port: 443, path: '/open-apis/bot/v2/hook/xxx', method: 'POST', headers: {'Content-Type': 'application/json'} }, res => {console.log(`statusCode: ${res.statusCode}`) res.on('data', d => process.stdout.write(d)) }) req.on('error', error => console.error(error)) req.write(data) req.end()
感激
- 近期文章产出少,事件太多,也懒了
- 感激网友的挂念和督促,被人牵挂的感觉真好