赞
踩
单元测试的作用毋庸置疑能带来很多好处,但是如何去写好单元测试.
TDD 的思想是根据需求先写测试用例,依照测试用例再去写功能代码。当增加或者修改某一项需求的时候,需要先修改测试用例,再依照测试用例去修改代码逻辑。
基本步骤:
与 TDD 相反,BDD 是根据需求先进行开发,等到该功能开发完毕后,再开始编写测试代码进行测试。
基本步骤:
了解到vue-cli构建的项目,在初始化时会询问是否使用单元测试,只需按步骤选择jest即可,会自动安装Vue Test Utils,它是 Vue.js 官方的单元测试实用工具库,为 jest
和 Vue
提供了一个桥梁,暴露出一些接口,让我们更加方便的通过 Jest
为 Vue
应用编写单元测试。
vue create vue-jest
cd vue-jest
code .
在生成的项目根目录下,会有一个 jest.config.json
文件,cli
自动生成所用的预设是@vue/cli-plugin-unit-jest
。我们可以在这里对 jest
进行个性化的配置。以下是一个配置文档的例子。配置文档具体参数说明
在项目根目录下找到jest.config.json
,修改如下,(有些配置已注释,按需打开)
module.exports = { preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel", // 开启测试报告 collectCoverage: true, // // 统计哪里的文件 collectCoverageFrom: ["**/src/components/**","**/src/views/common/**", "!**/node_modules/**"], // // 告诉jest针对不同类型的文件如何转义 transform: { "^.+\\.vue$": "vue-jest", // '^.+\\.(vue)$': '<rootDir>/node_modules/vue-jest', '^.+\\.js$': '<rootDir>/node_modules/babel-jest', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', '^.+\\.jsx?$': 'babel-jest', '^.+\\.ts?$': 'ts-jest' }, // 告诉jest需要解析的文件 moduleFileExtensions: [ 'js', 'ts', 'jsx', 'json', 'vue' ], // // 告诉jest去哪里找模块资源,同webpack中的modules moduleDirectories: [ 'src', 'node_modules' ], // // 告诉jest在编辑的过程中可以忽略哪些文件,默认为node_modules下的所有文件 transformIgnorePatterns: [ '<rootDir>/node_modules/' + '(?!(vue-awesome|vant|resize-detector|froala-editor|echarts|html2canvas|jspdf))' ], // // 别名,同webpack中的alias // moduleNameMapper: { // '^src(.*)$': '<rootDir>/src/$1', // '^@/(.*)$': '<rootDir>/src/$1', // '^block(.*)$': '<rootDir>/src/components/block/$1', // '^toolkit(.*)$': '<rootDir>/src/components/toolkit/$1' // }, // snapshotSerializers: [ // 'jest-serializer-vue' // ], // // 告诉jest去哪里找我们编写的测试文件 testMatch: [ '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' // '**/tests/unit/**/Test.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' ], // 在执行测试用例之前需要先执行的文件 // setupFiles: ['jest-canvas-mock'] };
项目添加了单元测试,里面默认会有一条写好的单元测试,执行npm run test:unit
命令,终端就可以看到一条单元测试通过的记录。
%Stmts(statement coverage): 语句覆盖率,是否每个语句都执行了?
%Branch(branch coverage): 分支覆盖率,是否每个if代码块都执行了?
%Funcs(branch coverage): 函数覆盖率,是否每个函数都调用了?
%Lines(line coverage): 行覆盖率,是否每一行都执行了?
在编写测试用例之前,我们先来简单了解一下 Vue Test Utils 的 几个 API 以及 Wrapper。
mount: 创建一个包含被挂载和渲染的 Vue 组件的 Wrapper
shallowMount: 与mount作用相同,但是不渲染子组件
render: 将一个对象渲染成为一个字符串并返回一个 cheerio 包裹器
createLocalVue: 返回一个 Vue 的类,供你添加组件、混入和安装插件而不会污染全局的 Vue 类
wrapper: 一个 wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法。
wrapper.vm: 可以访问一个实例所有的方法和属性
wrapper.get() : 返回DOM节点或者Vue组件
wrapper.setData() : 同Vue.set()
wrapper.trigger(): 异步触发事件
wrapper.find(): 返回DOM节点或者Vue组件
具体的 API 和 Warpper方法和实例 可以参考这里。
TodoApp
从一个带有单个 todo的简单组件开始:在src下新建common文件夹,新建TodoApp.vue文件,编写以下内容
<template> <div v-for="(todo,key) in todos" :key="key" data-test="todo" :class="[todo.completed ? 'completed' : '']"> {{ todo.text }} <input type="checkbox" v-model="todo.completed" data-test="todo-checkbox" /> </div> <form data-test="form" @submit.prevent="createTodo"> <input data-test="new-todo" v-model="state.newTodo" /> </form> </template> <script lang="ts" setup> import { reactive } from 'vue'; let state = reactive({ newTodo: '' }) let todos = reactive([ { text: 'Learn Vue.js 3', completed: false } ]) const createTodo = () => { todos.push({ text: state.newTodo, completed: false }) } </script>
在根目录下tests\unit下新建todo.spec.ts文件,首页测试页面默认第一个todo 是否已呈现
import { mount } from '@vue/test-utils'
import TodoApp from '@/views/common/TodoApp.vue'
test('renders a todo', () => {
// 1. 通过mount创建一个包含被挂载和渲染的 Vue 组件的 Wrapper
const wrapper = mount(TodoApp)
// 2.通过属性选择器获取该标签
const todo = wrapper.get('[data-test="todo"]')
// 3.通过text方法获取文本内容,最后断言是否等于Learn Vue.js 3
expect(todo.text()).toBe('Learn Vue.js 3')
})
执行npm run test:unit
可以看出我们的测试用例和项目自带的都通过了测试。
一个模块单独写test比较分散,可以利用describe 函数
通过test函数可以创建一个个的测试用例,那当我们的测试用例越来越多的时候,就需要对测试用例进行分类整理,那这就是describe函数的作用。
describe('todoAll', () => { it('renders a todo', () => { const wrapper = mount(TodoApp) const todo = wrapper.get('[data-test="todo"]') expect(todo.text()).toBe('Learn Vue.js 3') }) it('creates a todo', async () => { const wrapper = mount(TodoApp) // 1. 通过setValue设置input标签的value是New todo await wrapper.get('[data-test="new-todo"]').setValue('New todo') // 2. 通过trigger触发form表单提交事件 await wrapper.get('[data-test="form"]').trigger('submit') // 3. 判断长度是否等2,等于2代表创建成功了 expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2) }) })
- it和test的效果是一样的,两者语义差不多,只是it语义更好理解
- Jest 以同步方式执行测试,一旦调用最终函数就结束测试。然而,Vue 会异步更新 DOM。我们需要标记 test async,并调用await任何可能导致 DOM 更改的方法
执行npm run test:unit
,可以看到所有的单测都通过了.
可以通过判断div上是否有completed类,代表是否勾选
describe('todoAll', () => { it('renders a todo', () => { const wrapper = mount(TodoApp) const todo = wrapper.get('[data-test="todo"]') expect(todo.text()).toBe('Learn Vue.js 3') }) it('creates a todo', async () => { const wrapper = mount(TodoApp) // 1. 通过setValue设置input标签的value是New todo await wrapper.get('[data-test="new-todo"]').setValue('New todo') // 2. 通过trigger触发form表单提交事件 await wrapper.get('[data-test="form"]').trigger('submit') // 3. 判断长度是否等2,等于2代表创建成功了 expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2) }) it('completes a todo', async () => { const wrapper = mount(TodoApp) await wrapper.get('[data-test="todo-checkbox"]').setValue(true) // 通过classes方法获取标签上所有的类名,然后toContain判断数组里是否包含completed类名 expect(wrapper.get('[data-test="todo"]').classes()).toContain('completed') }) })
执行npm run test:unit
,可以看到到复选框选中时,父元素div是有completed,所以通过单测
像上面的单测,里面的内容都是写死的,无法根据实际情况动态修改参数,为了方便测试,把参数抽离出来。调整为外部文件动态传参。以创建todo为例,理想状态是传入一个list参数,和一个期望值,根据list数组的长度去创建n个todo,判断创建todo的长度是否和期望值相等。
在src/views/common下面新建configJS
文件夹,然后在新建configData.ts
文件.
configData.ts
let createsTodo = {
list: ['a', 'b', 'c'], // 想要动态创建多少个
expected: 5 // 期望长度
}
export default {
createsTodo
}
todo.spec.ts
import { mount } from '@vue/test-utils' import TodoApp from '@/views/common/TodoApp.vue' import configData from "@/views/common/configJS/configData"; // 引入测试数据 describe('todoAll', () => { it('creates a todo', async () => { const wrapper = mount(TodoApp) configData.createsTodo.list.forEach(async(el: any) => { await wrapper.get('[data-test="new-todo"]').setValue(el) await wrapper.get('[data-test="form"]').trigger('submit') }); expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(configData.createsTodo.expected) }) })
执行npm run test:unit
会发现单测没通过,拿到的不是页面更新后的数据,还是初始数据1,初步判断是async和await失效了,
所以改用nextTick,代码改成如下:
todo.spec.ts
import { mount } from '@vue/test-utils' import TodoApp from '@/views/common/TodoApp.vue' import { nextTick } from 'vue' import configData from "@/views/common/configJS/configData"; // 引入测试数据 describe('todoAll', () => { it('creates a todo', async () => { const wrapper = mount(TodoApp) //发现在forEach使用async 和await失效,所以改用nextTick方法 configData.createsTodo.list.forEach((el: any) => { wrapper.get('[data-test="new-todo"]').setValue(el) wrapper.get('[data-test="form"]').trigger('submit') }); await nextTick() expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(configData.createsTodo.expected) }) })
再次执行npm run test:unit
会发现单测通过了,可list长度是3,expected是4为什么会通过呢,原因是组件原本就有一条数据.
现在我们修改一下期望值,把他改成5,再次执行npm run test:unit
let createsTodo = {
list: ['a', 'b', 'c'],
expected: 5
}
发现单测没有通过,但是这是正常的。原因是期望值是错误的.
在数据文件里面写多种场景的数据,怎么才能都实现呢,想到的办法是每个单测场景都传入一个数组,根据数组的长度去循环创建n个单测(test or it).现在以是否完成代办单测为例。
configData.ts
let completesTodo = [
{
bool: false, // 是否勾选
expected: false // 标签是否有勾选的类名
},
{
bool: true,
expected: true
},
]
export default {
completesTodo,
}
todo.spec.ts
import { mount } from '@vue/test-utils' import TodoApp from '@/views/common/TodoApp.vue' import configData from "@/views/common/configJS/configData"; describe('todoAll', () => { configData.completesTodo.forEach(item => { it('completes a todo', async() => { const wrapper = mount(TodoApp) await wrapper.get('[data-test="todo-checkbox"]').setValue(item.bool) if (item.expected) { expect(wrapper.get('[data-test="todo"]').classes()).toContain('completed') } else { expect(wrapper.get('[data-test="todo"]').classes()).not.toContain('completed') } }) }) })
执行npm run test:unit
可以发现根据数组completesTodo的长度循环创建的两条单测都通过了。现在多加一条数据,勾选但是期望值是false。看看是不是通不过单测。
configData.ts
let completesTodo = [ { bool: false, // 是否勾选 expected: false // 标签是否有勾选的类名 }, { bool: true, expected: true }, { bool: true, expected: false }, ] export default { completesTodo, }
再次执行npm run test:unit
,发现有条单测不通过,符合预期效果.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。