这节讲一下如何用 jest 进行单元测试
使用 vue-cli 的预设
在执行:vue create xxx
时,选上:Unit Test
,框架选择Jest
框架。
初始化完成后可以看到/tests/unit
目录下有一个:example.spec.ts
文件:
example.spec.ts1 2 3 4 5 6 7 8 9 10 11 12
| import { shallowMount } from '@vue/test-utils' import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => { it('renders props.msg when passed', () => { const msg = 'new message' const wrapper = shallowMount(HelloWorld, { props: { msg }, }) expect(wrapper.text()).toMatch(msg) }) })
|
jest 的配置
跑 jest 的单元测试主要通过根目录下的:jest.config.js
:
1 2 3 4 5 6
| module.exports = { preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', transform: { '^.+\\.vue$': 'vue-jest', }, }
|
我们去到 preset 的目录/node_modules/@vue/cli-plugin-unit-jest/presets/typescript-and-babel
看到,有一个 jest-preset.js:
jest-preset.js1 2 3 4 5 6 7 8 9 10
| const deepmerge = require('deepmerge') const defaultTsPreset = require('../typescript/jest-preset')
module.exports = deepmerge(defaultTsPreset, { globals: { 'ts-jest': { babelConfig: true, }, }, })
|
可以看到这个预设主要是在基础预设上加上了ts-jest
的配置。继续看:/typescript/jest-preset
:
1 2 3 4 5 6 7 8 9
| const deepmerge = require('deepmerge') const defaultPreset = require('../default/jest-preset')
module.exports = deepmerge(defaultPreset, { moduleFileExtensions: ['ts', 'tsx'], transform: { '^.+\\.tsx?$': require.resolve('ts-jest'), }, })
|
可以看到这个moduleFileExtensions
指定了去寻找 ts 文件和 tsx 文件。
transform
用来指定编译代码,这里指定了 tsx
交给ts-jest
这个依赖来编译。
继续往/default/jest-preset
看,可以看到:
jest-preset1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| module.exports = { moduleFileExtensions: [ 'js', 'jsx', 'json', 'vue', ], transform: { '^.+\\.vue$': require.resolve('vue-jest'), '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': require.resolve('jest-transform-stub'), '^.+\\.jsx?$': require.resolve('babel-jest'), }, transformIgnorePatterns: ['/node_modules/'], moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, testEnvironment: 'jest-environment-jsdom-fifteen', snapshotSerializers: ['jest-serializer-vue'], testMatch: ['**/tests/unit/**/*.spec.[jt]s?(x)', '**/__tests__/*.[jt]s?(x)'], testURL: 'http://localhost/', watchPlugins: [ require.resolve('jest-watch-typeahead/filename'), require.resolve('jest-watch-typeahead/testname'), ], }
|
可以看到这些配置非常像 webpack。
单元测试
测试声明
jest 有三种 api,分别是:describe、it、test
。
describe 指一整个套件,it 包含在 describe 里面,特指单个测试。test 是单独的一个测试。
大部分情况都是使用 describe 和 it 去测试,test 可能只会用在一些小项目里,测一下简单的东西。
断言
断言指判断结果是否和我们预期的一样,比如:
example.spec.ts1 2 3 4 5 6 7 8 9 10 11 12
| import { shallowMount } from '@vue/test-utils' import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => { it('renders props.msg when passed', () => { const msg = 'new message' const wrapper = shallowMount(HelloWorld, { props: { msg }, }) expect(wrapper.text()).toMatch(msg) }) })
|
比如这个:expect(wrapper.text()).toMatch(msg)
,指的是期待:wrapper.text()
的结果要 msg 匹配。
除了 toMatch 还有 toBe、toEqual 等 api。可以看官方文档的介绍。
如果需要反转一下匹配结果,比如需要结果不等于什么,只需要在 toxxx 前面加上 not,比如:expect(wrapper.text()).not.toMatch(msg)
预设和清理
包括几个 api:beforeEach/afterEach、beforeAll/afterAll
beforeEach 指每个单元测试执行前都会执行,同理 afterEach 是测试后执行。beforeAll 所有测试开始前只执行一次,afterAll 是所有测试执行完后执行一次。
要注意这些都有作用域,通常写到 describe 里面,比如:
example.spec.ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { shallowMount } from '@vue/test-utils' import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => { beforeAll(() => { console.log('测试还没开始') }) beforeEach(() => { console.log('某个it测试还没开始') }) AfterEach(() => { console.log('某个it测试已经结束') }) afterAll(() => { console.log('所有测试已完成') }) it('renders props.msg when passed', () => { const msg = 'new message' const wrapper = shallowMount(HelloWorld, { props: { msg }, }) expect(wrapper.text()).toMatch(msg) }) })
|
异步测试
默认情况下测试都是同步测试,如果写上异步的代码可能会出现一些问题,比如:
1 2 3 4 5 6 7 8 9 10 11
| describe('HelloWorld.vue', () => { it('renders props.msg when passed', () => { const msg = 'new message' const wrapper = shallowMount(HelloWorld, { props: { msg }, }) setTimeout(() => { expect(wrapper.text()).toMatch('123') }, 123) }) })
|
理论上这个测试是不应该通过的,但是跑起来发现还是可以通过,原因是 jest 认为我们再跑同步测试,所以 setTimeout 里面的代码并没有去执行,解决方法如下。
done
可以传入一个参数 done,然后测试结束后执行:
1 2 3 4 5 6 7 8 9 10 11 12
| describe('HelloWorld.vue', () => { it('renders props.msg when passed', (done) => { const msg = 'new message' const wrapper = shallowMount(HelloWorld, { props: { msg }, }) setTimeout(() => { expect(wrapper.text()).toMatch('123') done() }, 123) }) })
|
promise
可以在函数的返回值使用 Promise 去解决:
1 2 3 4 5 6 7 8 9 10 11 12
| describe('HelloWorld.vue', () => { it('renders props.msg when passed', () => { const msg = 'new message' const wrapper = shallowMount(HelloWorld, { props: { msg }, }) return new Promise((resolve) => { expect(wrapper.text()).toMatch('msg') resolve('') }) }) })
|
测试可以看到也是不通过,说明 Promise 的方法还是有用的。
async、await 语法糖
可以使用上这些新的语法糖:
1 2 3 4 5 6 7 8 9 10 11 12
| describe('HelloWorld.vue', () => { it('renders props.msg when passed', async () => { const msg = 'new message' const wrapper = shallowMount(HelloWorld, { props: { msg }, }) await wrapper.setProps({ msg: '123', }) expect(wrapper.text()).toMatch(msg) }) })
|
因为 vue 修改 dom 不是同步的,如果改了值再想拿到 dom 的值需要 nextTick 这个 api,这也是一个异步操作,所以我们用 await 来等待 dom 修改完成。
测试是不通过的,然后将 toMatch 的值设为'123'
再跑就能通过了。
vue-test-utils 测试 vue3 组件