九、调试和测试

在上一章中,我们实现了训练管理页面。我们学习了如何使用 Google Firebase 数据存储机制来存储静态文件,并再次使用实时数据库来存储训练对象。我们使用 Bootstrap 为训练管理页面构建了一个响应性布局,并学习了如何使用 Bootstrap 的模式组件在一个漂亮的弹出窗口中显示每个单独的训练。现在我们有了一个完全负责任的应用程序。多亏了 Bootstrap,我们不需要实现任何特殊功能就可以拥有一个好的移动表示。以下是在手机屏幕上添加新训练的效果:

Test Test and Test

在手机屏幕上添加新训练

这就是我们的 modal 在移动设备上的外观:

Test Test and Test

在移动设备上显示训练模式

现在是测试我们的应用程序的时候了。我们将使用笑话(https://facebook.github.io/jest/ 构建单元测试并运行快照测试。在这一章中,我们将做以下工作:

  • 了解如何配置我们的 Vue.js 应用程序以使用 Jest
  • 使用 Jest 断言测试 Vuex 存储
  • 学习如何使用jest.mockjest.fn方法模拟复杂对象
  • 实现快照 Vue 组件测试了解如何

为什么测试很重要?

我们的 ProFitOro 应用程序工作正常,不是吗?我们已经在浏览器中打开了很多次,我们检查了所有实现的功能,所以它可以正常工作,对吗?是的,没错。现在转到设置页面,尝试将计时器的值更改为奇怪的值。用负值试试看,用大值试试看,用字符串试试看,用空值试试看……你认为这算是一种不错的用户体验吗?

Why is testing important?

你不想在这几分钟内工作,是吗?

你有没有尝试过创建一个奇怪的训练?您是否尝试过在创建时引入一个巨大的训练名称,并查看其显示方式?有数千个角落案例,所有这些都应该仔细测试。我们希望我们的应用程序是可维护的、可靠的,并且能够提供令人惊叹的用户体验。

什么是玩笑?

你知道,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)
 })
})

我们需要一个命令来运行测试。在将调用命令jestpackage.json文件中添加条目"test"

// package.json
"scripts": {
  //...
  "test": "jest"
}

现在如果您运行npm test,您将看到一些错误:

Getting started with Jest

当我们使用 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,测试就通过了!

Getting started with Jest

祝贺您刚刚设置并运行了第一个 Jest 测试!

覆盖范围

单元测试有助于确保他们正在检查的代码片段(单元)能够进行任何可能和不可能的输入。每一个书面的单元测试都覆盖了相应的代码块作为一个整体,保护代码不受未来故障的影响,并使我们对代码的功能性和可维护性感到满意。有不同类型的代码覆盖:语句覆盖、行覆盖、分支覆盖等等。代码覆盖的越多,它就越稳定,我们就越舒适。这就是为什么在编写单元测试时,每次运行时检查代码覆盖率是非常重要的。用 Jest 检查代码覆盖率很容易。您不需要安装任何外部工具或编写额外的配置。只需使用覆盖率标志执行 test 命令:

npm test -- --coverage

您将神奇地看到这个美丽的覆盖输出:

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…如果运行此测试,您将得到一个错误:

Testing utility functions

当然,可怜的 Jest,它不知道我们在 Nuxt 应用程序中使用的别名。它的[T0]符号等于零!幸运的是,它很容易修复。只需将jest条目添加到package.json文件中,其中包含一个名称映射器条目:

// package.json
"jest": {
  "moduleNameMapper": {
    "^~(.*)$": "<rootDir>/$1"
  }
}

现在 Jest 将知道以[T0]开头的所有内容都应该映射到根目录。如果您现在运行npm test -- --coverage,您将看到测试正在通过!

Testing utility functions

映射根目录别名后,测试将毫无问题地运行

然而,代码的覆盖率很低。这是因为我们的 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"
  }
},

耶!考试通过了。检查保险范围。它看起来很漂亮;但是,分支机构的覆盖范围仍然不完善:

Mocking with Jest

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文件,以更直观的方式检查到底发生了什么:

Mocking with Jest

代码覆盖率 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)
  })
})

现在使用覆盖率标志运行测试。天啊!这不是太棒了吗?

Mocking with Jest

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,我们将断言该值已应用于我们的模拟。因此,以下是步骤:

  1. 为状态对象创建模拟:let state = {config: {workingPomodoro: 1}}
  2. 使用新值调用突变:mutations.setWorkingPomodoro(state, 30)
  3. 断言该值已设置为模拟对象: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.jsfirebaseAppMock.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 对象的模拟必须包含预定义了displayNameuid的用户对象。让我们创建它:

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文件中的moduleNameMapperJest 条目中再添加一个条目:

// 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.jsfileMock.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 文件,它无法理解我们实际上是在导入它。因此,我们必须告诉它,从componentspages文件夹导入的任何内容都是 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 应用程序,然后导入我们的存储。现在,为了能够使用模拟存储正确地测试我们的组件,我们需要将存储注入其中。最好的方法是创建一个包含HeaderComponentVue实例:

// 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 组件相关的错误:

Testing Vue components using Jest

有关 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()
  })
})

对所有页面重复完全相同的步骤。运行测试!检查保险范围。现在我们在谈!实际上,我们已经接触了应用程序的几乎所有组件!看看这个:

Snapshot testing with Jest

几乎我们应用程序的所有组件和文件都出现在覆盖率报告中!

最重要的是测试文件夹中生成的名为__snapshots__的文件夹,这实际上是快照测试的全部目的。在这里,您将找到您所有页面的所有 HTML 标记的新生成快照。这些快照如下所示:

Snapshot testing with Jest

有 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 工具,以便我们的应用程序在每次推送到主分支时自动部署和测试。你准备好观看你的工作现场直播了吗?走吧!