shiuwn / front-end-knowlege Goto Github PK
View Code? Open in Web Editor NEWtake logs of front-end knowlege
take logs of front-end knowlege
本文使用的测试工具是jest
,关于jest
的基本配置与使用,已经有非常优秀的教程
本文不再赘述,All of what you want to know 都在这个教程里。接下来,我们主要介绍利用Vue
提供的测试套件工具,对Vue
组件进行单元测试。
本文基本环境如下:
我们的项目一开始并没有引入测试工具,首先我们需要引入单元测试的工具。我尝试了使用vue add unit-jest
的方式,委托vue cli
自动安装单元测试工具,但是因为我们项目环境的复杂性,vue cli
并不能识别ts
项目,生成的配置项并没有注入到我之前配置的
jest.config.ts
文件中,于是我删掉了所有vue cli
自动安装的包,选择按照Vue Test Utils
提供的手动安装选项,接下来按部就班的安装就行了。
yarn add -D @vue/test-utils@1 vue-jest
这里我之前对工具函数用jest
做了测试,所以我已经对jest
做了配置(其实几乎没怎么加配置项)。现在就需要把Vue
的jest
配置手动写进去。
{
...
"moduleFileExtensions": [
"js",
"mjs",
"cjs",
"jsx",
"ts",
"tsx",
"json",
"node",
// 识别vue文件
"vue"
],
"transform": {
// 解析ts文件
"^.+\\.ts?$": 'ts-jest',
// 解析vue文件
".*\\.(vue)$": "vue-jest",
}
}
Note
这里我们使用的是vue2
,所以一定要使用针对vue2
的测试套件
yarn add -D @vue/test-utils@1
如果不指定版本安装
@vue/test-utils
,测试套件就是针对vue3
的。
接下来,我们可以在测试目录里添加一个组件测试目录components
来测试一下我们的配置有没有问题
<!--test/components/HelloWorld.vue-->
<template>
<div>Hello World</div>
</template>
// test/components/HelloWorld.test.ts
import {shallowMount} from '@vue/test-utils'
import HelloWorld from './HelloWorld.vue'
describe('HelloWorld 组件测试', () => {
it('挂在组件', () => {
const wrapper = shallowMount(HelloWorld)
expect(wrapper.text()).toContain('Hello World')
})
})
测试通过,我们的配置已经生效。接下来,我们可以挂载自己的组件进行单元测试了。
我们的开发环境前面已经提到了
UI组件是我们自研的组件库winbox
类似于element-ui
。我们在组件中使用了@vue/composition-api
,测试中如果我们不注入
这个插件,我们使用setup
函数导出的变量是直接进入不了组件实例的,也就是说假设有一个组件
// example/Component.ts
const Component = {
setup() {
return {
test: 1
}
}
}
// test/example/Component.test.ts
import {shallowMount} from '@vue/test-utils'
import Component from 'example/Component.ts'
const wrapper = shallowMount(Component)
console.log(wrapper.vm.test === undefined) // expected true
针对vue2 + composition-api
的项目我们需要首先在项目中引入@vue/composition-api
因此上面的test文件改为如下
// test/example/Component.test.ts
import Vue from 'vue'
import CompositionApi from '@vue/composition-api'
import {shallowMount} from '@vue/test-utils'
import Component from 'example/Component.ts'
Vue.use(CompositionApi)
const wrapper = shallowMount(Component)
console.log(wrapper.vm.test === 1) // expected true
这就是在使用@vue/composition-api
需要注意的地方。另外,上面的代码属于伪代码,只描述基本逻辑,不保证运行正常。
现有待测试组件如下
<!--src/components/warning/RadioOptionGroup.vue-->
<template>
<section class="option-item">
<label class="label-name margin"><slot></slot></label>
<w-radio-group v-model="bindValue" class="options" @change="handleChange">
<w-radio-button
v-for="item in options"
:key="item.label"
class="btn"
:label="item.value"
>{{ item.label }}</w-radio-button
>
</w-radio-group>
</section>
</template>
<script lang="ts">
import { defineComponent, inject, PropType, ref, toRef } from '@vue/composition-api'
export default defineComponent({
name: 'OptionRadioItem',
props: {
options: Array as PropType<{ label: string; value: string | number }[]>,
binding: String as PropType<string>,
value: [String, Number] as PropType<string|number>
},
model: {
prop: 'value',
event: 'change'
},
setup (props, { emit }) {
const key = props.binding || 'default'
const data = inject('OptionData', { [key]: ref(props.value) })
const bindValue = toRef(data, key)
const handleChange = (newVal: string | number) => {
emit('change', newVal)
}
return {
bindValue,
handleChange
}
}
})
</script>
其中w-radio-group
和w-radio-button
组件类似于el-radio-group
和el-radio-button
组件
这个待测试组件要实现的功能基本上和el-radio-group
也差不多,接下来我们编写第一个组件测试代码
// test/components/warning/RadioOptionGroup.test.ts
import { mount, shallowMount } from '@vue/test-utils'
import type Vue from 'vue'
import '../../../src/useComposition'
import { provide, reactive } from '@vue/composition-api'
import RadioOptionGroup from '@/components/warning/RadioOptionGroup.vue'
import { RadioGroup, RadioButton } from 'winbox-ui'
type VueEmpower = Vue & {
bindValue: number | string
handleChange: (newVal: any) => void
}
describe('单选组件测试', () => {
// props data
const options = [
{
label: 'name1',
value: 1
},
{
label: 'name2',
value: 2
}
]
// 替换子组件
const stubs = {
'w-radio-group': RadioGroup,
'w-radio-button': RadioButton
}
it('作为普通组件展示', () => {
const wrapper = shallowMount(RadioOptionGroup, {
propsData: {
value: 1,
options: options
}
})
expect(wrapper.text()).toContain('name1')
expect(wrapper.text()).toContain('name2')
})
})
执行yarn jest
测试通过。这里我们有必要看一下shallowMount
挂载后的组件长什么样子
<section class="option-item"><label class="label-name margin"></label>
<w-radio-group class="options">
<w-radio-button label="1" class="btn">name1</w-radio-button>
<w-radio-button label="2" class="btn">name2</w-radio-button>
</w-radio-group>
</section>
w-radio-group
和 w-radio-button
组件并没有被渲染,即使将shallowMount
换成mount
结果并没有改变。
当然这并不影响我们的测试结果的正确性,不过这个正确性是建立在第三方组件没有bug这一前提之上的。
为了让子组件渲染出来,我们需要在挂载组件时挂载配置
用到stubs
这个选项。
上面的测试中我们在挂载组件使用了挂载配置项
,也就是shallowMount
传入的第二个参数, (关于配置的选项的使用参考文档)
上面只用到了propsData
这个选项,顾名思义就是给组件传入props
,而我们要使用到的stubs
这个选项,可以当作Vue
组件里的components
。
其实不加stubs
选项jest在运行过程中也会报错。
我们将stubs
加入到挂载项中,运行之后,就得到了完整的结果。
<section class="option-item"><label class="label-name margin"></label>
<div role="radiogroup" class="w-radio-group options"><label role="radio" aria-checked="true" tabindex="0" class="w-radio-button btn is-active"><input type="radio" tabindex="-1" autocomplete="off" class="w-radio-button__orig-radio" value="1"><span class="w-radio-button__inner">name1<!----></span></label><label role="radio" tabindex="-1" class="w-radio-button btn"><input type="radio" tabindex="-1" autocomplete="off" class="w-radio-button__orig-radio" value="2"><span class="w-radio-button__inner">name2<!----></span></label></div>
</section>
这时,我们也可以测试组件的交互了,接下来贴的代码只包含it
部分的代码,不包含重复代码
it('普通类型组件交互', async () => {
const wrapper = shallowMount(RadioOptionGroup, {
propsData: {
value: 1,
options: options
},
stubs: stubs
})
const btns = wrapper.findAllComponents(RadioButton)
expect(btns.exists()).toBeTruthy()
await btns.at(0).find('input').trigger('change')
expect((wrapper.vm as VueEmpower).bindValue).toBe(1)
await btns.at(1).find('input').trigger('change')
expect((wrapper.vm as VueEmpower).bindValue).toBe(2)
})
我们还需要测试组件里面的slot
是否可以生效
it('组件slot展示', () => {
const wrapper = shallowMount(RadioOptionGroup, {
propsData: {
value: 1,
options: [
{
label: 'name1',
value: 1
},
{
label: 'name2',
value: 2
}
]
},
slots: {
default: 'This is a title'
}
})
expect(wrapper.text()).toContain('This is a title')
})
我们的组件中使用了provide
由父组件提供数据,因此也需要对这一功能进行测试。但由于我们使用的是@vue/composition-api
提供的provide
函数,当我直接配置挂载项
里的provide
项时并没有生效,在这一部分我选择了构造一个parentComponent
为测试组件提供数据,同时也需要在挂载项
中提供parentComponent
配置项。
it('provide 类型组件交互', async () => {
const wrapper = shallowMount(RadioOptionGroup, {
propsData: {
options: [
{
label: 'name1',
value: 1
},
{
label: 'name2',
value: 2
}
],
binding: 'test'
},
stubs: stubs,
parentComponent: {
setup () {
provide('OptionData', { test: 1 })
}
}
})
expect((wrapper.vm as VueEmpower).bindValue).toBe(1)
const btns = wrapper.findAllComponents(RadioButton)
expect(btns.exists()).toBeTruthy()
await btns.at(0).find('input').trigger('change')
expect((wrapper.vm as VueEmpower).bindValue).toBe(1)
await btns.at(1).find('input').trigger('change')
expect((wrapper.vm as VueEmpower).bindValue).toBe(2)
})
这时测试组件里面的inject
逻辑就生效了,不过这并不完美。接下来我们需要测试组件的change
事件是否正常,在这一部分其实有
陷阱
。在上面的provide
中,我们提供的是一个普通的对象,而且也通过了测试。然而如果只是把上面的代码
稍加改造检查handleChange
函数的调用情况,那么我们可能会失望。
it('change 事件测试', async () => {
const wrapper = shallowMount(RadioOptionGroup, {
propsData: {
options: [
{
label: 'name1',
value: 1
},
{
label: 'name2',
value: 2
}
],
binding: 'test'
},
stubs: stubs,
parentComponent: {
setup () {
provide('OptionData', {test: 2})
}
}
})
const vm = wrapper.vm as VueEmpower
const spy = jest.spyOn(vm, '$emit')
const spy2 = jest.spyOn(vm, 'handleChange')
expect(spy).toBeCalledTimes(0)
const btns = wrapper.findAllComponents(RadioButton)
await btns.at(0).find('input').trigger('change')
expect(vm.bindValue).toBe(1)
expect(spy2).toBeCalledTimes(1)
expect(spy2.mock.lastCall[0]).toBe(1)
expect(spy.mock.lastCall).toEqual(['change', 1])
})
在这段测试代码中,总是卡在expect(spy2).toBeCalledTimes(1)
这行代码,经过一系列实验之后,我最终发现
父组件的provide
提供的对象不是响应式的,导致handleChange
函数不被调用。其实道理也很好理解,
vdom
的渲染变化依赖响应式对象的变化,这里我们使用provide
对象并不是响应式的,所以对象的变化
并不会触发vdom
的重新渲染,也就不会触发change
事件。这里还有一个知识点,provide
函数(配置项)并不会使普通对象
变成一个响应式对象,为了让handleChange
事件可以正常触发,我们需要把provide
里面的对象改成响应式的。
it('change 事件测试', async () => {
const wrapper = shallowMount(RadioOptionGroup, {
propsData: {
options: [
{
label: 'name1',
value: 1
},
{
label: 'name2',
value: 2
}
],
binding: 'test'
},
stubs: stubs,
parentComponent: {
setup () {
provide('OptionData', reactive({test: 2}))
}
}
})
const vm = wrapper.vm as VueEmpower
const spy = jest.spyOn(vm, '$emit')
const spy2 = jest.spyOn(vm, 'handleChange')
expect(spy).toBeCalledTimes(0)
const btns = wrapper.findAllComponents(RadioButton)
await btns.at(0).find('input').trigger('change')
expect(vm.bindValue).toBe(1)
expect(spy2).toBeCalledTimes(1)
expect(spy2.mock.lastCall[0]).toBe(1)
expect(spy.mock.lastCall).toEqual(['change', 1])
})
所有对象全部通过,我们完成了一个Vue Component
组件的测试
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.