单元测试

在工作中开始接到要写单元测视用例的需求了,接着机会,总结沉淀,最好能为其他的项目实现开箱即用。

测试用例场景:

  • Vue + Webpack 项目
  • Jest 测试框架
    • 至于用什么框架,其实一开始没太重要,但是入门曲线应该选择平缓一些的,先搭好框架,跑一遍
    • 之后,什么框架只是大同小异

1. 搭建测试框架

1.1 安装 Jest

在项目根目录下安装 Jest 和 Vue Test Utils:

$ npm i -D jest @vue/test-utils
1

然后在 package.json 中添加测试的 npm scripts 命令:

// package.json
{
    // ...
    "scripts": {
        // ...
        "test": "jest"
    }
} 
1
2
3
4
5
6
7
8

1.2 在 Jest 处理测试单文件组件

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

$ npm i -D vue-jest
1

然后在根目录下新建一份配置文件 jest.config.js

// jest.config.js

// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html

module.exports = {
    clearMocks: true,
    // 如果你在文件中引入了其他不带后缀的模块
    // 则 Jest 会默认按这些后缀去查找
    // 优先级为从左到右
    moduleFileExtensions: [
        "js",
        "json",
        "jsx",
        "ts",
        "tsx",
        "node",
        "vue"
    ],
    // 忽略路径
    testPathIgnorePatterns: [
        "/node_modules/",
    ],
    // 针对不同类型的文件使用不同的转换器
    transform: {
        // 用 `vue-jest` 处理 `*.vue` 文件
        ".*\\.(vue)$": "vue-jest",
    },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

同样地,你也可以不用新建上面的配置文件,而是将配置写到 package.json 中的 jest 字段中:

// package.json
{
    // ...
    "jest": {
        // ...
    }
}
1
2
3
4
5
6
7

1.3 处理 Webpack 的别名

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

// jest.config.js

module.exports = {
    // ...
    // 支持源代码中相同的 `@` -> `src` 别名
    moduleNameMapper: {
      "^@/(.*)$": "<rootDir>/src/$1"
    }
};
1
2
3
4
5
6
7
8
9

1.4 为 Jest 配置 Babel

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

$ npm i -D babel-jest
1

然后在 jest.config.jstransform 里添加一个入口,来告诉 Jest 用 babel-jest 处理 JavaScript 测试文件:

// jest.config.js

module.exports = {
    // ...
    transform: {
        // ...
        // 用 `babel-jest` 处理 js
        "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
        // 或者
        // "^.+\\.js$": "babel-jest"
    }
};
1
2
3
4
5
6
7
8
9
10
11
12

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

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

// .babelrc
{
    "presets": [
        [ "env", { "modules": false } ]
    ],
    "env": {
        "test": {
            "presets": [
                [ "env", { "targets": { "node": "current" } } ]
            ]
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

1.5 创建测试文件夹目录

默认情况下,Jest 会递归查找整个工程里面的 *.spec.js 或者 *.test.js 扩展名的测试文件。

因此我们在根目录下创建一个 __tests__ 目录,在里面放置测试文件。

不过要当心 Jest 会为快照测试在临近测试文件的地方创建一个 __snapshots__ 目录。

1.6 测试覆盖率

Jest 可以被用来生成多种格式的测试覆盖率报告。

jest.config.js 配置文件中:

module.exports = {
    // 开启收集测试覆盖率
    "collectCoverage": true,
    // 定义需要收集测试覆盖率信息的文件
    "collectCoverageFrom": ["**/*.{js,vue}", "!**/node_modules/**"],
    // 可以重定制默认个事的测试覆盖率报告
    // 更多:https://jestjs.io/docs/zh-Hans/configuration#collectcoverage-boolean
    "coverageReporters": ["html", "text-summary"]
};
1
2
3
4
5
6
7
8
9

1.7 快照测试

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

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

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

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

然后在 jest.config.js 配置文件中:

module.exports = {
    // ...
    // 快照的序列化工具
    "snapshotSerializers": ["jest-serializer-vue"]
};
1
2
3
4
5

2. 总结

上面说了一堆,总结一下,其实很清晰。

安装:

$ npm i -D jest @vue/test-utils
$ npm i -D vue-jest
$ npm i -D babel-jest
$ npm i -D jest-serializer-vue
1
2
3
4

根目录新建配置文件 jest.config.js

// jest.config.js
module.exports = {
    clearMocks: true,
    // 如果你在文件中引入了其他不带后缀的模块
    // 则 Jest 会默认按这些后缀去查找
    // 优先级为从左到右
    moduleFileExtensions: [
        "js",
        "json",
        "jsx",
        "ts",
        "tsx",
        "node",
        "vue"
    ],
    // 忽略路径
    testPathIgnorePatterns: [
        "/node_modules/",
    ],
    // 针对不同类型的文件使用不同的转换器
    transform: {
        "^.+\\.js$": "babel-jest",
        // 用 `vue-jest` 处理 `*.vue` 文件
        ".*\\.(vue)$": "vue-jest",
    },
    // 支持源代码中相同的 `@` -> `src` 别名
    moduleNameMapper: {
      "^@/(.*)$": "<rootDir>/src/$1"
    },
    // 开启收集测试覆盖率
    "collectCoverage": true,
    // 定义需要收集测试覆盖率信息的文件
    "collectCoverageFrom": ["**/*.{js,vue}", "!**/node_modules/**"],
    // 可以重定制默认个事的测试覆盖率报告
    // 更多:https://jestjs.io/docs/zh-Hans/configuration#collectcoverage-boolean
    "coverageReporters": ["html", "text-summary"],
    // 快照的序列化工具
    "snapshotSerializers": ["jest-serializer-vue"]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

补全 .babelrc 配置文件:

// .babelrc
{
    "presets": [
        [ "env", { "modules": false } ]
    ],
    "env": {
        "test": {
            "presets": [
                [ "env", { "targets": { "node": "current" } } ]
            ]
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

3. 注意问题

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

4. 疑问解答

文件命名

问题 1:看了一下 test 文件下的文件,有点疑惑,为什么有些文件是以 .spec 做后缀名,而有些文件是以 .test 做后缀名,两者分别代表什么含义呢?

问题 2:另外,为什么会同时存在 .test.js.spec.js

**问题 1 解答: ** 这算是一个不成文的约定吧,比如源文件是 button.vue 或者 button.js,那么它的单元测试文件就会是 button.test.js 或者 button.spec.js 或者 button.unit.js

  • button.test 比较好理解,就是测试的意思
  • button.spec 中的 spec 是 specification 的缩写,表示规格,也就是 button 应该满足的规则,所以 button.spec.js 表示对 button 应该满足的规则。
  • button.unit 中的 unit 就是单元测试的意思

**问题 2 解答: ** 因为 .test.js 是旧的测试文件(没有使用 vue-test-utils), .spec.js 是新的测试文件(使用了 vue-test-utils

参考链接