Gu's Blog

单元测试

经常听到自动化测试,单元测试这种概念,一直以来我都处于一种概念性的了解。其实目前我们大环境下,还是很多公司对自动化测试这块没有那么重视,很多都还处于人工测试的阶段。改一个公用函数,我们生怕影响到所有的业务代码,靠人力,我们无法说服自己测试全面了。那么,自动化测试可以做到哪种地步呢?


什么是单元测试

我们有很多测试方法,单元测试、集成测试、端到端测试、可视化测试、功能测试等等等等。不同的人对不同的测试方法有不同的理解。
业界有这样的一种说法,单元测试和端到端测试是测试的两极,其余的测试都在在这两个测试范围中间。
这里我们只聊聊单元测试。那么,什么叫单元测试呢?顾名思义,就是以代码单元为单位的测试。这个代码单元可以是一个函数,一个对象,或者一个类。

其实在自动化之前,我们也在人工做单元测试。比如,本人写了如下一个函数:

1
2
3
4
给url添加参数
function appendUrl(url, params){
..... //具体实现
}

校验这个函数写的对不对,常用操作就会在工作台先跑一下示例,看返回对不对。分别测一下:
1、没有输入url或者url格式不正确
2、参数格式校验是否正确
3、输入的url本身有/无参数 返回是否正确
4、输入的url带hash 返回是否正确
… (你还可以有更多)
以上呢,就是我们平时所谓的测试用例。
在没有所谓的自动化测试之前呢,我们一般都是通过人工跑示例测试的。而且,下次换个人开发,更改了这个函数,他自己默默又把这个测试用例都来一遍。现在我们引入单元测试,一个呢让测试更自动化更方便;二呢在不断迭代的过程中,以前的测试用例即可以保证新更改的正确性,又可以方便后面的开发更明了地理解代码实现了什么功能。

那么接下来,我将用一个简单的例子讲讲,怎么做的这个单元测试。


测试方案

简单测试工具介绍

现在市场上有很多测试工具:

  • 测试框架: MochaJasmineJest等等。提供一些清晰明了的语法对测试用例进行分组,描述,测试用例通过, 测试失败了(为什么)等等。Mocha对断言库和工作没做任何限制,比较灵活,相对比较成熟;Jasmine提供了自带的断言;Jest是facebook出品的,React官方推荐的单元测试框架。
  • 断言库: Should.jsChai等等,以及node自带的assert
  • 代码覆盖率: istanbul
  • 测试平台: Karma

目录结构

这是我对测试目录的初步规划

1
2
3
4
5
6
7
8
9
your-app
├── src/ 被测试代码
├── test/ 测试相关
│ ├── converage/ 测试报告
│ ├── unit/ 单元测试相关
| │ ├── index.js 测试入口文件
│ │ └── specs/ 单元测试用例
│ └── ... 添加其他测试
└── karma.config.js karma相关配置

当然业界也会有别的目录规划,直接把单元测试和被测试代码写在一个目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
your-app
├── src/
│ ├── cpt1
│ │ ├── index.js 被测试代码
│ │ └── index.spec.js 测试用例
│ ├── cpt2
│ │ ├── index.js 被测试代码
│ │ └── index.spec.js 测试用例
│ └── ... 其他组件
├── test/ 测试相关
│ ├── converage/ 测试报告
│ └── index.js 测试入口文件
└── karma.config.js karma相关配置

不管哪种目录规划,我们都是有一个测试入口文件,作为执行测试用例的Setup。
拿第一种举例,入口文件:

1
2
3
//导入测试用例,具体怎么匹配看具体情况
const testsContext = require.context(".", true, /\.spec$/)
testsContext.keys().forEach(testsContext)

karma配置

我们可以全局安装下karma-cli来初始化测试环境:

1
npm install -g karma-cli

初始化测试环境:

1
2
3
4
运行
karma init
我选择了Mocha测试框架 + PhantomJS运行环境 这样的组合(这个看个人哈)
这样,初步的 karma.conf.js文件生成了

PhantomJS是什么呢?如果有兴趣可以看看这里

提供一个浏览器环境的命令行接口,你可以把它看作一个“虚拟浏览器”,除了不能浏览,其他与正常浏览器一样。它的内核是WebKit引擎,不提供图形界面,只能在命令行下使用,我们可以用它完成一些特殊的用途

搭建测试环境需要的包:

1
2
npm install -D karma mocha karma-mocha
npm install -D karma-phantomjs-launcher phantomjs-prebuilt

配置项

简单的karma.conf.js配置如下:

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
module.exports = function(config) {
config.set({
//测试框架
frameworks: ['mocha'],
// 测试用例入口文件,入口文件中引入所有测试用例
files: [
'./test/unit/index.js'
],
// 为选定脚本指定前处理器
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// 报告配置
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// 日志级别
// 取值: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// 启用/禁用监视文件变化重新执行测试的功能
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// 选择运行的平台,这里只以PhantomJS做基础配置
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['PhantomJS'],
//引用的插件, 如果默写,karma会默认加载 karma-* 的npm包
plugins: ['karma-mocha', 'karma-phantomjs-launcher'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}


测试用例怎么写

基本测试用例

先来一个最简单的测试用例。
在src/添加一个index.js

1
2
3
4
5
6
7
module.exports = {
getUrlHash: (url) => {
url = url || window.location.href;
let arr = url.split('#');
return arr[1] ? arr[1]: '';
}
}

在spec/添加一个简单的测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Url from '../../../src/url.js';
const assert = require('assert');
describe('test getUrlHash', function () {
it('url 没有hash', function(){
assert.equal(Url.getUrlHash('https://www.you.com'), '')
})
it('url有hash没有参数', function(){
assert.equal(Url.getUrlHash('https://www.you.com#hash'), 'hash')
})
it('url有hash且有参数', function(){
assert.equal(Url.getUrlHash('https://www.you.com?param=2222#hash'), 'hash')
})
})

然后我们在package.json中配置下test命令:

1
2
3
"scripts": {
"test": "karma start"
}

运行下npm run test, 发现有报错。
因为我们的开发源码和测试用例都用了es6的语法,所以需要用Webpack处理一下。

1
2
npm install -D webpack karma-webpack
npm install -D babel-loader @babel/core @babel/preset-env

karma.conf.js中增加webpack的配置:

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
preprocessors: {
'./test/unit/index.js': ['webpack']
},
...
plugins: ['karma-webpack', 'karma-mocha', 'karma-phantomjs-launcher'],
...
webpack: {
module: {
rules:[
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
query: {
presets: ['@babel/preset-env']
}
}
}
]
}
},
webpackMiddleware: {
noInfo: true
}

再运行一下,我们就能看到以下界面啦
测试用例示意图


测试覆盖率和报告

在知道了测试结果是否成功之后,我们得有个清晰的认知啊。比如,是否每种情况我们都覆盖到了?这时候,我们就要引入覆盖率工具了。
什么叫代码覆盖率呢?

代码覆盖率描述的是代码被测试的比例和程度。简单地说,光是编写用例来测试代码,而不检查究竟哪些代码被测试过,哪些还没有测试过。

代码覆盖率有四个指标:

  • 行覆盖率(line coverage):每一行执行情况
  • 函数覆盖率(function coverage):每个函数调用情况
  • 分支覆盖率(branch coverage):条件代码块执行情况
  • 语句覆盖率(statement coverage):语句执行情况

示例

引入覆盖率工具包

1
npm install -D istanbul-instrumenter-loader karma-coverage-istanbul-reporter

如果想用别的报告配置,可以下载对应的插件(mocha-reporter之类)
做相关配置,最终版karma.conf.js如下:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
const path = require('path');
module.exports = function (config) {
config.set({
//测试框架
frameworks: ['mocha'],
// 测试用例入口文件,入口文件中引入所有测试用例
files: [
'./test/unit/index.js'
],
// 为选定脚本指定前处理器
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'./test/unit/index.js': ['webpack']
},
// 报告配置
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'coverage-istanbul'],
// 配置代码覆盖率插件
coverageIstanbulReporter: {
// 以什么格式, 这里设置了输出 html文件 ,info文件 ,及控制台
reports: ['html', 'lcovonly', 'text-summary'],
// 将文件输出路径定位
dir: path.join(__dirname, '/test/coverage'),
// 修正 webpack 路径
fixWebpackSourcePaths: true,
// 将生成的html放到./coverage/html/下
'report-config': {
html: {
subdir: 'html'
}
}
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// 日志级别
// 取值: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// 启用/禁用监视文件变化重新执行测试的功能
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// 选择运行的平台,这里只以PhantomJS做基础配置
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
//引用的插件, 如果默写,karma会默认加载 karma-* 的npm包
plugins: ['karma-webpack', 'karma-mocha', 'karma-phantomjs-launcher', 'karma-coverage-istanbul-reporter'],
webpack: {
module: {
rules: [{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
query: {
presets: ['@babel/preset-env']
}
}
},
// 覆盖率只针对源代码
{
test: /\.jsx?$/,
use: { loader: 'istanbul-instrumenter-loader' },
include: path.resolve('src/')
}]
},
devtool: 'inline-source-map'
},
webpackMiddleware: {
noInfo: true
}
})
}

此时运行测试,就会在test目录下生成coverage文件下,里面有相关测试报告,命令行也会有测试覆盖率的简单输出。

1
2
3
4
5
6
=============================== Coverage summary ===============================
Statements : 100% ( 4/4 )
Branches : 75% ( 3/4 )
Functions : 100% ( 1/1 )
Lines : 100% ( 4/4 )
================================================================================

有什么条件没有运行到呢,打开coverage下的html
测试用例示意图
标黄的window.location.href就是没有测试到的条件。

怎么进行的覆盖率统计

生成的lcov.info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TN:
SF:/Users/xxx/Documents/practice/testing/src/index.js
FN:2,(anonymous_0)
FNF:1
FNH:1
FNDA:4,(anonymous_0)
DA:1,1
DA:3,4
DA:4,4
DA:5,4
LF:4
LH:4
BRDA:3,0,0,4
BRDA:3,0,1,1
BRDA:5,1,0,2
BRDA:5,1,1,2
BRF:4
BRH:4
end_of_record

SF代表文件
FN代表函数,2代表函数开始的行数
FNF:1 表示一共有1个函数
FNH:1 表示有一个函数被测试用例覆盖
FNDA:4 表示函数被执行了4次

html文件里面是个网页报告

原理

大概原理就是,对源代码进行语法分析,插入覆盖率统计的代码,将执行统计结果放到对应的全局变量中,再将语法树转成js代码运行。在测试用例跑完之后再重新读取这些全局变量。

参考:
代码测试覆盖率分析
聊一聊前端自动化测试