九、调试和测试
在上一章中,我们实现了训练管理页面。我们学习了如何使用 Google Firebase 数据存储机制来存储静态文件,并再次使用实时数据库来存储训练对象。我们使用 Bootstrap 为训练管理页面构建了一个响应性布局,并学习了如何使用 Bootstrap 的模式组件在一个漂亮的弹出窗口中显示每个单独的训练。现在我们有了一个完全负责任的应用程序。多亏了 Bootstrap,我们不需要实现任何特殊功能就可以拥有一个好的移动表示。以下是在手机屏幕上添加新训练的效果:
在手机屏幕上添加新训练
这就是我们的 modal 在移动设备上的外观:
在移动设备上显示训练模式
现在是测试我们的应用程序的时候了。我们将使用笑话(https://facebook.github.io/jest/ 构建单元测试并运行快照测试。在这一章中,我们将做以下工作:
- 了解如何配置我们的 Vue.js 应用程序以使用 Jest
- 使用 Jest 断言测试 Vuex 存储
- 学习如何使用
jest.mock
和jest.fn
方法模拟复杂对象 - 实现快照 Vue 组件测试了解如何
为什么测试很重要?
我们的 ProFitOro 应用程序工作正常,不是吗?我们已经在浏览器中打开了很多次,我们检查了所有实现的功能,所以它可以正常工作,对吗?是的,没错。现在转到设置页面,尝试将计时器的值更改为奇怪的值。用负值试试看,用大值试试看,用字符串试试看,用空值试试看……你认为这算是一种不错的用户体验吗?
你不想在这几分钟内工作,是吗?
你有没有尝试过创建一个奇怪的训练?您是否尝试过在创建时引入一个巨大的训练名称,并查看其显示方式?有数千个角落案例,所有这些都应该仔细测试。我们希望我们的应用程序是可维护的、可靠的,并且能够提供令人惊叹的用户体验。
什么是玩笑?
你知道,Facebook 的人从来不会厌倦创建新的工具。React、redux、React native 和所有这些反应性家族对他们来说都是不够的,他们创建了一个非常强大、易于使用的测试框架,名为 Jest:https://facebook.github.io/jest/ 。Jest 非常酷,因为它是自包含的,足以让您不被广泛的配置或寻找异步测试插件、模拟库或假计时器(与您喜爱的框架一起使用)所分心。玩笑集所有功能于一身,尽管非常轻巧。除此之外,在每次运行中,它只运行自上次测试运行以来更改过的测试,这非常优雅和漂亮,因为它很快!
Jest 最初是为测试 React 应用程序而创建的,后来证明它适合于其他用途,包括 Vue.js 应用程序。
查看 Roman Kuba 在 2017 年 6 月于波兰举行的 Vue.js 会议上发表的精彩演讲(https://youtu.be/pqp0PsPBO_0 ),他简单地解释了如何使用 Jest 测试 Vue 组件。
我们的应用程序不仅仅是一个 Vue 应用程序,它是一个 Nuxt 应用程序,在其中使用 Vuex 存储和 Firebase。所有这些依赖性都使测试变得有点困难,因为我们必须模拟所有的东西,并且因为 Nuxt 应用程序本身的特殊性。然而,这是可能的,在一切都设置好之后,编写测试的乐趣是巨大的!走吧!
开始开玩笑
让我们首先测试一个小和函数,并检查它是否正确地对两个数字求和。
当然,第一步是安装 Jest:
npm install jest
创建目录test
并添加名为sum.js
的文件,内容如下:
// test/sum.js
export default function sum (a, b) {
return a + b
}
现在为该函数添加一个测试规范文件:
// sum.spec.js
import sum from './sum'
describe('sum', () => {
it('create sum of 2 numbers', () => {
expect(sum(15, 8)).toBe(23)
})
})
我们需要一个命令来运行测试。在将调用命令jest
的package.json
文件中添加条目"test"
:
// package.json
"scripts": {
//...
"test": "jest"
}
现在如果您运行npm test
,您将看到一些错误:
当我们使用 Jest 运行测试时,测试输出中出现错误
这是因为我们的玩笑没有意识到我们正在使用ES6!因此,我们需要添加babel-jest
依赖项:
npm install babel-jest --save-dev
安装babel jest后,我们需要添加一个.babelrc
文件,内容如下:
// .babelrc
{
"presets": ["es2015"]
}
关于[T0]、[T1]和其他未被识别的全局性的 IDE 警告,您不感到恼火吗?只需在您的.eslintrc.js
文件中添加一个条目jest: true
:
// .eslintrc.js
module.exports = {
root: true,
parser: 'babel-eslint',
env: {
browser: true,
node: true,
jest: true
},
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
rules: {},
globals: {}
}
现在如果你运行npm test
,测试就通过了!
祝贺您刚刚设置并运行了第一个 Jest 测试!
覆盖范围
单元测试有助于确保他们正在检查的代码片段(单元)能够进行任何可能和不可能的输入。每一个书面的单元测试都覆盖了相应的代码块作为一个整体,保护代码不受未来故障的影响,并使我们对代码的功能性和可维护性感到满意。有不同类型的代码覆盖:语句覆盖、行覆盖、分支覆盖等等。代码覆盖的越多,它就越稳定,我们就越舒适。这就是为什么在编写单元测试时,每次运行时检查代码覆盖率是非常重要的。用 Jest 检查代码覆盖率很容易。您不需要安装任何外部工具或编写额外的配置。只需使用覆盖率标志执行 test 命令:
npm test -- --coverage
您将神奇地看到这个美丽的覆盖输出:
使用覆盖率运行 Jest 测试
就像一个咒语,对吗?
在chapter9/1/profitoro
目录中找到代码。别忘了在上面跑npm install
。
测试实用功能
现在让我们测试我们的代码!让我们从 utils 开始。创建一个名为utils.spec.js
的文件并导入leftPad
函数:
import { leftPad } from '~/utils/utils'
再次查看此函数:
// utils/utils.js
export const leftPad = value => {
if (('' + value).length > 1) {
return value
}
return '0' + value
}
如果输入字符串的长度大于1
,则此函数应返回输入字符串。如果字符串的长度为1
,则应返回前面有0
的字符串。
似乎很容易测试,对吗?我们将编写两个测试用例:
// test/utils.spec.js
describe('utils', () => {
describe('leftPad', () => {
it('should return the string itself if its length is more than 1', () => {
expect(leftPad('01')).toEqual('01')
})
it('should add a 0 from the left if the entry string is of the length of 1', () => {
expect(leftPad('0')).toEqual('00')
})
})
})
Argh…如果运行此测试,您将得到一个错误:
当然,可怜的 Jest,它不知道我们在 Nuxt 应用程序中使用的别名。它的[T0]符号等于零!幸运的是,它很容易修复。只需将jest
条目添加到package.json
文件中,其中包含一个名称映射器条目:
// package.json
"jest": {
"moduleNameMapper": {
"^~(.*)$": "<rootDir>/$1"
}
}
现在 Jest 将知道以[T0]开头的所有内容都应该映射到根目录。如果您现在运行npm test -- --coverage
,您将看到测试正在通过!
映射根目录别名后,测试将毫无问题地运行
然而,代码的覆盖率很低。这是因为我们的 UTIL 中还有另一个功能需要测试。检查utils.js
文件。你能看到numberOfSecondsFromNow
方法吗?它还需要一些测试覆盖率。它计算从给定输入时间到现在所经过的时间。我们应该如何处理这个Date.now
?我们无法预测测试结果,因为我们无法保证现在的测试运行时刻与我们检查时相同。每一毫秒都很重要。容易的我们应该嘲笑Date.now
对象!
戏谑
事实证明,即使是一些看似不可能的事情(停止时间)也可以通过玩笑实现。使用jest.fn()
函数模拟Date.now
对象相当容易。
查看有关嘲弄的文档:
http://facebook.github.io/jest/docs/en/snapshot-testing.html#tests-应该是确定性的
我们可以通过调用Date.now = jest.fn(() => 2000)
来模拟这个Date.now
函数。
现在我们可以轻松测试'numberOfSecondsFromNow'
功能:
// test/utils.spec.js
import { leftPad, numberOfSecondsFromNow } from '~/utils/utils'
//...
describe('numberOfSecondsFromNow', () => {
it('should return the exact number of seconds from now', () => {
Date.now = jest.fn(() => 2000)
expect(numberOfSecondsFromNow(1000)).toEqual(1)
})
})
现在的覆盖范围更好了,但如果我们能覆盖我们有趣的beep
功能,那就太完美了。我们应该在它里面测试什么?让我们试着测试一下,当调用beep
函数时,调用Audio.play
方法。模拟函数有一个名为模拟的特殊属性,该属性包含有关此函数的所有信息—对其执行的调用数、传递给它们的信息,等等。因此,我们可以这样模拟Audio.prototype.play
方法:
let mockAudioPlay = jest.fn()
Audio.prototype.play = mockAudioPlay
调用 beep 方法后,我们可以检查模拟上执行的调用数,如下所示:
expect(mockAudioPlay.mock.calls.length).toEqual(1)
或者我们可以断言 mock 的调用方式如下:
expect(mockAudioPlay).toHaveBeenCalled()
整个测试可能如下所示:
describe('beep', () => {
it('should call the Audio.play functuon', () => {
let mockAudioPlay = jest.fn()
Audio.prototype.play = mockAudioPlay
beep()
expect(mockAudioPlay.mock.calls.length).toEqual(1)
expect(mockAudioPlay).toHaveBeenCalled()
})
})
为了避免模拟本机函数产生的副作用,我们可能希望在测试后重置模拟:
it('should call the Audio.play functuon', () => {
// ...
expect(mockAudioPlay).toHaveBeenCalled()
mockAudioPlay.mockReset()
})
检查这方面的 Jest 文档:https://facebook.github.io/jest/docs/en/mock-function-api.html#mockfnmockreset 。
或者,您可以配置 Jest 设置,以便在每次测试后自动重置模拟。为此,将clearMocks
属性添加到package.json
文件中的 Jestconfig
对象中:
//package.json
"jest": {
"clearMocks": true,
"moduleNameMapper": {
"^~(.*)$": "<rootDir>/$1"
}
},
耶!考试通过了。检查保险范围。它看起来很漂亮;但是,分支机构的覆盖范围仍然不完善:
utils.js 文件中的分支覆盖率仅为 75%
为什么会发生这种情况?首先检查Uncovered Lines
栏。它向我们显示了测试未涵盖的线路。这是numberOfSecondsFromNow
方法的22
行:
export const numberOfSecondsFromNow = startTime => {
const SECOND = 1000
if (!startTime) {
return 0
}
return Math.floor((Date.now() - startTime) / SECOND)
}
或者,您可以检查项目目录中的coverage
文件夹,并在浏览器中打开lcov-report/index.html
文件,以更直观的方式检查到底发生了什么:
代码覆盖率 HTML 以一种很好的视觉方式显示覆盖行和未覆盖行
在这里,您可以清楚地看到行22
被标记为红色,这意味着它没有被测试覆盖。好吧,让我们来报道它吧!当startTime
属性未传递给此方法时,只需添加一个覆盖该情况的新测试,并确保其返回0
:
// test/utils.js
describe('numberOfSecondsFromNow', () => {
it('should return 0 if no parameter is passed', () => {
expect(numberOfSecondsFromNow()).toEqual(0)
})
it('should return the exact number of seconds from now', () => {
Date.now = jest.fn(() => 2000)
expect(numberOfSecondsFromNow(1000)).toEqual(1)
})
})
现在使用覆盖率标志运行测试。天啊!这不是太棒了吗?
100%的代码覆盖率,是不是很棒?
本节的最终代码可在chapter9/2/profitoro
文件夹中找到。
开玩笑测试 Vuex 商店
现在让我们来测试一下我们的 Vuex 商店。我们要测试的存储最关键的部分是我们的行为和突变,因为它们实际上可以改变存储的状态。让我们从突变开始。在test
文件夹中创建mutations.spec.js
文件并导入mutations.js
:
// test/mutations.spec.js
import mutations from '~/store/mutations'
我们已经准备好为我们的变异函数编写单元测试了。
检测突变
突变是非常简单的函数,它接收状态对象并将其某些属性设置为给定值。因此,测试突变是相当容易的,我们只需模拟 state 对象,并用我们想要设置的值将其传递给我们想要测试的突变。最后,我们必须检查该值是否已实际设置。例如,让我们测试突变setWorkingPomodoro
。这就是我们的突变看起来的样子:
// store/mutations.js
setWorkingPomodoro (state, workingPomodoro) {
state.config.workingPomodoro = workingPomodoro
}
在我们的测试中,我们需要为 state 对象创建一个 mock。它不需要表示完整的状态;它至少需要模拟状态的config
对象的workingPomodoro
属性。然后我们将调用该变异,将我们的模拟状态和新值传递给它workingPomodoro
,我们将断言该值已应用于我们的模拟。因此,以下是步骤:
- 为状态对象创建模拟:
let state = {config: {workingPomodoro: 1}}
- 使用新值调用突变:
mutations.setWorkingPomodoro(state, 30)
- 断言该值已设置为模拟对象:
expect(state.config).toEqual({workingPomodoro: 30})
此测试的完整代码如下所示:
// test/mutations.spec.js
import mutations from '~/store/mutations'
describe('mutations', () => {
describe('setWorkingPomodoro', () => {
it('should set the workingPomodoro property to 30', () => {
let state = {config: {workingPomodoro: 1}}
mutations.setWorkingPomodoro(state, 30)
expect(state.config).toEqual({workingPomodoro: 30})
})
})
})
应该应用完全相同的机制来检测其余的突变。去吧,把他们都干完!
带 Jest 的异步测试–测试动作
让我们转到更复杂的东西来测试我们的行为!我们的操作大多是异步的,它们在内部使用复杂的 Firebase 应用程序对象。这让他们很难测试,但我们确实喜欢挑战,不是吗?让我们看一下actions.js
文件中的第一个动作。uploadImages
动作看起来像这样:
uploadImages ({state}, files) {
return Promise.all(files.map(this._uploadImage))
}
我们可以在这里测试什么?例如,我们可以测试_uploadImage
函数被调用的次数是否与所传递图像数组的大小完全相同。为此,我们必须模仿_uploadImage
方法。为了做到这一点,我们也将其导出到我们的actions
:
// store/actions.js
function _uploadImage (file) {
//...
}
export default {
_uploadImage,
uploadImages ({state}, files) {
return Promise.all(files.map(this._uploadImage))
}
//...
}
现在我们可以模拟这个方法并检查mock
被调用的次数。嘲弄本身很容易;我们只需要将actions._uploadImage
分配给jest.fn()
:
// test/actions.spec.js
it('should call method _uploadImage 3 times', () => {
actions._uploadImage = jest.fn()
})
从现在开始,我们的actions._uploadImage
有一个特殊的魔法属性,叫做mock
,我们已经讨论过了。此对象使我们有机会访问在_uploadImage
方法上进行的调用数:
actions._uploadImage.mock.calls
因此,要断言调用数为 3,我们只需运行以下断言:
expect(actions._uploadImage.mock.calls.length).toEqual(3)
提示
在此处查看有关模拟函数的完整文档:
https://facebook.github.io/jest/docs/mock-functions.html#content
很好,但是我们应该把这种期望称为什么呢?uploadImages
功能是异步的;它回报了一个承诺。不知何故,我们可以潜入未来,聆听承诺决议,并在那里呼唤我们的主张。我们是否应该定义一些回调并在承诺解决后调用它们?不,没必要。只需调用您的函数并在then
回调中运行断言。因此,我们的测试看起来很简单,如下所示:
// test/actions.spec.js
import actions from '~/store/actions'
describe('actions', () => {
describe('uploadImages', () => {
it('should call method _uploadImage 3 times', () => {
actions._uploadImage = jest.fn()
actions.uploadImages({}, [1, 2, 3]).then(() => {
expect(actions._uploadImage.mock.calls.length).toEqual(3)
})
})
})
})
它只是工作!
现在让我们为firebaseApp
创建一个更复杂的模拟。我们如何决定嘲笑什么和如何嘲笑?只需查看代码并检查正在执行的操作。例如,让我们检查一下createNewWorkout
方法:
// store/actions.js
createNewWorkout ({commit, state}, workout) {
//...
let newWorkoutKey = state.workoutsRef.push().key
let updates = {}
updates['/workouts/' + newWorkoutKey] = workout
updates['/user-workouts/' + state.user.uid + '/' + newWorkoutKey] = workout
return firebaseApp.database().ref().update(updates)
}
这是怎么回事?状态的workoutsReference
生成一些新密钥,然后创建名为updates
的对象。此对象包含两个条目,每个条目对应于保存训练对象的 Firebase 数据库资源。
然后使用此对象调用 Firebase 的数据库update
方法。因此,我们必须模拟数据库的update
方法,以便检查调用它的数据。我们还必须以某种方式将此模拟注入到大型 Firebase 应用程序模拟中。创建一个文件夹来保存我们的模拟文件,并将其命名为__mocks__
。将两个文件添加到此目录-firebaseMocks.js
和firebaseAppMock.js
。在firebaseMocks
文件中为update
方法创建一个空函数:
// __mocks__/firebaseMocks.js
export default {
update: () => {}
}
为firebaseApp
对象创建一个模拟,该对象将在其database
方法中调用模拟的update
函数:
// __mocks__/firebaseAppMock.js
import firebaseMocks from './firebaseMocks'
export default {
database: () => {
return {
ref: function () {
return {
update: firebaseMocks.update
}
}
}
}
}
为了测试createNewWorkout
方法,我们将使用jest.mock
函数将 Firebase 对象绑定到其模拟对象。查看有关jest.mock
功能的详细文档:
http://facebook.github.io/jest/docs/en/jest-object.html#jestmockmodulename-工厂选项。
我们需要在导入actions.js
模块之前绑定我们的模拟。这样,它将已经使用模拟对象。因此,我们的导入部分如下所示:
// test/actions.spec.js
import mockFirebaseApp from '~/__mocks__/firebaseAppMock'
jest.mock('~/firebase', () => mockFirebaseApp)
import actions from '~/store/actions'
让我们看看一个训练对象发生了什么,这样我们就知道模拟什么以及如何进行确定性测试。我们有以下几行:
// actions.js
workout.username = state.user.displayName
workout.uid = state.user.uid
因此,我们对 state 对象的模拟必须包含预定义了displayName
和uid
的用户对象。让我们创建它:
let state = {
user: {
displayName: 'Olga',
uid: 1
}}
接下来会发生什么?
workout.date = Date.now()
workout.rate = 0
再一次,我们需要模拟Date.now
对象。让我们像在utils
测试规范中一样:
Date.now = jest.fn(() => 2000)
让我们进一步阅读我们的方法。它包含一行,根据workoutsRef
状态的对象生成newWorkoutKey
变量:
let newWorkoutKey = state.workoutsRef.push().key
让我们在我们的州模拟中模拟workoutsRef
:
let state = {
user: {
displayName: 'Olga',
uid: 1
},
workoutsRef: {
push: function () {
return {
key: 59
}
}
}}
现在我们知道,当我们调用addNewWorkout
方法时,最终它将使用一个包含两个条目的对象调用 Firebase 数据库update
方法,一个包含键/user-workouts/1/59
,另一个包含键/workouts/59
,这两个条目对于workout
对象都是相同的:
{
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
}
所以,首先我们需要创造一个间谍。间谍是一个特殊的函数,它将替换我们绑定它的函数,并监视这个函数发生的任何事情。同样,您不需要为间谍安装任何外部插件或库。Jest 为他们提供了开箱即用的服务。
注
查看官方文件中的笑话间谍:
T0http://facebook.github.io/jest/docs/jest-object.html#jestspyonobject-方法名 T1
所以,我们想监视update
模拟函数。让我们在上面创建一个间谍:
const spy = jest.spyOn(firebaseMocks, 'update')
最后,我们的断言如下所示:
expect(spy).toHaveBeenCalledWith({
'/user-workouts/1/59': {
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
},
'/workouts/59': {
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
}
})
整个测试将如下所示:
describe('createNewWorkout', () => {
it('should call update with', () => {
const spy = jest.spyOn(firebaseMocks, 'update')
Date.now = jest.fn(() => 2000)
let state = {
user: {
displayName: 'Olga',
uid: 1
},
workoutsRef: {
push: function () {
return {
key: 59
}
}
}}
actions.createNewWorkout({state: state}, {})
expect(spy).toHaveBeenCalledWith({
'/user-workouts/1/59': {
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
},
'/workouts/59': {
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
}
})
})
})
现在您知道了如何在不同的 Firebase 方法上创建模拟,以及如何在它们上创建间谍,您可以创建其余的测试规范来测试其余的操作。在chapter9/3/profitoro
文件夹中查看此部分的代码。
让我们继续学习如何使用 Jest 测试我们的 Vue 组件!
让 Jest 与 Vuex、Nuxt.js、Firebase 和 Vue 组件一起工作
测试依赖 Vuex 存储和 Nuxt.js 的 Vue 组件并不是最简单的任务。我们得准备几样东西。
首先,我们必须安装jest-vue-preprocessor
才能告诉 Jest Vue 组件文件是有效的。我们还必须安装babel-preset-stage-2
,否则 Jest 将投诉 ES6spread运营商。运行以下命令:
npm install --save-dev jest-vue-preprocessor babel-preset-stage-2
安装依赖项后,将stage-2
条目添加到.babelrc
文件中:
// .babelrc
{
"presets": ["es2015", "stage-2"]
}
现在我们需要告诉 Jest,它应该对常规 JavaScript 文件使用babel-jest
转换器,对 Vue 文件使用jest-vue-transformer
。为此,将以下内容添加到package.json
文件中的 jest 条目:
// package.json
"jest": {
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
".*\\.(vue)$": "<rootDir>/node_modules/jest-vue-preprocessor"
}
}
我们在组件中使用了一些图像和样式。这可能会导致一些错误,因为 Jest 不知道这些 SVG 文件是关于什么的。让我们在package.json
文件中的moduleNameMapper
Jest 条目中再添加一个条目:
// package.json
"jest": {
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.js",
// ...
}
}
我们这样做是因为我们不想测试图片或 CSS/SCSS 文件。
将styleMock.js
和fileMock.js
添加到__mocks__
目录,内容如下:
// styleMock.js
module.exports = {}
// fileMock.js
module.exports = 'test-file-stub'
有关这方面的更多详细信息,请查看官方文件:https://facebook.github.io/jest/docs/webpack.html 。
为 Vue 和 Vuex 文件添加名称映射器:
// package.json
"jest": {
// ...
"moduleNameMapper": {
// ...
"^vue$": "vue/dist/vue.common.js",
"^vuex$": "vuex/dist/vuex.common.js",
"^~(.*)$": "<rootDir>/$1"
}
},
作为配置的最后一步,我们需要映射 Vue 文件的名称。Jest 很笨,如果我们导入的是没有扩展名的 Vue 文件,它无法理解我们实际上是在导入它。因此,我们必须告诉它,从components
或pages
文件夹导入的任何内容都是 Vue 文件。因此,在这些配置步骤的最后,jest 的moduleNamMapper
条目将如下所示:
"jest": {
//...
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.js",
"^vue$": "vue/dist/vue.common.js",
"^vuex$": "vuex/dist/vuex.common.js",
"^~/(components|pages)(.*)$": "<rootDir>/$1/$2.vue",
"^~(.*)$": "<rootDir>/$1"
}
}
我们现在准备测试我们的组件。您可以在chapter9/4/profitoro
文件夹中找到所有这些配置步骤的最终代码。
使用 Jest 测试 Vue 组件
让我们从测试Header
组件开始。由于它依赖于 Vuex 存储,而 Vuex 存储反过来又高度依赖 Firebase,因此我们必须在将存储注入测试组件之前,执行与刚才测试 Vuex 操作模拟 Firebase 应用程序完全相同的操作。首先创建一个规范文件HeaderComponent.spec.js
并将以下内容粘贴到其import
部分:
import Vue from 'vue'
import mockFirebaseApp from '~/__mocks__/firebaseAppMock'
jest.mock('~/firebase', () => mockFirebaseApp)
import store from '~/store'
import HeaderComponent from '~/components/common/HeaderComponent'
请注意,我们首先模拟 Firebase 应用程序,然后导入我们的存储。现在,为了能够使用模拟存储正确地测试我们的组件,我们需要将存储注入其中。最好的方法是创建一个包含HeaderComponent
的Vue
实例:
// HeaderComponent.spec.js
let $mounted
beforeEach(() => {
$mounted = new Vue({
template: '<header-component ref="headercomponent"></header-component>',
store: store(),
components: {
'header-component': HeaderComponent
}
}).$mount()
})
请注意,我们已将引用绑定到已安装的组件。现在我们可以通过调用$mounted.$refs.headercomponent
来访问头部组件:
let $headerComponent = $mounted.$refs.headercomponent
我们可以在这个组件中测试什么?它实际上没有那么多功能。它有一个方法onLogout
,调用logout
动作并将/
路径推送到组件的$router
属性。因此,我们可以模拟$router
属性,调用onLogout
方法,并检查该属性的值。我们还可以监视logout
动作并检查是否已被调用。因此,我们对组件的onLogout
方法的测试可以如下所示:
// HeaderComponent.spec.js
test('onLogout', () => {
let $headerComponent = $mounted.$refs.headercomponent
$headerComponent.$router = []
const spy = jest.spyOn($headerComponent, 'logout')
$headerComponent.onLogout()
expect(spy).toHaveBeenCalled()
expect($headerComponent.$router).toEqual(['/'])
})
运行测试。您将看到许多与未正确注册 Nuxt 组件相关的错误:
有关 nuxt 链接组件的 Vue 错误
好吧,如果你能容忍这些错误,那就容忍它们吧。否则,请在生产模式下运行测试:
// package.json
"test": "NODE_ENV=production jest"
提示
请注意,如果在生产模式下运行测试,实际上可能会错过一些相关错误。
祝贺您能够通过 Jest 测试依赖 Nuxt、Vuex 和 Firebase 的 Vue 组件!在chapter9/5/profitoro
目录中检查此测试的代码。
用 Jest 进行快照测试
Jest 的最酷的特性之一是快照测试。什么是快照测试?当呈现我们的组件时,它们会生成一些 HTML 标记,对吗?一旦你的应用程序稳定了,新添加的功能都不会破坏已经存在的稳定标记,这一点非常重要,你不这么认为吗?这就是快照测试存在的原因。为某个组件生成快照后,它将保留在快照文件夹中,并在每次测试运行时将输出与现有快照进行比较。创建快照非常简单。挂载组件后,只需调用该组件 HTML 上的期望值toMatchSnapshot
:
let $html = $mounted.$el.outerHTML
expect($html).toMatchSnapshot()
我将对一个测试套件文件中的所有页面运行快照测试。在执行此操作之前,我将模拟 Vuex 存储的 getter,因为有些页面使用未初始化的用户对象,从而导致错误。因此,在我们的__mocks__
文件夹中创建一个文件gettersMock
,并添加以下内容:
// __mocks__/gettersMock.js
export default {
getUser: () => {
return {displayName: 'Olga'}
},
getConfig: () => {
return {
workingPomodoro: 25,
shortBreak: 5,
longBreak: 10,
pomodorosTillLongBreak: 3
}
},
getDisplayName: () => {
return 'Olga'
},
getWorkouts: () => {
return []
},
getTotalPomodoros: () => {
return 10
},
isAuthenticated: () => {
return false
}
}
让我们回到进口。正如我们已经了解到的,Jest 在了解我们的导入内容方面并不是很好,因此它会抱怨相对导入(那些从点开始的导入,例如,在每个components
文件夹中的index.js
文件中)。让我们将所有这些相对导入路径替换为它们的绝对等效路径:
// components/landing/index.js
export {default as Authentication} from '~/components/landing/Authentication'
//...
我还向package.json``jest
条目中的名称映射器条目添加了一个映射:
"jest": {
"moduleNameMapper": {
//...
"^~/(components/)(common|landing|workouts)$": "<rootDir>/$1/$2"
//...
}
}
伟大的创建一个pages.snapshot.spec.js
文件并导入所有必要的模拟对象和所有页面。不要忘记将相应的 mock 绑定到 Vuexgetters
函数和 Firebase 应用程序对象。您的导入部分应如下所示:
// pages.snapshot.spec.js
import Vue from 'vue'
import mockFirebaseApp from '~/__mocks__/firebaseAppMock'
import mockGetters from '~/__mocks__/getterMocks'
jest.mock('~/firebase', () => mockFirebaseApp)
jest.mock('~/store/getters', () => mockGetters)
import store from '~/store'
import IndexPage from '~/pages/index'
import AboutPage from '~/pages/about'
import LoginPage from '~/pages/login'
import PomodoroPage from '~/pages/pomodoro'
import SettingsPage from '~/pages/settings'
import StatisticsPage from '~/pages/statistics'
import WorkoutsPage from '~/pages/workouts'
我们将为每个页面创建一个测试规范。我们将以与绑定Header
组件相同的方式绑定每个页面组件。我们将要测试的组件导出为 Vue 实例的组件,并在创建后装载此 Vue 实例。因此,索引组件绑定将如下所示:
// pages.snapshot.spec.js
let $mounted = new Vue({
template: '<index-page></index-page>',
store: store(),
components: {
'index-page': IndexPage
}
}).$mount()
您现在必须做的唯一一件事就是执行快照预期。因此,索引页面的完整测试规范如下所示:
// pages.snapshot.spec.js
describe('pages', () => {
test('index snapshot', () => {
let $mounted = new Vue({
template: '<index-page></index-page>',
store: store(),
components: {
'index-page': IndexPage
}
}).$mount()
let $html = $mounted.$el.outerHTML
expect($html).toMatchSnapshot()
})
})
对所有页面重复完全相同的步骤。运行测试!检查保险范围。现在我们在谈!实际上,我们已经接触了应用程序的几乎所有组件!看看这个:
几乎我们应用程序的所有组件和文件都出现在覆盖率报告中!
最重要的是测试文件夹中生成的名为__snapshots__
的文件夹,这实际上是快照测试的全部目的。在这里,您将找到您所有页面的所有 HTML 标记的新生成快照。这些快照如下所示:
有 ProFitOro 页面的快照
每一次,当你做一些会影响你的标记的事情时,测试就会失败。如果确实要更新快照,请使用 update 标志运行测试:
npm test -- --u
我发现快照测试是一个非常有趣和激动人心的特性!
提示
提交快照文件非常重要!查看 Jest 官方网站上有关快照测试的详细文档:
‘T0’。https://facebook.github.io/jest/docs/snapshot-testing.html “T1”。
本章的最终代码可在chapter9/6/profitoro
文件夹中找到。
总结
在本章中,我们使用了非常热门的技术来测试我们的 Vue 应用程序。我们使用 Jest 并学习了如何创建模拟、测试组件以及使用它运行快照测试。
在下一章中,我们将最终看到我们的应用程序直播!我们将使用 GoogleFirebase 主机部署它,并提供必要的 CI/CD 工具,以便我们的应用程序在每次推送到主分支时自动部署和测试。你准备好观看你的工作现场直播了吗?走吧!
版权属于:月萌API www.moonapi.com,转载请注明出处