安装

选择一个测试运行器

测试运行器 (test runner) 就是运行测试的程序。

主流的 JavaScript 测试运行器有很多,但 Vue Test Utils 都能够支持。它是与测试运行器无关的。

当然在我们选用测试运行器的时候也需要考虑一些事项:功能集合、性能和对单文件组件预编译的支持等。在仔细比对现有的库之后,我们推荐其中的两个测试运行器:

  • Jest 是功能最全的测试运行器。它所需的配置是最少的,默认安装了 JSDOM,内置断言且命令行的用户体验非常好。不过你需要一个能够将单文件组件导入到测试中的预处理器。我们已经创建了 vue-jest 预处理器来处理最常见的单文件组件特性,但仍不是 vue-loader 100% 的功能。

  • mocha-webpack 是一个 webpack + Mocha 的包裹器,同时包含了更顺畅的接口和侦听模式。这些设置的好处在于我们能够通过 webpack + vue-loader 得到完整的单文件组件支持,但这本身是需要很多配置的。

浏览器环境

Vue Test Utils 依赖浏览器环境。技术上讲你可以将其运行在一个真实的浏览器,但是我们并不推荐,因为在不同的平台上都启动真实的浏览器是很复杂的。我们推荐取而代之的是用 JSDOM 在 Node 虚拟浏览器环境运行测试。

Jest 测试运行器自动设置了 JSDOM。对于其它测试运行器来说,你可以在你的测试入口处使用 jsdom-global 手动设置 JSDOM。

npm install --save-dev jsdom jsdom-global

// 在测试的设置 / 入口中
require('jsdom-global')()

测试单文件组件

Vue 的单文件组件在它们运行于 Node 或浏览器之前是需要预编译的。我们推荐两种方式完成编译:通过一个 Jest 预编译器,或直接使用 webpack。

vue-jest 预处理器支持基本的单文件组件功能,但是目前还不能处理样式块和自定义块,这些都只在 vue-loader 中支持。如果你依赖这些功能或其它 webpack 特有的配置项,那么你需要基于 webpack + vue-loader 进行设置。

对于不同的设置方式请移步下面的教程:

相关资料

用 Jest 测试单文件组件

我们在 GitHub 上放有一个关于这些设置的示例工程。

Jest 是一个由 Facebook 开发的测试运行器,致力于提供一个“bettery-included”单元测试解决方案。你可以在其官方文档学习到更多 Jest 的知识。

安装 Jest

我们假定你在一开始已经安装并配置好了 webpack、vue-loader 和 Babel——例如通过 vue-cli 创建了 webpack-simple 模板脚手架。

另一种方案,如果你在使用 Vue CLI 构建你的工程也可以使用 cli-plugin-unit-jest 插件来运行 Jest 测试。

跳到[在 Jest 中处理单文件组件](#在 Jest 中处理单文件组件)并遵循其指示以处理你的 Vue 单文件组件。

如果你在使用 Babel 进行转译 (这是创建新工程的默认配置),你也需要在项目中的 package.json 配置 Jest 的 JavaScript 转换,参考为 Jest 配置 Babel,不过这样你应该跳过该章节其余的部分。

我们要做的第一件事就是安装 Jest 和 Vue Test Utils:

$ npm install --save-dev jest @vue/test-utils

然后我们需要在 package.json 中定义一个单元测试的脚本。

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

在 Jest 中处理单文件组件

为了告诉 Jest 如何处理 *.vue 文件,我们需要安装和配置 vue-jest 预处理器:

npm install --save-dev vue-jest

接下来在 package.json 中创建一个 jest 块:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      // 告诉 Jest 处理 `*.vue` 文件
      "vue"
    ],
    "transform": {
      // 用 `vue-jest` 处理 `*.vue` 文件
      ".*\\.(vue)$": "vue-jest"
    }
  }
}

注意:vue-jest 目前并不支持 vue-loader 所有的功能,比如自定义块和样式加载。额外的,诸如代码分隔等 webpack 特有的功能也是不支持的。如果要使用这些不支持的特性,你需要用 Mocha 取代 Jest 来运行你的测试,同时用 webpack 来编译你的组件。想知道如何起步,请阅读教程里的用 Mocha + webpack 测试单文件组件

**注意:**如果你使用了 Babel 7 或更高版本,你需要在你的 devDependencies 里添加 babel-bridge ($ npm install --save-dev babel-core@^7.0.0-bridge.0)。

处理 webpack 别名

如果你在 webpack 中配置了别名解析,比如把 @ 设置为 /src 的别名,那么你也需要用 moduleNameMapper 选项为 Jest 增加一个匹配配置:

{
  // ...
  "jest": {
    // ...
    // 支持源代码中相同的 `@` -> `src` 别名
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    }
  }
}

为 Jest 配置 Babel

尽管最新版本的 Node 已经支持绝大多数的 ES2015 特性,你可能仍然想要在你的测试中使用 ES modules 语法和 stage-x 的特性。为此我们需要安装 babel-jest

npm install --save-dev babel-jest

接下来,我们需要在 package.jsonjest.transform 里添加一个入口,来告诉 Jest 用 babel-jest 处理 JavaScript 测试文件:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // 用 `babel-jest` 处理 js
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
    }
    // ...
  }
}

默认情况下,babel-jest 会在其安装完毕后自动进行配置。尽管如此,因为我们已经显性的添加了对 *.vue 文件的转换,所以现在我们也需要显性的配置 babel-jest

我们假设 webpack 使用了 babel-preset-env,这时默认的 Babel 配置会关闭 ES modules 的转译,因为 webpack 已经可以处理 ES modules 了。然而,我们还是需要为我们的测试而开启它,因为 Jest 的测试用例会直接运行在 Node 上。

同样的,我们可以告诉 babel-preset-env 面向我们使用的 Node 版本。这样做会跳过转译不必要的特性使得测试启动更快。

为了仅在测试时应用这些选项,可以把它们放到一个独立的 env.test 配置项中 (这会被 babel-jest 自动获取)。

.babelrc 文件示例:

{
  "presets": [["env", { "modules": false }]],
  "env": {
    "test": {
      "presets": [["env", { "targets": { "node": "current" } }]]
    }
  }
}

放置测试文件

默认情况下,Jest 将会递归的找到整个工程里所有 .spec.js.test.js 扩展名的文件。如果这不符合你的需求,你也可以在 package.json 里的配置段落中改变它的 testRegex

Jest 推荐你在被测试代码的所在目录下创建一个 __tests__ 目录,但你也可以为你的测试文件随意设计自己习惯的文件结构。不过要当心 Jest 会为快照测试在临近测试文件的地方创建一个 __snapshots__ 目录。

测试覆盖率

Jest 可以被用来生成多种格式的测试覆盖率报告。以下是一个简单的起步的例子:

扩展你的 jest 配置 (通常在 package.jsonjest.config.js 中) 的 collectCoverage 选项,然后添加 collectCoverageFrom 数组来定义需要收集测试覆盖率信息的文件。

{
  "jest": {
    // ...
    "collectCoverage": true,
    "collectCoverageFrom": ["**/*.{js,vue}", "!**/node_modules/**"]
  }
}

这样就会开启默认格式的测试覆盖率报告。你可以通过 coverageReporters 选项来定制它们。

{
  "jest": {
    // ...
    "coverageReporters": ["html", "text-summary"]
  }
}

更多文档内容请移步至 Jest 配置文档,在那里你可以找到覆盖率阀值、目标输出目录等选项。

测试规范示例

如果你已经熟悉了 Jasmine,你应该很适应 Jest 的断言 API

import { mount } from '@vue/test-utils'
import Component from './component'

describe('Component', () => {
  test('is a Vue instance', () => {
    const wrapper = mount(Component)
    expect(wrapper.isVueInstance()).toBeTruthy()
  })
})

快照测试

当你用 Vue Test Utils 挂载一个组件时,你可以访问到 HTML 根元素。这可以保存为一个快照为 Jest 快照测试所用。

test('renders correctly', () => {
  const wrapper = mount(Component)
  expect(wrapper.element).toMatchSnapshot()
})

我们可以通过一个自定义的序列化工具改进被保存的快照:

npm install --save-dev jest-serializer-vue

然后在 package.json 中配置它:

{
  // ...
  "jest": {
    // ...
    // 快照的序列化工具
    "snapshotSerializers": ["jest-serializer-vue"]
  }
}

相关资料

用 Mocha 和 webpack 测试单文件组件

我们在 GitHub 上放有一个关于这些设置的示例工程。

另一个测试单文件组件的策略是通过 webpack 编译所有的测试文件然后在测试运行器中运行。这样做的好处是可以完全支持所有 webpack 和 vue-loader 的功能,所以我们不必对我们的源代码做任何妥协。

从技术的角度讲,你可以使用任何喜欢的测试运行器并把所有的东西都手动串联起来,但是我们已经找到了 mochapack 能够为这项特殊任务提供非常流畅的体验。

设置 mochapack

我们假定你在一开始已经安装并配置好了 webpack、vue-loader 和 Babel——例如通过 vue-cli 创建了 webpack-simple 模板脚手架。

首先要做的是安装测试依赖:

npm install --save-dev @vue/test-utils mocha mochapack

接下来我们需要在 package.json 中定义一个测试脚本。

// package.json
{
  "scripts": {
    "test": "mochapack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
  }
}

这里有一些注意事项:

  • --webpack-config 标识指定了该测试使用的 webpack 配置文件。在大多数情况下该配置会在其实际项目的配置文件基础上做一些小的调整。我们晚些时候会再聊到这一点。

  • --require 标识确保了文件 test/setup.js 会在任何测试之前运行,这样我们可以在该文件中设置测试所需的全局环境。

  • 最后一个参数是该测试包所涵盖的所有测试文件的聚合。

提取 webpack 配置

暴露 NPM 依赖

在测试中我们很可能会导入一些 NPM 依赖——这里面的有些模块可能没有针对浏览器的场景编写,也不适合被 webpack 打包。另一个考虑是为了尽可能的将依赖外置以提升测试的启动速度。我们可以通过 webpack-node-externals 外置所有的 NPM 依赖:

// webpack.config.js
const nodeExternals = require('webpack-node-externals')

module.exports = {
  // ...
  externals: [nodeExternals()]
}

源码表

源码表在 mochapack 中需要通过内联的方式获取。推荐配置为:

module.exports = {
  // ...
  devtool: 'inline-cheap-module-source-map'
}

如果是在 IDE 中调试,我们推荐添加以下配置:

module.exports = {
  // ...
  output: {
    // ...
    // 在源码表中使用绝对路径 (对于在 IDE 中调试时很重要)
    devtoolModuleFilenameTemplate: '[absolute-resource-path]',
    devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]?[hash]'
  }
}

设置浏览器环境

Vue Test Utils 需要在浏览器环境中运行。我们可以在 Node 中使用 jsdom-global 进行模拟:

npm install --save-dev jsdom jsdom-global

然后在 test/setup.js 中写入:

require('jsdom-global')()

这行代码会在 Node 中添加一个浏览器环境,这样 Vue Test Utils 就可以正确运行了。

选用一个断言库

Chai 是一个流行的断言库,经常和 Mocha 配合使用。你可能也想把 Sinon 用于创建间谍和存根。

另外你也可以使用 expect,它现在是 Jest 的一部分,且在 Jest 文档里暴露了完全相同的 API

这里我们将使用 expect 且令其全局可用,这样我们就不需要在每个测试文件里导入它了:

npm install --save-dev expect

然后在 test/setup.js 中编写:

require('jsdom-global')()

global.expect = require('expect')

为测试优化 Babel

注意我们使用了 babel-loader 来处理 JavaScript。如果你在你的应用中通过 .babelrc 文件使用了 Babel,那么你就已经算是把它配置好了。这里 babel-loader 将会自动使用相同的配置文件。

有一件事值得注意,如果你使用了 Node 6+,它已经支持了主要的 ES2015 特性,那么你可以配置一个独立的 Babel 环境选项,只转译该 Node 版本中不支持的特性 (比如 stage-2 或 flow 语法支持等)。

添加一个测试

src 目录中创建一个名为 Counter.vue 的文件:

<template>
  <div>
    {{ count }}
    <button @click="increment">自增</button>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        count: 0
      }
    },

    methods: {
      increment() {
        this.count++
      }
    }
  }
</script>

然后创建一个名为 test/Counter.spec.js 的测试文件并写入如下代码:

import Vue from 'vue'
import { shallowMount } from '@vue/test-utils'
import Counter from '../src/Counter.vue'

describe('Counter.vue', () => {
  it('increments count when button is clicked', async () => {
    const wrapper = shallowMount(Counter)
    wrapper.find('button').trigger('click')
    await Vue.nextTick()
    expect(wrapper.find('div').text()).toMatch('1')
  })
})

现在我们运行测试:

npm run test

喔,我们的测试运行起来了!

测试覆盖率

如果想设置 mochapack 的测试覆盖率,请参照 mochapack 测试覆盖率指南

相关资料

用 Karma 测试单文件组件

我们在 GitHub 上放有一个该设置的示例工程。

Karma 是一个启动浏览器运行测试并生成报告的测试运行器。我们会使用 Mocha 框架撰写测试,同时使用 chai 作为断言库。

设置 Mocha

我们会假设你一开始已经正确配置好了 webpack、vue-loader 和 Babel——例如通过 vue-cliwebpack-simple 模板搭建起来。

第一件要做的事是安装测试依赖:

npm install --save-dev @vue/test-utils karma karma-chrome-launcher karma-mocha karma-sourcemap-loader karma-spec-reporter karma-webpack mocha

接下来我们需要在 package.json 定义一个测试脚本。

// package.json
{
  "scripts": {
    "test": "karma start --single-run"
  }
}
  • --single-run 标识告诉了 Karma 一次性运行该测试套件。

Karma 配置

在项目的主目录创建一个 karma.conf.js 文件:

// karma.conf.js

var webpackConfig = require('./webpack.config.js')

module.exports = function(config) {
  config.set({
    frameworks: ['mocha'],

    files: ['test/**/*.spec.js'],

    preprocessors: {
      '**/*.spec.js': ['webpack', 'sourcemap']
    },

    webpack: webpackConfig,

    reporters: ['spec'],

    browsers: ['Chrome']
  })
}

这个文件用来配置 Karma。

我们需要用 webpack 预处理文件。为此,我们将 webpack 添加为预处理器,并引入我们的 webpack 配置。我们可以在项目基础中使用该 webpack 配置文件而无需任何修改。

在我们的配置中,我们在 Chrome 中运行测试。如果想添加其它浏览器,可查阅Karma 文档的浏览器章节

选用一个断言库

Chai 是一个流行的常配合 Mocha 使用的断言库。你也可以选用 Sinon 来创建监视和存根。

我们可以安装 karma-chai 插件以在我们的测试中使用 chai

npm install --save-dev karma-chai

添加一个测试

src 中创建一个名为 Counter.vue 的文件:

<template>
  <div>
    {{ count }}
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        count: 0
      }
    },

    methods: {
      increment() {
        this.count++
      }
    }
  }
</script>

然后添加一个名为 test/Counter.spec.js 的测试文件,并写入如下代码:

import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import Counter from '../src/Counter.vue'

describe('Counter.vue', () => {
  it('increments count when button is clicked', () => {
    const wrapper = shallowMount(Counter)
    wrapper.find('button').trigger('click')
    expect(wrapper.find('div').text()).contains('1')
  })
})

接下来我们可以运行测试:

npm run test

Woohoo,我们的测试跑起来了!

覆盖率

我们可以使用 karma-coverage 插件来设置 Karma 的代码覆盖率。

默认情况下,karma-coverage 不会使用 source map 来对照覆盖率报告。所以我们需要使用 babel-plugin-istanbul 来确认正确匹配的覆盖率。

安装 karma-coveragebabel-plugin-istanbulcross-env

npm install --save-dev karma-coverage cross-env

我们会使用 cross-env 来设置一个 NODE_ENV 环境变量。这样我们就可以在编译测试的时候使用 babel-plugin-istanbul——因为我们不想在生产环境下引入 babel-plugin-istanbul

npm install --save-dev babel-plugin-istanbul

更新你的 .babelrc 文件,在因测试设置了 NODE_ENV 时使用 babel-plugin-istanbul

{
  "presets": [["env", { "modules": false }], "stage-3"],
  "env": {
    "test": {
      "plugins": ["istanbul"]
    }
  }
}

现在更新 karma.conf.js 文件来进行覆盖率测试。添加 coveragereporters 数组,并添加一个 coverageReporter 字段:

// karma.conf.js

module.exports = function(config) {
  config.set({
    // ...

    reporters: ['spec', 'coverage'],

    coverageReporter: {
      dir: './coverage',
      reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }]
    }
  })
}

然后更新 test 脚本来设置 NODE_ENV

// package.json
{
  "scripts": {
    "test": "cross-env NODE_ENV=test karma start --single-run"
  }
}

相关资料

不经过构建而使用

在我们习惯于使用工具,诸如 webpack 打包 Vue 应用、Vue Loader 处理单文件组件、Jest 转写富于表现力测试的同时,使用 Vue Test Utils 其实不需要引入这么多。除了本库之外,使用 Vue Test Utils 最低要求是:

  • Vue
  • vue-template-compiler
  • DOM (可以是 Node 环境下的 jsdom 或真实浏览器)

在这个示例中,我们会展示如何仅使用上述最小化的依赖撰写一个简单的测试。最终的代码可以在这里找到。

安装依赖

我们需要安装一些依赖,如上所述:npm install vue vue-template-compiler jsdom jsdom-global @vue/test-utils。该示例不需要测试运行器或打包工具。

引入库

现在我们需要引入这些库。这里有一些轻微的注意事项并解释在了如下代码片段的注释中。

// `jsdom-global` 必须在 `@vue/test-utils` 之前被引入,
// 因为 `@vue/test-utils` 需要一个已经存在的 DOM 环境 (真实的 DOM 或 JSDOM)
require('jsdom-global')()

const assert = require('assert')

const Vue = require('vue')
const VueTestUtils = require('@vue/test-utils')

如注释中所述,jsdom-global 必须优先于 @vue/test-utils 被引入。因为 Vue Test Utils 需要一个 DOM 环境来渲染 Vue 组件。如果你在一个真实的浏览器中运行测试,你就完全不需要 jsdom 了。Vue 也必须在 @vue/test-utils 之前被引入,原因很明显——Vue Test Utils 也需要可以正常工作。我们还从 Node 标准库中引入了 assert。一般我们会使用测试运行器提供的方法,通常形如 expect(...).toEqual(...),但在这个示例中 assert 就可以用来做这件事。

撰写一个测试

现在万事俱备,我们需要一个待测试的组件。为了保持简介,我们之渲染一些文本并断言该组件的渲染结果。

const App = Vue.component('app', {
  data() {
    return {
      msg: 'Hello Vue Test Utils'
    }
  },

  template: `
    <div>{{ msg }}</div>
  `
})

const wrapper = VueTestUtils.shallowMount(App)

assert.strictEqual('Hello Vue Test Utils', wrapper.text())

如你所见的一样简单。不过因为我们没有构建步骤,我们无法使用单文件组件。但没有什么可以阻止我们通过 <script> 标签从 CDN 引入并以同样的方式使用 Vue。