Write the first Vue.js Component Unit Test in Jest
在Jest框架中设计你的第一则Vue.js单元测试用例
Learn how to write unit tests with the official VueJS tools and the Jest framework.
学习如何运用VueJS官方测试工具与Jset框架设计单元测试用例
vue-test-utils, the official VueJS testing library and based on avoriaz, is just around the corner. @EddYerburgh is indeed doing a very good job creating it. It provides all necessary tooling for making easy to write unit test in a VueJS application.
首先,vue-test-utils
基于avoriaz
,是VueJS官方的测试库,集成了众多测试工具,简化了对VueJs应用进行单元测试的设计。
Jest, on the other side, is the testing framework developed at Facebook, which makes testing a breeze, with awesome features such as:
另外一方面,Jest
是Facebook开发的测试框架,有很多清爽简便的特性:
- Almost no config by default
- 默认情况基本无需用户配置相关参数
- Very cool interactive mode
- 交互模式非常酷
- Run tests in parallel
- 对用例并发测试
- Spies, stubs and mocks out of the box
- Spies, 可以提供函数调用的信息,但不会改变函数的行为
- Stubs, 与spies类似,但是会完全替换目标函数。这使得一个被stubbed的函数可以做任何你想要的 —— 例如抛出一个异常,返回某个特定值等等。
- Mocks, 通过组合spies和stubs,使替换一个完整对象更容易。
- Built in code coverage
- 内置可视化覆盖率报告功能
- Snapshot testing
- 快照测试功能
- Module mocking utilities
- 实用的mocking模块
Probably you’ve already written test without this tools, and just by using karma + mocha + chai + sinon + …, but you’ll see how much easier it can be 😉.
也许你曾经没有用过上述工具,只是用karma、mocha、chai、sinon等设计测试用例,那么接下来你将看到颠覆性的用例设计模式。
Set up a vue-test sample project
搭建一个Vue的测试项目
Let’s start by creating a new project using vue-cli
answering NO to all yes/no questions:
我们用vue-cli
搭设一个全新的项目,对所有需要您回答“是否需要。。。”的步骤全部选择“不要(NO)”。
npm install -g vue-cli
vue init webpack vue-test
cd vue-test
Then we’ll need to install some dependencies:
然后我们需要安装以下几个必要的依赖:
# Install dependencies
npm i -D jest jest-vue-preprocessor babel-jest
jest-vue-preprocessor
is needed for making jest understand .vue
files, and babel-jest
for the integration with Babel.
jest-vue-preprocessor
是用来识别并解析VUE组件文件的工具,babel-jest
则是用来整合Babel
工具库的桥梁。
As vue-test-utils
, it can be installed already from npm, since beta.1
has been published.
vue-test-utils
已经发布了beta.1
版本,现在我们已经可以在npm
上安装了。
npm i -D vue-test-utils
Let’s add the following Jest configuration in the package.json
:
我们在package.json
文件中添加以下Jest的配置:
...
"jest": {
"moduleNameMapper": {
"^vue$": "vue/dist/vue.common.js"
},
"moduleFileExtensions": [
"js",
"vue"
],
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
".*\\.(vue)$": "<rootDir>/node_modules/jest-vue-preprocessor"
}
}
...
moduleFileExtensions will tell Jest which extensions to look for, and transform which preprocessor to use for a file extension.
moduleFileExtensions
是来告诉Jest扫描哪些类型的文件;transform
设定了各个类型的文件对应哪些预处理工具。
At last, add a test script to the package.json:
最后我们添加一段测试脚本在package.json
的scripts
里:
{
"scripts": {
"test": "jest",
...
},
...
}
Testing a Component
对一个组件进行测试
I’ll be using Single File Components here, and I haven’t checked if it works by splitting them in their own html, css or js files, so let’s assume you’re doing that as well.
我将会用单文件组件进行测试,我暂时先不管它的html结构、样式和js逻辑是否可行,我先假设你已经把他们都处理好了。
First create a MessageList.vue component under src/components:
首先先创建一个MessageList.vue
组件,放在src/components
目录下:
<template>
<ul>
<li v-for="message in messages">
{{ message }}
</li>
</ul>
</template>
<script>
export default {
name: 'list',
props: ['messages']
}
</script>
And update App.vue to use it, as follows:
然后在App.vue中引入MessageList.vue
:
<template>
<div id="app">
<MessageList :messages="messages"/>
</div>
</template>
<script>
import MessageList from './components/MessageList'
export default {
name: 'app',
data: () => ({ messages: ['Hey John', 'Howdy Paco'] }),
components: {
MessageList
}
}
</script>
We have already a couple of components that we can test. Let’s create a test folder under the project root, and a App.test.js:
我们已经有了一对组件可以进行测试。然后我们在项目根目录下创建一个测试文件夹和一个App.test.js
文件:
import Vue from 'vue'
import App from '../src/App'
describe('App.test.js', () => {
let cmp, vm
beforeEach(() => {
cmp = Vue.extend(App) // Create a copy of the original component
vm = new cmp({
data: { // Replace data value with this fake data
messages: ['XXX']
}
}).$mount() // Instances and mounts the component
})
it('equals messages to ["XXX"]', () => {
expect(vm.messages).toEqual(['XXX'])
})
})
Right now, if we run npm test (or npm t as a shorthand version), the test should run and pass. Since we’re modifying the tests, let’s better run it in watch mode:
现在我们在命令行执行npm test
(或短命令npm run t
),可以看到用例已经被执行,而且已经通过了测试。如果想观察修改用例会有什么效果,可以执行npm t --watch
命令。
npm t -- --watch
The problem with nested components
测试过程中要注意的嵌套组件问题
This test is too simple. Let’s check that the output is the expected as well. For that we can use the amazing Snapshots feature of Jest, that will generate a snapshot of the output and check it against in the upcoming runs. Add after the previous it in App.test.js:
这个测试有些过于简单,我们来检测一下输出结果是否符合我们的预期。
此时我们可以用Jest神奇的快照功能来进行测试,此功能会产生一个输出结果的快照,与接下来后续测试的结果进行对比。
我们先将一下代码添加到App.test.js
中:
it('has the expected html structure', () => {
expect(vm.$el).toMatchSnapshot()
})
That will create a test/snapshots/App.test.js.snap file. Let’s open it and inspect it:
进程会在__snapshots__
目录中产生一个App.test.js.snap
文件。
我们打开后看到里面的代码是:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App.test.js has the expected html structure 1`] = `
<div
id="app"
>
<ul>
<li>
XXX
</li>
</ul>
</div>
`;
In case you haven’t noticed, there is a big problem here: the MessageList component has been rendered as well. Unit tests must be tested as an independent unit, meaning that in App.test.js we wanna test App component and don’t care at all about anything else.
也许你没有发现,这里有个问题——MessageList组件在父组件的快照中已经被成功渲染。
单元测试的理念是单个组件需要被作为独立单元检测,这意味着我们测试App根组件的时候,其实是不在乎其中的子组件具体是什么状态的。
This can be the reason of several problems. Imagine for example, that the children components (MessageList in this case) perform side effect operations on the created hook, such as calling fetch, a Vuex action or state changes? That’s something we definitely don’t want.
这会引起几个相关问题。想象一下,像MessageList这样的子组件,会在created
钩子中引发副作用,比如会不会调用一些获取数据的方法、Vuex的action被调用来改变了state中的数据?这些不是我们想要看到的。
Luckily, Shallow Rendering solves this nicely.
幸运的是,浅渲染可以帮我们完美解决问题!
What is Shallow Rendering?
什么是浅渲染
Shallow Rendering is a technique that assures your component is rendering without children. This is useful for:
浅渲染是确保组件不受子组件影响,被独立测试的技术,其用武之地包括:
- Testing only the component you want to test (that’s what Unit Test stands for)
- 只测试你想测的组件(这本身就是单元测试的初衷)
- Avoid side effects that children components can have, such as making HTTP calls, calling store actions…
- 避免子组件对父组件的影响,如请求HTTP、store中的actions等动作
Testing a Component with vue-test-utils
运用vue-test-utils
来对组件测试
vue-test-utils
provide us with Shallow Rendering among other features. We could rewrite the previous test as follows:
vue-test-utils
提供给我们一系列有用的特性,其中就包括浅渲染,我们可以改写一下我们之前写的测试用例:
import { shallow } from 'vue-test-utils'
import App from '../src/App'
describe('App.test.js', () => {
let cmp
beforeEach(() => {
cmp = shallow(App, { // Create a shallow instance of the component
data: {
messages: ['XXX']
}
})
})
it('equals messages to ["XXX"]', () => {
// Within cmp.vm, we can access all Vue instance methods
expect(cmp.vm.messages).toEqual(['XXX'])
})
it('has the expected html structure', () => {
expect(cmp.element).toMatchSnapshot()
})
})
And now, if you’re still running Jest in watching mode, you’ll see the test still pass, but the Snapshot doesn’t match. Press u to regenerate it. Open and inspect it again:
现在,如果你之前执行的是监听模式,你可以看到测试用例依然通过了,但是快照已经报错了,提示不匹配。 你可以在命令行用u参数,来重新生成快照,我们现在看看快照代码的内容变成什么样了:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App.test.js has the expected html structure 1`] = `
<div
id="app"
>
<!-- -->
</div>
`;
You see? Now no children have been rendered and we tested the App
component fully isolated from the component tree. Also, if you have any created
or whatever hooks in the children components, they haven’t been called either 😉.
看到了吗?现在没有子组件被渲染后的代码了。我们现在终于客观上完成了对App组件进行真正意义上的单元测试。此时无论你在子组件中任何生命周期的钩子里有什么样的动作,都不会被包括在此轮的测试中。
If you’re curious about how shallow render is implemented, check out the source code and you’ll see that basically is stubbing the components
key, the render
method and the lifecycle hooks.
如果你好奇浅渲染的底层逻辑是什么,你可以查看到源码中关于生命周期钩子和相关方法的代码。
In the same vein, you can implement the MessageList.test.js
test as follows:
同样的方式,我们可以创建MessageList.test.js
文件,来对MessageList进行测试:
import { shallow } from 'vue-test-utils'
import MessageList from '../src/components/MessageList'
describe('MessageList.test.js', () => {
let cmp
beforeEach(() => {
cmp = shallow(MessageList, {
// Beaware that props is overriden using `propsData`
propsData: {
messages: ['XXX']
}
})
})
it('has received ["XXX"] as the message property', () => {
expect(cmp.vm.messages).toEqual(['XXX'])
})
it('has the expected html structure', () => {
expect(cmp.element).toMatchSnapshot()
})
})