前言
Jest是Facebook推出的一个单元测试框架,支持断言、异步操作、单元测试覆盖率的统计。
本文将简单的介绍使用Jest使用TDD的方式开发一个vue3的组件。
1 安装Jest
1.1 在项目目录中安装Jest
执行脚本
yarn add jest@27.2.5 -D
1.2 安装Jest的Vue的依赖
执行脚本
yarn add @vue/compiler-sfc@3.2.47 -D
yarn add @vue/test-utils@2.3.0 -D
yarn add vue3-jest@27.0.0-alpha.1 -D
依赖说明
| 依赖 | 用途 |
|---|---|
| @vue/compiler-sfc | 处理一些需要使用.vue文件的模块系统,将vue的单文件组件(SFCs)转化为JavaScript |
| @vue/test-utils | Vue3的组件测试工具 |
| vue3-jest | Jest针对vue3单文件组件的转换器 |
1.3 安装Jest的TypeScript依赖
执行脚本
yarn add babel-jest@27.5.1 -D
yarn add @babel/core@7.21.0 -D
yarn add @babel/preset-env@7.20.2 -D
yarn add @babel/preset-typescript@7.21.0 -D
yarn add @types/jest@29.4.0 -D
yarn add typescript@4.9.5 -D
依赖说明
在Jest的单元测试中使用TypeScript时,需要依赖Babel。
| 依赖 | 用途 |
|---|---|
| babel-jest | Jest中使用Babel编译JavaScript时需要安装babel-jest、@babel/core、@babel/preset-env |
| @babel/core | - |
| @babel/preset-env | - |
| @babel/preset-typescript | 使用TS时,需要针对TS进行预处理,编译为JS,再使用Babel编译 |
| @types/jest | Jest的ts声明文件 |
| typescript | TypeScript |
1.4 添加测试脚本
jest默认使用的是Node环境进行单元测试,由于vue用于开发WebApp,需要使用浏览器的一些API因此,在启动测试时,需要将env切换为jsdom,用以模拟浏览器的运行环境。
// package.json
{
"scripts": {
"test": "jest --watch --env=jsdom"
}
}
1.5 正确的配置babel、jest和ts
babel配置
// babel.config.js
module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
ts配置
// tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["@types/jest"]
},
"include": [] // ts file path
}
当在Jest中使用TS对.vue文件进行测试时,需要对.vue文件补充他的模块声明
// shim.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}
jest配置
jest的配置文件可以通过jest命令行工具进行生成
jest --init
// or
npx jext --init
// jest.config.js
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
module.exports = {
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// The test environment that will be used for testing
testEnvironment: 'jsdom',
// The glob patterns Jest uses to detect test files
testMatch: ['**/?(*.)+(spec).[jt]s?(x)'], // ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
// A map from regular expressions to paths to transformers
transform: {
// .vue文件用 vue-jest 处理
'^.+\\.vue$': 'vue3-jest',
// .js或者.jsx用 babel-jest处理
'^.+\\.jsx?$': 'babel-jest',
//.ts文件用ts-jest处理
'^.+\\.ts$': 'ts-jest'
}
}
1.6 运行单元测试
开启单元测试
yarn test
运行效果
2 TDD开发一个Vue3组件
2.1 梳理需求
| 功能 | 说明 |
|---|---|
| 列表展示 | |
| 1、列表具有序号、名字、编码、状态、操作三列; | |
| 2、操作列有编辑、查看、删除三个按钮,点击出现对应的对话框 | |
| 列表筛选 | |
| 1、有搜索输入框,可输入字符串,有搜索和重置两个按钮 | |
| 2、点击搜索按钮,在输入非空时,可以进行表格的筛选展示、点击重置按钮,可以清空输入框中已经输入的内容 |
2.2 需求拆解
上面的业务组件里可以分成两块,一块是表格组件、一块是输入搜索组件,因此我们需要两块的单元测试,分别测试这两个组件的单一功能。
2.3 测试用例
搜索组件需要使用v-model的方式进行数据的双向绑定 先新建一个SearchInput的组件
<template>
<div class='search-input-container'>
<el-input class="input"></el-input>
<el-button class="search" type="primary" size="default" @click=""></el-button>
<el-button class="reset" type="primary" size="default" @click=""></el-button>
</div>
</template>
编写组件的单元测试
import { mount } from '@vue/test-utils'
import { ElButton, ElInput } from 'element-plus'
import SearchInput from '../components/SearchInput.vue'
// 模拟搜索组件的挂载事件
const searchInputComp = mount(SearchInput, {
// 模拟调用时 v-model="Search input test"
props: {
modelValue: 'Search input test',
'onUpdate:modelValue': (e:string) => searchInputComp.setProps({ modelValue: e })
},
// element组件在项目启动时全局挂载,jest无法处理这一点,因此需要在这里显示的生命注册的组件
components: {
'el-input': ElInput,
'el-button': ElButton
},
})
/**
* 获取输入组件,find和findComponent的注意事项可以参考官方文档
* https://v1.test-utils.vuejs.org/api/wrapper/#findcomponent
*/
const inputEle = searchInputComp.findComponent<typeof ElInput>('.input')
const searchBtn = searchInputComp.findComponent<typeof ElButton>('.search')
const resetBtn = searchInputComp.findComponent<typeof ElButton>('.reset')
test('测试搜索组件组件挂载',()=>{
/**
* 测试各个组件是否正常挂载
*/
expect(inputEle.exists()).toBe(true)
expect(searchBtn.exists()).toBe(true)
expect(resetBtn.exists()).toBe(true)
})
单元测试执行结果
继续补充测试用例,测试组件的v-model功能
test('测试搜索组件的v-model功能',()=>{
/**
* 使用findComponent和getComponent时需要注意事项
* https://test-utils.vuejs.org/guide/advanced/component-instance.html#usage-with-getcomponent-and-findcomponent
*/
const inputEle = searchInputComp.findComponent<typeof ElInput>('.input')
expect(inputEle.props('modelValue')).toBe('Search input test')
inputEle.setValue('new value')
expect(searchInputComp.props('modelValue')).toBe('new value')
})
测试结果
在这个时候,我们的组件没有完成v-model的功能,此时的单元测试是失败的,接下来,我们去完善SearchInput组件的v-model功能
<template>
<div class='search-input-container'>
<el-input v-model="inputVal" class="input" @input="handleChange()"></el-input>
<el-button class="search" type="primary" size="default" @click=""></el-button>
<el-button class="reset" type="primary" size="default" @click=""></el-button>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const props = defineProps<{
modelValue: string
}>()
const emits = defineEmits(['update:modelValue'])
const inputVal = ref('')
watch(()=>props.modelValue,()=>{
inputVal.value = props.modelValue
},{
immediate:true
})
const handleChange = ()=>{
emits('update:modelValue', inputVal.value)
}
</script>
此时单元测试就能够通过了
接下来需要编写重置搜索功能的单元测试
test('测试搜索重置功能',async ()=>{
await inputEle.find('input').setValue('Test search input string')
// trigger返回一个Promise,因此测试阶段依赖于该trigger行为的后续测试需要等待vue的更新完毕
await resetBtn.trigger('click')
expect(searchInputComp.props('modelValue')).toBe('')
})
此时单元测试的结果如下
继续完善重置搜索的功能
<template>
<div class="search-input-container">
<el-input v-model="inputVal" class="input" @input="handleChange()"></el-input>
<el-button class="search" type="primary" size="default" @click=""></el-button>
<el-button class="reset" type="primary" size="default" @click="resetSearch()"></el-button>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const props = defineProps<{
modelValue: string
}>()
const emits = defineEmits(['update:modelValue'])
const inputVal = ref('')
watch(
() => props.modelValue,
() => {
inputVal.value = props.modelValue
},
{
immediate: true
}
)
const handleChange = () => {
emits('update:modelValue', inputVal.value)
}
const resetSearch = () => {
inputVal.value = ''
emits('update:modelValue', inputVal.value)
}
</script>
单元测试通过
接下来就是组件交互逻辑的最后一步,我们需要在组件内点击搜索按钮时,组件的调用处可以接收到search事件
// 可以查看官方示例:https://test-utils.vuejs.org/guide/essentials/event-handling.html#the-counter-component
test('测试搜索按钮点击搜索功能',async ()=>{
await inputEle.find('input').setValue('search keyword')
await searchBtn.trigger('click')
const searchEvent = searchInputComp.emitted('search')
expect(searchEvent).toBeTruthy()
/* 测试触发的次数,多次触发视为异常,多次触发时数据如下
* [ [第一次触发的数据载体], [第二次触发的数据载体]... ]
* 以下测试用例表示只触发一次
*/
expect(searchEvent?.length).toBe(1)
// 测试event payload, 第一个次触发的第一个参数
expect(searchEvent![0][0]).toBe('search keyword')
})
在组件中补全对应的时间触发逻辑
<template>
<div class="search-input-container">
<el-input v-model="inputVal" class="input" @input="handleChange()"></el-input>
<el-button class="search" type="primary" size="default" @click="handleSearch()"></el-button>
<el-button class="reset" type="primary" size="default" @click="resetSearch()"></el-button>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const props = defineProps<{
modelValue: string
}>()
const emits = defineEmits(['update:modelValue', 'search'])
const inputVal = ref('')
watch(
() => props.modelValue,
() => {
inputVal.value = props.modelValue
},
{
immediate: true
}
)
const handleChange = () => {
emits('update:modelValue', inputVal.value)
}
const resetSearch = () => {
inputVal.value = ''
emits('update:modelValue', inputVal.value)
}
const handleSearch = () => {
emits('search', inputVal.value)
}
</script>
至此,关于搜索组件的input功能已经完全开发完成了,接下来我们需要进行人机交互进行组件的自测。
2.4 交互验证
// 调用组件
<template>
<SearchInput v-model="input" @search="handleSearch"></SearchInput>
<p>{{ input }}</p>
</template>
<script setup lang="ts">
import { ElMessageBox } from 'element-plus';
import { ref } from 'vue';
import SearchInput from './components/SearchInput.vue';
const input = ref('')
const handleSearch = (searchVal:string)=>{
ElMessageBox.alert(searchVal)
}
</script>
实际效果
为了使组件的交互更加的严谨,可以继续的添加单元测试,并完善组件的逻辑,至此,就已经使用TDD的方式完成了一个vue组件的开发,接下来就可以针对组件进行UI层面的调整和优化了。 在后续组件的重构时,当测试用例可以通过,就表示组件的优化和变更,没有影响到正常的功能,可以作为一个重构和优化的重要指标。
参考资料
示例代码地址:github.com/Arfly/vue-u…
单元测试框架对比:npmtrends.com/ava-vs-jest…
Jest文档:jestjs.io/zh-Hans/doc…
Jest配置:jestjs.io/zh-Hans/doc…
vue-test-utils示例文档:test-utils.vuejs.org/installatio…