----------------- 11-5更新 -------------------
哎呀,今天太忙了,被实现大数据表格搞得头晕脑涨,就当写日记了吧,立个Flag
----------------- 11-6更新 -------------------
这篇文章主要介绍如何对Vue组件进行单元测试。我们首先介绍一下单元测试对于开发一个可维护性好的应用的重要性和我们该测试什么。
详细内容包括以下部分:
- 为一个Vue组件新建并运行一个单元测试
- 从各个角度测试一个Vue组件
- 使用mock模拟数据测试异步函数
- 检测单元测试的覆盖率
- 创建单元测试文件
环境准备
- Vue - JavaScript框架
- Vue CLI - Vue开发工具
- Jest - JavaScript测试框架
- Node - JavaScript运行环境
学习目标
- 能够解释单元测试的重要性
- 描述需要测试(或者不应该测试)的内容
- 为一个Vue组件写一个测试套件(unit test suite)
- 使用Vue CLI为你的Vue项目运行单元测试
- 在测试套件中使用beforeEach()和afterEach()方法
- 为Vue组件中的实现细节编写单元测试
- 为Vue组件的行为编写单元测试(点击事件等等)
- 理解为什么mock数据有助于单元测试
- 为mock库和异步函数编写单元测试
- 检测单元测试覆盖率
- 为Vue组件编写一个具有良好架构的单元测试文件
为什么需要单元测试
一般情况下,测试是为了更好地保证你的应用在你的用户面前可以符合预期的那样运行。
一个软件即便拥有很高的测试覆盖率也绝对无法证明是完美的,但可以作为判断一个软件质量的最初指标。另外,可测试的代码一般可以证明一个软件具有良好的架构,这就是为什么一个高级的工程师在整个开发中都会考虑测试。
测试可以分成三个阶段:
- 单元(unit)
- 整体(integration)
- 用户(end-to-end)
单元测试就是将部分独立的具有某些功能代码从整个项目中分离出来并对其进行测试,这个是防止错误bug和逻辑矛盾的第一个措施,单元测试是作为测试驱动开发(TDD)过程中一部分,啥是测试驱动开发?顾名思义,自己百度!
单元测试可以提高代码的可维护性
可维护性有助于加强你代码的健壮性或者说有助于bug修复,也有助未来其他开发者对你的代码进行重构升级
单元测试应该与持续集成(CI)流程相结合,以确保单元测试能够持续执行,理想状态下,是在每次提交到你的仓库时执行,一个稳定的测试套件可以保证在你开发过程中及时的捕捉到异常并修复,避免你的用户在生产环境碰到他。
----------------- 11-7更新 -------------------
测试什么
你应该测试什么,或者说你不应该测试什么。
就单元测试而言,有三种东西可以测试:
- 实现细节,一个组件根据输入得到某种结果的底层业务逻辑
- 公共方法/组件,根据特定输入的到特定的结果
- 附带影响,比如点击一个按钮触发了某个事件
因为你不可能测试所有东西,所以我们的关注点应该在哪里?关注用户交互位置的输入输出,用户的直接体验永远在第一位。通过关注某个模块的输入输出,让你只测试用户会要体验到东西。
- 输入:data, props, user interaction, lifecycle methods, Vuex store, route params, query strings
- 输出:rendered output, events, data results, Vuex store updates, dispatches
或许还有很多内部复杂的逻辑需要测试,但这些测试最好是在过后重构优化代码的过程中完成。
----------------- 11-8更新 -------------------
在Vue中进行单元测试
因为组件就像是vue中的一块块积木,也是你整个应用中重要的组成部分。所以花时间去为你的组件编写单元测试吧。
一个好的测试工具满足以下几点:
- 易于编写测试
- 快速编写测试
- 通过一个简单的命令行运行测试
- 测试运行够快
我希望你能感受单元测试的乐趣,并驱使你做更多的测试
单元测试工具
- Vue Test Utils - Vue官方的单元测试工具包
- Jest - 负责找到测试文件并执行测试的运行工具
单元测试概述
开始之前,让我们先谈谈关于在vue中单元测试文件的命名规定,单元测试文件应该按照以下格式命名:
组件名.spec.js
通常情况下,需要为每个vue组件准备一个单元测试文件,每个文件中包含一个测试套件或者多个测试套件
所有测试文件需要跟vue组件的存放位置要分开,像放在tests/unit文件夹下:
├── node_modules
├── public
├── src
│ ├── assets
│ └── components
└── tests
└── unit
运行测试
Vue cli可以通过执行以下命令使用jest来运行单元测试:
npm run test:unit
得到下面结果
> vue-weather-app@0.1.0 test:unit vue-weather-app
> vue-cli-service test:unit
PASS tests/unit/Footer.spec.js
PASS tests/unit/Header.spec.js
PASS tests/unit/Banner.spec.js
PASS tests/unit/Weather.spec.js
PASS tests/unit/Search.spec.js
PASS tests/unit/App.spec.js
=============================== Coverage summary ===============================
Statements : 89.36% ( 42/47 )
Branches : 90% ( 9/10 )
Functions : 94.12% ( 16/17 )
Lines : 89.36% ( 42/47 )
================================================================================
Test Suites: 6 passed, 6 total
Tests: 22 passed, 22 total
Snapshots: 0 total
Time: 4.255s
Ran all test suites.
示例
使用Vue Weather App这个看天气的小项目来看一些测试的例子
示例1
让我们来看看vue单元测试的第一个示例,第一个单元测试文件在tests/unit/Header.spec.js下用来测试header组件
import { shallowMount } from '@vue/test-utils'
import Header from '@/components/Header.vue'
describe('Header.vue Test', () => {
it('renders message when component is created', () => {
// render the component
const wrapper = shallowMount(Header, {
propsData: {
title: 'Vue Project'
}
})
// check the name of the component
expect(wrapper.name()).toMatch('Header')
// check that the title is rendered
expect(wrapper.text()).toMatch('Vue Project')
})
})
mounting
在Header.spec.js测试文件中的第一行从Vue Test Utils库中引入了一个叫shallowMount的方法,所谓mounting意思就是每个独立组件的加载到可以测试的状态
Vue Test Utils中有两个方法:
- shallowMount() -创建一个wrapper包裹住Vue组件,但不包含子组件,只保留子组件占有的位置(推荐)
- mount() - 跟shallowMount()一样但包含子组件
因为我们要测试的是每个独立的组件,所以我们接下来使用的是shallowMount()
**shallowMount()**对测试每个独立的组件来说是更好的选择,因为子组件已经被排除,是单元测试的理想状态
另外, **shallowMount()**对测试的执行速度有很大提升,因为没有了渲染子组件的性能消耗
**mount()**在你想要测试子组件行为的时候同样也有用
在代码的第二行引入了要测试的vue组件,Header.vue
Describe
在使用了import声明之后,describe定义了一个测试套件(unit test suite)
在单元测试文件中,可以拥有多个定义了不同unit test suite的describe,意味着每个describe可以使用it包含多个单元测试
describe和it其中的区别就是:
- describe - unit test suite
- it - 每个独立的单元测试方法
在vue中进行单元测试很棒的地方就是它拥有很多内置的方法添加注释,比如,describe允许你在第一个参数传入该unit test suite的命名,可以传入要测试的组件名字方便看哪个组件的测试结果
it同样地可以传入每个测试的描述,比如测试组件被创建时的渲染信息。
Expects
在实际单元测试中,第一步就是加载Vue组件然后才能被测试
// 渲染组件
const wrapper = shallowMount(Header, {
propsData: {
title: 'Vue Project'
}
})
shallowMount方法返回一个包含了已经加载的component和测试该组件方法的wrapper对象,wrapper对象允许我们测试所有Vue component生成的HTML和属性(比如data)
另外,传递给Header组件的props属性,要放在shallowMount方法的第二个参数上
真正运行测试的代码:
// 检查测试组件的名字
expect(wrapper.name()).toMatch('Header')
// 检查标题是否被渲染
expect(wrapper.text()).toMatch('Vue Project')
这两行代码在wrapper上执行以下检查来测试Vue组件,vue官方文档有说明wrapper可以检查什么
- 检查组件的名字是不是Header
- 检查组件生成的标题是不是’Vue Project’
这些检查都是比较string字符串,所以推荐的测试方法是toMatch()
Jest Helpers
因为Header组件只检查了string值,但Jest其实还有很多可选的测试类型可以检查,详细可查看Jest官方文档(或者等我之后补充)。
另外,还有not限定符可以使用
expect(wrapper.name()).not.toMatch('Header')
示例2 - 测试初始条件
该示例会展示如何提前设置好Vue组件的初始条件(或者说状态)。
tests/unit/Weather.spec.js
import { shallowMount } from '@vue/test-utils'
import Weather from '@/components/Weather.vue'
describe('Weather.vue Implementation Test', () => {
let wrapper = null
// 运行单元测试前的配置
beforeEach(() => {
// render the component
wrapper = shallowMount(Weather, {
propsData: {
city: '',
weatherSummary: '',
weatherDescription: '',
currentTemperature: 0.0,
lowTemperature: 0.0,
highTemperature: 0.0
}
})
})
// 单元测试运行完后销毁
afterEach(() => {
wrapper.destroy()
})
it('initializes with correct elements', () => {
...
})
it('processes valid props data', () => {
...
})
it('emits a custom event when clearWeather() is called', () => {
...
})
})
BeforEach和AfterEach
在describe里面定义了两个新方法:
- beforeEach() - 在每个unit test suite运行前执行
- afterEach() - 在每个unit test suite运行后执行
**beforeEach()**让每个单元测试运行前都有一个不变的状态数据,这样一个单元测试的结果不会影响到下一个测试甚至整个测试
在下面例子中,**beforeEach()**方法保持默认数据不变的情况渲染组件
// SETUP - run before to each unit test
beforeEach(() => {
// render the component
wrapper = shallowMount(Weather, {
propsData: {
city: '',
weatherSummary: '',
weatherDescription: '',
currentTemperature: 0.0,
lowTemperature: 0.0,
highTemperature: 0.0
}
})
})
afterEach()负责清理每个单元测试运行过后访问数据,在示例中,afterEach()会在每次单元测试执行过后销毁wrapper,这样可以保证wrapper在下一次单元测试时保持初始状态。
afterEach(() => {
wrapper.destroy()
})
当然了,如果你想在所有单元测试执行完后再执行以上操作可以使用:
beforeAll(() => {
/* Runs before all tests */
})
afterAll(() => {
/* Runs after all tests */
})
Expects
it('initializes with correct elements', () => {
// check the name of the component
expect(wrapper.name()).toMatch('Weather')
// check that the heading text is rendered
expect(wrapper.findAll('h2').length).toEqual(2)
expect(wrapper.findAll('h2').at(0).text()).toMatch('Weather Summary')
expect(wrapper.findAll('h2').at(1).text()).toMatch('Temperatures')
// check that 6 fields of data for the temperature are displayed
expect(wrapper.findAll('p').length).toEqual(6)
expect(wrapper.findAll('p').at(0).text()).toMatch('City:')
expect(wrapper.findAll('p').at(1).text()).toMatch('Summary:')
expect(wrapper.findAll('p').at(2).text()).toMatch('Details:')
expect(wrapper.findAll('p').at(3).text()).toMatch('Current: 0° F')
expect(wrapper.findAll('p').at(4).text()).toMatch('High (Today): 0° F')
expect(wrapper.findAll('p').at(5).text()).toMatch('Low (Today): 0° F')
})
检查的东西:
- 第一个空格行前expect区块检查的内容是组件的名字
- 第二个区块检查两个标题元素符合预期
- 第三个区块检查六个数据符合预期
----------------- 11-9更新 -------------------
示例3 - 测试Props
第二个单元测试负责检查传入Weather组件的props属性是否被正确地处理
it('processes valid props data', () => {
// Update the props passed in to the Weather component
wrapper.setProps({
city: 'Chicago',
weatherSummary: 'Cloudy',
weatherDescription: 'Cloudy with a chance of rain',
currentTemperature: 45.1,
lowTemperature: 42.0,
highTemperature: 47.7
})
// check that the prop data is stored as expected within the component
expect(wrapper.vm.city).toMatch('Chicago')
expect(wrapper.vm.weatherSummary).toMatch('Cloudy')
expect(wrapper.vm.weatherDescription).toMatch('Cloudy with a chance of rain')
expect(wrapper.vm.currentTemperature).toEqual(45.1)
expect(wrapper.vm.lowTemperature).toBeCloseTo(42.0)
expect(wrapper.vm.highTemperature).toBe(47.7)
// check that 6 fields of data for the temperature are displayed
expect(wrapper.findAll('p').length).toEqual(6)
expect(wrapper.findAll('p').at(0).text()).toMatch('City: Chicago')
expect(wrapper.findAll('p').at(1).text()).toMatch('Summary: Cloudy')
expect(wrapper.findAll('p').at(2).text()).toMatch('Details: Cloudy with a chance of rain')
expect(wrapper.findAll('p').at(3).text()).toMatch('Current: 45.1° F')
expect(wrapper.findAll('p').at(4).text()).toMatch('High (Today): 47.7° F')
expect(wrapper.findAll('p').at(5).text()).toMatch('Low (Today): 42° F')
})
因为**beforeEach()方法已经设置了默认初始传入的prop数据,所以我们需要使用setProps()**方法覆盖这个原始prop数据
检查的东西:
- 随着props的更新,我们可以使用wrapper.vm拿到修改后的props并对其进行检查
- 第二类负责检查prop是否符合预期地被更新到元素上
----------------- 11-10更新 -------------------
示例4 - 测试用户输入
这次测试用户点击"Clear Weather Data"按钮时Weather组件中的clear-weather-data事件有没有emit
it('emits a custom event when the Clear Weather Data button is clicked', () => {
// 'Clear Weather Data' 按钮被点击时触发一个事件
wrapper.findAll('button').at(0).trigger('click')
// 检查触发事件有没有emit
expect(wrapper.emitted('clear-weather-data')).toBeTruthy()
expect(wrapper.emitted('clear-weather-data').length).toBe(1)
})
为了触发点击事件,必须在wrapper中找到button元素,并用trigger方法触发点击事件
一旦按钮被点击,单元测试就会检查该自定义事件是否被emit
----------------- 11-11更新 -------------------
Mock示例
在App组件中,当用户搜索天气后,将会使用第三方库Axios向Open Weather发起HTTP GET请求获取天气数据
// get请求用户数据
axios.get('http://api.openweathermap.org/data/2.5/weather?q=' + inputCity + '&units=imperial&APPID=' + this.openweathermapApiKey)
.then((response) => {
// 请求成功
console.log(response)
this.weatherData.city = response.data.name
this.weatherData.weatherSummary = response.data.weather[0].main
this.weatherData.weatherDescription = response.data.weather[0].description
this.weatherData.currentTemperature = response.data.main.temp
this.weatherData.lowTemperature = response.data.main.temp_min
this.weatherData.highTemperature = response.data.main.temp_max
this.validWeatherData = true
})
.catch((error) => {
// 请求失败
this.messageType = 'Error'
this.messageToDisplay = 'ERROR! Unable to retrieve weather data for ' + inputCity + '!'
console.log(error.message)
this.resetData()
})
.finally((response) => {
// 总是最后执行
console.log('HTTP GET Finished!')
})
当发起请求时要考虑两种情形:
- 请求成功
- 请求失败
使用外部API进行请求相比编写mock要简单很多,但mock也有其优缺点:
优点:
- 不用依赖网络
- 不会因为外部API失效而请求失败
- 比起外部API请求更快
缺点:
- 当API协议变更后测试也要变更
- 在微服务架构中很难保持API的变化
- mock会添加很多杂乱的东西在test suites当中
----------------- 11-12更新 -------------------
示例5 - 测试异步代码(成功案例)
该单元测试放在tests/unit/App.spec.js
要先引入axios库
import { shallowMount } from '@vue/test-utils'
import Content from '@/components/App.vue'
import axios from 'axios'
为了不用实际地请求外部API,我们需要mock模拟axios库
// 模拟axios库
jest.mock('axios');
这行代码主要是为了告诉Jest需要模拟axios库
在单元测试文件tests/unit/Content.spec.js,为了测试content组件:
- 模拟一个成功的get请求
- 模拟一个失败的get请求
describe('App.vue Test with Successful HTTP GET', () => {
let wrapper = null
beforeEach(() => {
const responseGet = { data:
{
name: 'Chicago',
weather: [
{
main: 'Cloudy',
description: 'Cloudy with a chance of rain'
}
],
main: {
temp: 56.3,
temp_min: 53.8,
temp_max: 58.6
}
}
}
// 设置请求成功的响应结果
axios.get.mockResolvedValue(responseGet)
// 渲染组件
wrapper = shallowMount(App)
})
afterEach(() => {
jest.resetModules()
jest.clearAllMocks()
})
...
})
BeforeEach和AfterEach
在beforeEach()方法当中,设置了调用axios.get()后的响应结果,关键代码如下:
// 设置请求成功后响应结果
axios.get.mockResolvedValue(response_get);
改行代码的关键在于不用真正地发起请求而是让jest知道axios.get()被调用时返回response_get结果
afterEach(() => {
jest.resetModules();
jest.clearAllMocks();
});
因为**afterEach()**在每个单元测试执行完后执行,目的是为了清空重置已经mock的脏数据,保证每一次单元测试都是从最初的状态的开始而不被上一次单元测试的结果影响
Expects
it('does load the weather data when a successful HTTP GET occurs', () => {
wrapper.vm.searchCity('Chicago')
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.get).toBeCalledWith(expect.stringMatching(/Chicago/))
wrapper.vm.$nextTick().then(function () {
// check that the user data is properly set
expect(wrapper.vm.weatherData.city).toMatch('Chicago')
expect(wrapper.vm.weatherData.weatherSummary).toMatch('Cloudy')
expect(wrapper.vm.weatherData.weatherDescription).toMatch('Cloudy with a chance of rain')
expect(wrapper.vm.weatherData.currentTemperature).toEqual(56.3)
expect(wrapper.vm.weatherData.lowTemperature).toEqual(53.8)
expect(wrapper.vm.weatherData.highTemperature).toEqual(58.6)
expect(wrapper.vm.validWeatherData).toBe(true)
})
})
因为我们已经定义了mock并通过**shallowMount()加载了组件,接下来我可以开始测试
首先我们调用searchCity()**方法
wrapper.vm.searchCity('Chicago')
先检查**axios.get()**是否指被调用一次,并且是用正确的城市名发起请求
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.get).toBeCalledWith(expect.stringMatching(/Chicago/))
测试**axios.get()**被调用后返回的数据与预想的数据保持一致
wrapper.vm.$nextTick().then(function () {
// check that the user data is properly set
expect(wrapper.vm.weatherData.city).toMatch('Chicago')
expect(wrapper.vm.weatherData.weatherSummary).toMatch('Cloudy')
expect(wrapper.vm.weatherData.weatherDescription).toMatch('Cloudy with a chance of rain')
expect(wrapper.vm.weatherData.currentTemperature).toEqual(56.3)
expect(wrapper.vm.weatherData.lowTemperature).toEqual(53.8)
expect(wrapper.vm.weatherData.highTemperature).toEqual(58.6)
expect(wrapper.vm.validWeatherData).toBe(true)
})
注意这里使用了$nextTick(),因为axios.get()是异步操作,要保证请求并且DOM上的数据更新后再测试,否则会失败
----------------- 11-14更新 -------------------
示例6 - 测试异步代码(失败案例)
BeforeEach和AfterEach
beforeEach(() => {
axios.get.mockRejectedValue(new Error('BAD REQUEST'))
wrapper = shallowMount(App)
})
与之前不同,我们要让**beforeEach()中模拟axios.get()**请求失败
afterEach(() => {
jest.resetModules();
jest.clearAllMocks();
});
afterEach的作用和之前一样
Expects
it('does not load the weather data when a failed HTTP GET occurs', () => {
wrapper.vm.searchCity('Chicago')
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.get).toBeCalledWith(expect.stringMatching(/Chicago/))
wrapper.vm.$nextTick().then(function () {
// 检查当请求失败时不应该有数据
expect(wrapper.vm.weatherData.city).toMatch(/^$/)
expect(wrapper.vm.weatherData.weatherSummary).toMatch(/^$/)
expect(wrapper.vm.weatherData.weatherDescription).toMatch(/^$/)
expect(wrapper.vm.weatherData.currentTemperature).toEqual(0)
expect(wrapper.vm.weatherData.lowTemperature).toEqual(0)
expect(wrapper.vm.weatherData.highTemperature).toEqual(0)
expect(wrapper.vm.validWeatherData).toBe(false)
// 检查有没有提示错误信息
expect(wrapper.vm.messageToDisplay).toMatch('ERROR! Unable to retrieve weather data for Chicago!')
expect(wrapper.vm.messageType).toMatch('Error')
expect(global.console.log).toHaveBeenCalledWith('BAD REQUEST');
})
})
覆盖率
所谓覆盖率就是有多少源代码被测试到
100%的覆盖率不代表你的代码被很好地测试到
这只能证明你已经做好了大量的测试工作,单元测试的质量仍然需要你亲自去检查
为了让jest生成覆盖率报告,你需要在jest.config.js里面进行以下配置
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.{js,vue}",
"!**/node_modules/**"
],
coverageReporters: [
"html",
"text-summary"
]
- collectCoverage:告诉jest是否生成覆盖率报告
- collectCoverageFrom: 生成的数据放在哪
- coverageReporters: 如何展示数据
通过在执行以下命令查看测试数据
$ npm run test:unit
> vue-weather-app@0.1.0 test:unit vue-weather-app
> vue-cli-service test:unit
PASS tests/unit/App.spec.js
PASS tests/unit/Search.spec.js
PASS tests/unit/Weather.spec.js
PASS tests/unit/Banner.spec.js
PASS tests/unit/Header.spec.js
PASS tests/unit/Footer.spec.js
=============================== Coverage summary ===============================
Statements : 89.36% ( 42/47 )
Branches : 90% ( 9/10 )
Functions : 93.75% ( 15/16 )
Lines : 89.36% ( 42/47 )
================================================================================
Test Suites: 6 passed, 6 total
Tests: 19 passed, 19 total
Snapshots: 0 total
Time: 6.077s
Ran all test suites.
单元测试结构
通过各种以上单元测试,推荐使用以下结构作为vue组件的单元测试
import { shallowMount } from '@vue/test-utils'
import App from '@/App.vue' // 引入要测试的组件
import axios from 'axios' // 引入要模拟请求的库
// 模拟axios
jest.mock('axios.')
describe('Tests for the ... Component', () => {
let wrapper = null
beforeEach(() => {
// 设置初始值或者创建模拟请求库
// 渲染组件
wrapper = shallowMount(App)
})
afterEach(() => {
jest.resetModules()
jest.clearAllMocks() // 只在模拟请求时需要
})
it('check the initial conditions when the component is rendered', () => {
// 检查组件名字
expect(wrapper.name()).toMatch('...')
})
it('check successful events', () => {
})
...
})
- 将相关测试放在同一个unit test suite中
- 利用**beforeEach()和afterEach()**函数来创建独立的单元测试函数
- 为测试的Vue组件使用到的库做一个模拟d
- 在**beforeEach()**中渲染组件,在单元测试方法中更新prop数据
- 比起**mount()更应该使用shallowMount()**去测试每一个独立的组件
总结
- 为什么写单元测试
- 单元测试应该测试或者不应该测试什么
- 如何写单元测试
简单地说,思考该测试什么的时候,只需要关注输入和输出(实际的结果),结果如何得到不需要考虑测试。
本文详细介绍了如何对Vue组件进行单元测试,包括为何需要单元测试、测试内容、使用Vue CLI与Jest进行测试,以及如何模拟数据测试异步函数。文章通过多个示例展示了如何创建和运行单元测试,强调了单元测试在提高代码可维护性、确保应用质量方面的重要性。
1449

被折叠的 条评论
为什么被折叠?



