单元测试

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

测试用例场景:

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

0. 相关概念

0.1 冒烟测试

「冒烟测试」,就是完成一个新版本开发之后,对该版本的最基本功能进行测试,保证基本的功能和流程能走通。严格按照冒烟测试的流程和标准来执行,一旦基本功能点不通过,我们将不予受理测试任务。

版本测试必须要有这样一个冒烟测试的过程来约束开发人员,让他们洁身自好,真正负责任的去做产品,帮助开发人员提高自身的质量意识,从而可以更有效的提高产品的质量,和版本发布速度。也许说,效率是要靠团队来推动的。

0.2 回归测试

「回归测试」的定义就是修改代码之后,是否会对原来的代码或者功能造成影响。

「回归测试」是只重复以前的全部或部分相同的功能测试,最常见的就是版本迭代过程中,查看之前版本的功能是否正常,是否引入了其他的问题。

新加入的测试模块会影响哪些之前的功能,需要进行版本之间的兼容性测试。

0.3 测试用例等级

一般我们会把用例标记为三个标准:P0、P1、P2:

  • P0:主要功能流程的用例,需要满足主要的需求、功能流程,这部分用例也是”冒烟测试”过程中需要执行的用例;
  • P1:非主要功能用例,一般是对某个测试点进行的补充或详细说明,UI 测试等。并不是开发必须要执行的,但是在开发提测之后必须要详细测试执行;
  • P2:一些边缘的测试用例,不影响产品使用和主要的功能,比如兼容测试等。

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

1.8 手工 mock

当一些 js 或者组件文件中包含了一些浏览器运行时才支持的代码,那么在 jest 跑单元测试的时候会报错的。例如:

// src/util/cookie.js
// 以下这段代码在跑单元测试的时候是会报错的
const _domain_ = document.domain.split('.')[0];

export default {
    get () {
        // ...
    },

    set () {
        // ...
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/util/index.js
import cookie from './cookie';
// ...

export default {
    // ...
    cookie,
};
1
2
3
4
5
6
7
8

现在基于以上两个文件,我想针对 src/util/index.js 写单元测试文件,但是该文件的依赖 src/util/cookie.js 中有让 jest 报错的代码,怎么办呢?

两种方法:

  1. 编写能够支持单测的代码,也就是有点 TDD 味道,就是写出来的代码,需要支持单测;
  2. 手工 mock 一下,也就是说导入 src/util/cookie.js 的时候其实导入自己另外创建的文件,我们来详细说说这种方法。

首先,我们知道 cookie.js 的存放位置是 src/util/,那么我们就要在该路径下,创建一个 __mocks__ 文件夹,即:src/util/__mocks__,然后在里面创建一个 cookie.js 文件,写入 mock 内容,即可。

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

参考链接