Appearance
在vue2中进行单元测试
测试准则
测试规范
文件结构
测试文件的命名规范为:*.test.js
,*
为需要测试的组件名称或文件名称,文件放在模块的__tests__目录下,比如短信模块的测试文件目录结构如下:
markdown
.
├─ message
│ ├─ __tests__
| └─ New.test.js
命名
在需要测试的 dom 或组件上添加data-test
属性,不要使用className来测试,因为它们可能会被删除或变动。正确的测试如下:
html
<Foo data-test="foo" class="foo" />
<button data-test="saveButton" />
相关文档
测试功能
测试起步
- mount/shallowMount
渲染测试组件的方法,具体的区别见此处。 mount和shallowMount在注册时可以传入一些选项帮助我们完善测试环境所需要的上下文:首先让我们了解下最常用到的两个配置stubs和mocks。
- stubs
存根,也即测试组件中所包含的子组件,一般不需要测试它们,但有时需要借助它们去抛出事件来模拟操作场景,所以按需注册即可。详细的使用文档见此处。
- mocks
用来模拟vue组件的实例方法:比如$errorHandler、$router、$message等。详细的使用文档见此处。
shallowMount or mount
可以遵循以下准则:能使用shallowMount的情况绝不使用mount。 那么mount一般在什么时候使用?以下是可能使用mount的场景:
- 测试stubs组件内的内容:由于shallowMount无法真实的渲染stubs组件,所以对于组件内的内容测试需要用过mount来渲染测试,当然这并不是说推荐我们去测试子组件的内容,而是针对父组件的测试内容涵盖了子组件渲染的部分我们需要借助mount来渲染正确的dom结构,比方说a-table/c-table下的列表操作的测试。
js
it('二维码不存在的提示', async () => {
const record = {
"id": "1139087491756986368",
"remark": "11",
"url": "https://alibaba.beta.pageseagle.com/1140565968175497216/index?source_id=1010781213482733568&source=来源名称&utm_campaign=全员营销",
"sourceId": "1010781213482733568",
"source": "来源名称",
"utmSource": "",
"utmMedium": "",
"utmCampaign": "全员营销",
"utmTerm": "",
"utmContent": "",
qrCodeId: '123',
"assetId": "1139087491706654720",
"createdAt": "2023-08-15T06:47:01.2575758+00:00"
}
const option = wrapperOption()
GetScene.mockResolvedValue([record])
// 设置测试中用到的stubs
Object.assign(option.stubs, {
'c-table': CTable,
'a-table': Table
})
option.propsData = {
id: '123',
assetId: '456'
}
// 通过mount来渲染table
const wrapper = mount(Sources, option)
// 等待初始化的数据加载完成
await sleep(0)
// table中某行的操作按钮
await wrapper.find('[data-testid=qrcodeIcon]').trigger('click')
// 模拟组件触发事件
wrapper.findComponent(SomeVueComponent).vm.$emit('someEvent',...someParams)
expect(wrapper.html()).toContain('二维码被删除')
})
定义好的工具类
- test/mock.js
在src/utils/test/mock.js
中封装了wrapperMocks
、wrapperOption
,用于创建测试组件的上下文,具体的含义可以参考代码文档。
- test/setup/request.js 异步请求的通用配置文件,根据路由规则对请求进行mock处理,详细的使用文档见此处
根据不同的数据集进行测试
- setData / setProps
javascript
it('displays error message when content is too long', async () => {
wrapper = shallowMount(New, {
...option,
{
// 设置props
propsData: {
id: 'test'
}
}
})
// 设置data(更推荐使用交互或者事件的方式代替,比如tirgger('click')、$emit('input'))
await wrapper.setData({ content: 'a'.repeat(500) })
await wrapper.find('button').trigger('click')
expect(mocks.$errorHandler).toBeCalledWith(Error('内容超出最大长度限制'))
})
vue实例方法
虽然正确的做法是只专注dom输出和公共接口,但是出于测试的便利和可行性,我们可以在测试中测试是否调用了vue实例的方法来模拟某些场景(eg.路由是否发生了改变可以通过是否调用$router来判断,是否展示了错误异常可以通过是否调用了$errorHandler来判断)。
在src/utils/test/mock.js
中封装了wrapperOption
,其中包含了组件实例的方法,比如我们要测试$errorHandler
有没有被调用:
javascript
import { wrapperOption } from '@/utils/test/mock'
describe('New.vue 创建新短信', () => {
let wrapper
const option = wrapperOption()
beforeEach(() => {
wrapper = shallowMount(New, option);
})
it('displays error message when content is empty', async () => {
await wrapper.find('button').trigger('click')
expect(mocks.$errorHandler).toBeCalledWith(Error('短信内容不能为空'))
})
})
以下是测试$router
的例子
javascript
it('navigates to MessageTemplate page after saving', async () => {
const d = {
"content": "test",
"signatureId": "test",
"title": "test",
"type": 1,
}
await wrapper.setData(d)
await wrapper.find('button').trigger('click')
expect(SaveTemplate).toBeCalledWith(d)
await sleep(0)
expect(mocks.$router.push).toBeCalled()
})
测试第三方组件,比如ant-deisign-vue
js
// 使用常用组件
import {wrapperStubs} from '@/utils/test/mock'
shallowMount(New, { stubs: wrapperStubs() } )
// wrapperStubs中包含了常用的测试组件,如果不满足需求,可以自定义,比如:
import {Modal} from 'ant-design-vue'
shallowMount(New, { stubs: {'a-modal': Modal} } )
处理异步请求
本项目对axios进行了统一的封装,所有请求都会返回一个空的promise.resolve对象。
- 如果只是为了保证程序能正常运行,防止undefined之类的错误,可以在setup文件中定义:
WARNING
需要注意的是这边只是默认的返回结果以保证程序不出错,一般为空数据,不要定义具体的返回结果
javascript
// __test__/setup/request.js
const getImplementation = (route, params) => {
if ([/campaignType$/, /configcenter\/list$/].some(reg => reg.test(route))) {
return Promise.resolve([])
}
return Promise.resolve({ data: [] })
}
- 如果测试依赖于请求的具体内容,则需要对请求进行mock处理,以下介绍了两种mock方式:
javascript
import { SaveTemplate } from '@/api/message/template'
// 短信副本
const copyTemplate = {
type: 1,
title: 'test',
content: 'hhh',
signatureId: 'test'
}
// mock api
jest.mock('@/api/message/template', () => {
const originalModule = jest.requireActual('@/api/message/template')
return {
__esModule: true,
...originalModule,
// 在整个测试文件使用一个测试用例
GetTemplateById: jest.fn().mockResolvedValue(copyTemplate),
SaveTemplate: jest.fn()
};
})
describe('New.vue 创建短信副本', () => {
let wrapper
const option = wrapperOption()
beforeEach(() => {
wrapper = shallowMount(New, {
...option,
propsData: {
id: 'test'
}
});
})
it('创建副本后直接保存为新短信', async () => {
await wrapper.find('button').trigger('click')
expect(SaveTemplate).toBeCalledWith({ ...copyTemplate, title: '副本-' + copyTemplate.title })
})
// 在这条测试中使用特定的测试用例
it('创建副本后直接保存为新短信', async () => {
GetTemplateById.mockResolvedValue(copyTemplate)
// 实例需要在mock后再注册
const wrapper = shallowMount(New, {
...wrapperOption(),
propsData: {
id: 'test'
}
});
await wrapper.find('button').trigger('click')
expect(SaveTemplate).toBeCalledWith({ ...copyTemplate, title: '副本-' + copyTemplate.title })
})
})
- 等待异步请求执行完成
由于异步请求是一个微任务,所以我们在下一个宏任务中执行断言,这时候需要等待异步请求执行完成,这里提供了一个sleep
方法,可以在测试中使用:
js
import { sleep } from '@/utils/common'
sleep(0)
运行完整的测试
bash
yarn test:unit:dev
测试覆盖率
查看测试覆盖率,可以按以下操作执行:
- 把测试的文件目录添加到
jest.config.json
中的jest.collectCoverageFrom
中
json
"collectCoverageFrom": [
// 统计message和mail文件夹下的测试覆盖率
"src/**/{message,mail}/**/*.{js,vue}",
"!src/utils/test/*.js",
]
- 运行命令
bash
yarn test:coverage
QAs:
如何将文件排除在测试外
skip.
的开头js或者vue文件不会包含到测试和测试覆盖率中
如何对单个测试文件进行调试
- 手动调用
bash
yarn test:unit:dev src/components/campaign/market/__tests__/WebinarLanding.test.js
- 配置VSCode并执行task
- F1唤起命令行,输入
Configure Task
- 输入配置
json
"tasks": [
{
"label": "Run Test",
"type": "shell",
"command": "yarn",
"args": [
"run",
"test:unit:dev",
"${file}"
],
"problemMatcher": [],
"group": {
"kind": "test",
"isDefault": true
}
},
]
- F1唤起命令行,输入
Run Task
执行命令
- 配置launch.json以在调试中运行task
json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "单元测试",
"skipFiles": [
"<node_internals>/**"
],
"preLaunchTask": "Run Test",
}
]
}