对代码进行单元测试是几乎每个软件工程师都要完成的工作。本文以 C++语言为基础,讲解如何进行单元测试并生成测试报告。测试是软件开发过程中一个必须的环节,测试确保软件的质量符合预期。
对于工程师自己来说,单元测试也是提升自信心的一种方式。直接交付没有经过测试的代码是不太好的,因为这很可能会浪费整个团队的时间,在一些原本早期就可以发现的问题上。而单元测试,就是发现问题一个很重要的环节。
本文以 C++语言为基础,讲解如何进行单元测试并生成测试报告。
在工具上,我们会使用下面这些:
为了方便本文的讲解,我专门编写了一个演示项目作为代码示例。
演示项目的源码可以在我的 Github 上获取: paulQuei/gtest-and-coverage( github /paulQuei/gtest-and-coverage) 。
你可以通过下面几条命令下载和运行这个项目:
git clone https://github.com/paulQuei/gtest-and-coverage.git
cd gtest-and-coverage
./make_all.sh
要运行这个项目,你的机器上必须先安装好前面提到的工具。如果没有,请阅读下文以了解如何安装它们。
如果你使用的是 Mac 系统,下文假设你的系统上已经安装了 brew(brew sh/) 包管理器。如果没有,请通过下面这条命令安装它:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
演示项目的目录结构如下:
.
├── CMakeLists.txt
├── googletest-release-1.8.1.zip
├── include
│ └── utility.h
├── make_all.sh
├── src
│ └── utility.cpp
└── test
└── unit_test.cpp
这里演示的内容是:以测试一个我们要提供的软件库为例,讲解如何对其进行单元测试并生成测试报告。
为了简单起见,这个软件库只有一个头文件和一个实现文件。
当然,在实际上的项目中,一个软件库会通常包含更多的文件,不过这并不影响我们要说明的问题。
演示项目中的文件说明如下:
| 文件名称 | 说明 |
|---|---|
| make_all.sh | 入口文件,会执行:编译,测试和生成报告等所有工作 |
| CMakeLists.txt | 项目的编译文件 |
| googletest-release-1.8.1.zip | google test 源码压缩包 |
| utility.h | 待测试的软件库的头文件 |
| utility.cpp | 待测试的软件库的实现文件 |
| unit_test.cpp | 对软件库进行单元测试的代码 |
演示项目在如下的环境中测试过。
为了简化编译的过程,这里使用 CMake 作为编译工具。关于 CMake 的更多内容请参见请官网: cmake 组织网 。
关于如何安装 CMake 请参见这里: Installing CMake( cmake 组织网/install/) 。
另外,你也可以通过一条简单的命令来安装 CMake:
brew install cmake
sudo apt install cmake
由于篇幅所限,这里不打算对 CMake 做过多讲解,读者可以访问其官网或者在网络上搜寻其使用方法。
这里仅仅对演示项目中用到的内容做一下说明。演示项目中的 CMakeLists.txt 内容如下:
cmake_minimum_required(VERSION 2.8.11) ①
project(utility) ②
set(CMAKE_CXX_STANDARD 11) ③
set(GTEST googletest-release-1.8.1) ④
include_directories("./include" "${GTEST}/googletest/include/")
link_directories("build/gtest/googlemock/gtest/")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") ⑤
add_library(${CMAKE_PROJECT_NAME}_lib src/utility.cpp) ⑥
add_executable(unit_test test/unit_test.cpp) ⑦
target_link_libraries(unit_test ${CMAKE_PROJECT_NAME}_lib gtest gtest_main pthread) ⑧
以编号为序,这段代码说明如下:
软件测试有很多种分类方式。从测试的级别来说,可以大致分为:
这其中,单元测试是最局部和具体的。它通常需要对代码中的每一个类和函数进行测试。
单元测试通常由开发者完成,需要针对代码逻辑进行测试。所以它是一种 白盒测试( en.wikipedia 组织网/wiki/White-box_testing) 。
xUnit 是几种单元测试框架的总称。最早源于 Smalltalk 的单元测试框架 SUnit,它是由 Kent Beck( en.wikipedia 组织网/wiki/Kent_Beck) 开发的。
除此之外,还有针对 Java 语言的 JUnit,针对 R 语言的 RUnit。
在本文中,我们使用 Google 开发的 xUnit 框架:Google Test。
Google Test 的项目主页在 Github 上: Github: Google Test( github /google/googletest) 。
实际上,这个项目中同时包含了 GoogleTest 和 GoogleMock 两个工具,本文中我们只会讲解第一个。
Google Test 支持的操作系统包含下面这些:
目前有很多的项目都使用了 Google Test,例如下面这些:
关于如何编译 Google Test 请参见这里: Generic Build Instructions( github /google/googletest/tree/master/googletest) 。
为了便于读者使用,我们在演示项目中包含了 Google Test 1.8.1 的源码压缩包。并且在 CMake 文件中,同时包含了 Google Test 的编译和使用配置工作。
如果使用演示项目,读者将不需要手动处理 Google Test 的编译和安装工作。
为了便于下文说明,演示项目中包含了几个简单的函数。
可以从这里下载源码以便查看其中的内容: paulQuei/gtest-and-coverage( github /paulQuei/gtest-and-coverage) 。
演示项目中的软件库包含一个头文件和一个实现文件。头文件内容如下:
// utility.h
#ifndef INCLUDE_UTILITY_
#define INCLUDE_UTILITY_
enum CalcType {
ADD,
MINUS,
MULTIPLE,
DIVIDE
};
class Utility {
public:
int ArithmeticCalculation(CalcType op, int a, int b);
double ArithmeticCalculation(CalcType op, double a, double b);
bool IsLeapYear(int year);
};
#endif
这个头文件说明如下:
这三个函数的实现也不复杂:
// utility.cpp
#include "utility.h"
#include <iostream>
#include <limits>
using namespace std;
int Utility::ArithmeticCalculation(CalcType op, int a, int b) {
if (op == ADD) {
return a + b;
} else if (op == MINUS) {
return a - b;
} else if (op == MULTIPLE) {
return a * b;
} else {
if (b == 0) {
cout << "CANNO Divided by 0" << endl;
return std::numeric_limits<int>::max();
}
return a / b;
}
}
double Utility::ArithmeticCalculation(CalcType op, double a, double b) {
if (op == ADD) {
return a + b;
} else if (op == MINUS) {
return a - b;
} else if (op == MULTIPLE) {
return a * b;
} else {
if (b == 0) {
cout << "CANNO Divided by 0" << endl;
return std::numeric_limits<double>::max();
}
return a / b;
}
}
bool Utility::IsLeapYear(int year) {
if (year % 100 == 0 && year % 400 == 0) {
return true;
}
if (year % 100 != 0 && year % 4 == 0) {
return true;
}
return false;
}
接下来我们就要对上面这些代码进行测试了。
要使用 Google Test 进行测试,整个过程也非常的简单。只要进行下面三部:
并且,测试代码写起来也非常的简单,像下面这样:
#include "utility.h"
#include "gtest/gtest.h"
TEST(TestCalculationInt, ArithmeticCalculationInt) {
Utility util;
EXPECT_EQ(util.ArithmeticCalculation(ADD, 1, 1), 2);
EXPECT_EQ(util.ArithmeticCalculation(MINUS, 2, 1), 1);
EXPECT_EQ(util.ArithmeticCalculation(MULTIPLE, 3, 3), 9);
EXPECT_EQ(util.ArithmeticCalculation(DIVIDE, 10, 2), 5);
EXPECT_GT(util.ArithmeticCalculation(DIVIDE, 10, 0), 999999999);
}
是的,就是这么简单的几行代码,就对整数四则运算的函数进行了测试。
TEST 后面所包含的内容称之为一条 case,通常我们会为每个函数创建一个独立的 case 来进行测试。一个测试文件中可以包含很多条 case。同时,一条 case 中会包含很多的判断(例如 EXPECT_EQ... )。
注意:在做单元测试的时候,保证每条 case 是独立的,case 之间没有前后依赖关系是非常重要的。
当然,测试代码中包含的判断的多少将影响测试结果的覆盖率。所以在编写每条 case 的时候,我们需要仔细思考待测试函数的可能性,有针对性的进行测试代码的编写。
这段代码应该很好理解,它分别进行了下面这些测试:
你可能会发现,这段代码里面甚至没有 main 函数。它也依然可以生成一个可执行文件。这就是我们链接 gtest_main 所起的作用。
在实际的测试过程中,你想判断的情况可能不止上面这么简单。下面我们来看看 Google Test 还能做哪些测试。
Google Test 对于结果的判断,有两种形式:
可以进行的判断方法主要有下面这些:
| Fatal | Nonfatal | 说明 |
|---|---|---|
| ASSERT_TRUE(condition) | EXPECT_TRUE(condition) | 判断 condition 为 true |
| ASSERT_FALSE(condition) | EXPECT_FALSE(condition) | 判断 condition 为 false |
| Fatal | Nonfatal | 说明 |
|---|---|---|
| ASSERT_EQ(expected, actual) | EXPECT_EQ(expected, actual) | 判断两个数值相等 |
| ASSERT_NE(val1, val2) | EXPECT_NE(val1, val2) | val1 != val2 |
| ASSERT_LT(val1, val2) | EXPECT_LT(val1, val2) | val1 < val2 |
| ASSERT_LE(val1, val2) | EXPECT_LE(val1, val2) | val1 <= val2 |
| ASSERT_GT(val1, val2) | EXPECT_GT(val1, val2) | val1 > val2 |
| ASSERT_GE(val1, val2) | EXPECT_GE(val1, val2) | val1 >= val2 |
说明:
| Fatal | Nonfatal | 说明 |
|---|---|---|
| ASSERT_STREQ(expected, actual) | EXPECT_STREQ(expected, actual) | 两个 C string 相同 |
| ASSERT_STRNE(str1, str2) | EXPECT_STRNE(str1, str2) | 两个 C string 不相同 |
| ASSERT_STRCASEEQ(exp, act) | EXPECT_STRCASEEQ(exp, act) | 忽略大小写,两个 C string 相同 |
| ASSERT_STRCASENE(str1, str2) | EXPECT_STRCASENE(str1, str2) | 忽略大小写,两个 C string 不相同 |
| Fatal | Nonfatal | 说明 |
|---|---|---|
| ASSERT_FLOAT_EQ(exp, act) | EXPECT_FLOAT_EQ(exp, act) | 两个 float 数值相等 |
| ASSERT_DOUBLE_EQ(exp, act) | EXPECT_DOUBLE_EQ(exp, act) | 两个 double 数值相等 |
| ASSERT_NEAR(val1, val2, abs_err) | EXPECT_NEAR(val1, val2, abs_err) | val1 和 val2 的差距不超过 abs_err |
| Fatal | Nonfatal | 说明 |
|---|---|---|
| ASSERT_THROW(stmt, exc_type) | EXPECT_THROW(stmt, exc_type) | stmt 抛出了 exc_type 类型的异常 |
| ASSERT_ANY_THROW(stmt) | EXPECT_ANY_THROW(stmt) | stmt 抛出了任意类型的异常 |
| ASSERT_NO_THROW(stmt) | EXPECT_NO_THROW(stmt) | stmt 没有抛出异常 |
在某些情况下,我们可能希望多条测试 case 使用相同的测试数据。例如,我们的演示项目中,每条 case 都会需要创建 Utility 对象。
有些时候,我们要测试的对象可能很大,或者创建的过程非常的慢。这时,如果每条 case 反复创建这个对象就显得浪费资源和时间了。此时,我们可以使用 Test Fixture 来共享测试的对象。
要使用 Test Fixture 我们需要创建一个类继承自 Google Test 中的 ::testing::Test 。
还记得我们前面说过,我们要尽可能的保证每条测试 case 是互相独立的。但是,当我们在多条 case 之间共享有状态的对象时,就可能出现问题。
例如,我们要测试的是一个队列数据结构。有的 case 会向队列中添加数据,有的 case 会从队列中删除数据。case 执行的顺序不同,则会导致 Queue 中的数据不一样,这就可能会影响 case 的结果。
为了保证每条 case 是独立的,我们可以在每条 case 的执行前后分别完成准备工作和清理工作,例如,准备工作是向队列中添加三个数据,而清理工作是将队列置空。
这两项重复性的工作可以由 ::testing::Test 类中的 Setup 和 TearDown 两个函数来完成。
我们演示用的 Utility 类是无状态的,所以不存在这个问题。因此,这里我们仅仅在 Setup 和 TearDown 两个函数中打印了一句日志。
使用 Test Fixture 后,我们的代码如下所示:
class UtilityTest : public ::testing::Test {
protected:
void SetUp() override {
cout << "SetUp runs before each case." << endl;
}
void TearDown() override {
cout << "TearDown runs after each case." << endl;
}
Utility util;
};
这段代码说明如下:
要使用这里定义的 Test Fixture,测试 case 的代码需要将开头的 TEST 变更为 TEST_F 。
这里 _F 就是 Fixture 的意思。
使用 TEST_F 的 case 的代码结构如下:
TEST_F(TestCaseName, TestName) {
... test body ...
}
这里的 TestCaseName 必须是 Test Fixture 的类名。
所以我们的测试代码写起来是这样:
TEST_F(UtilityTest, ArithmeticCalculationDouble) {
EXPECT_EQ(util.ArithmeticCalculation(ADD, 1.1, 1.1), 2.2);
}
TEST_F(UtilityTest, ArithmeticCalculationIsLeapYear) {
EXPECT_FALSE(util.IsLeapYear(1997));
EXPECT_TRUE(util.IsLeapYear(2000));
EXPECT_TRUE(util.IsLeapYear(2016));
EXPECT_FALSE(util.IsLeapYear(2100));
}
我们针对 ArithmeticCalculation 方法故意只进行了一种情况的测试。这是为了最终生成代码覆盖率所用。
编写完单元测试之后,再执行编译工作便可以运行测试程序以查看测试结果了。
测试的结果像下面这样:

如果测试中包含了失败的 case,则会以红色的形式输出。同时,会看到失败的 case 所处的源码行数,这样可以很方便的知道哪一个测试失败了,像下面这样:

如果只想有选择性的跑部分 case,可以通过 --gtest_filter 参数进行过滤,这个参数支持 * 通配符。
像下面这样:
$ ./build/unit_test --gtest_filter=*ArithmeticCalculationInt
Running main() from googletest/src/gtest_main.cc
Note: Google Test filter = *ArithmeticCalculationInt
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from TestCalculationInt
[ RUN ] TestCalculationInt.ArithmeticCalculationInt
CANNO Divided by 0
[ OK ] TestCalculationInt.ArithmeticCalculationInt (0 ms)
[----------] 1 test from TestCalculationInt (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[ PASSED ] 1 test.
如果想要更好的理解这些内容。请读者下载演示项目之后完成下面这些操作:
- 在 utility.h 和 utility.cpp 中添加一些新的函数。
- 在新添加的函数中故意包含一个 bug。
- 为新添加的函数编写测试代码,并测试出函数中包含的 bug。
在进行单元测试之后,我们当然希望能够直观的看到我们的测试都覆盖了哪些代码。
理论上,如果我们能做到 100%的覆盖我们的所有代码,则可以说我们的代码是没有 Bug 的。
但实际上,100%的覆盖率要比想象得困难。对于大型项目来说,能够达到 80% ~ 90%的语句覆盖率就已经很不错了。
先来看一下,当我们在说“覆盖率”的时候我们到底是指的什么。
实际上,代码覆盖率有下面几种类型:
这其中,函数覆盖率最为简单,就不做说明了。
语句覆盖率是我们最常用的。因为它很直观的对应到我们写的每一行代码。
而分支覆盖率和条件覆盖率可能不太好理解,需要做一下说明。
以下面这个 C 语言函数为例:
int foo (int x, int y) {
int z = 0;
if ((x > 0) && (y > 0)) {
z = x;
}
return z;
}
这个函数中包含了一个 if 语句,因此 if 语句成立或者不成立构成了两个分支。所以如果只测试了 if 成立或者不成立的其中之一,其分支覆盖率只有 1/2 = 50% 。
而条件覆盖率需要考虑每种可能性的情况。
对于 if (a && b) 这样的语句,其一共有四种可能的情况:
请读者思考一下:对于三层 if 嵌套,每个 if 语句包含三个布尔变量的代码,如果要做到 100%的条件覆盖率,一共要测试多少种情况。
很显示,在编写代码的时候,尽可能的减少代码嵌套,并且简化逻辑运算是一项很好的习惯。
便于测试的代码也是便于理解和维护的,反之则反。
有了这些概念之后,我们就可以看懂测试报告中的覆盖率了。
gcov( gcc.gnu 组织网/onlinedocs/gcc/gcov.html) 是由 GCC 工具链提供的代码覆盖率生成工具。它可以很方便的和 GCC 编译器配合使用。
通常情况下,安装好 GCC 工具链,也就同时包含了 gcov 命令行工具。
对于代码覆盖率工具所做的工作,可以简单的理解为:标记一次运行过程中,哪些代码被执行过,哪些没有执行。
因此,即便没有测试代码,直接运行编译产物也可以得到代码的覆盖率。只不过,通常情况下这样得到的覆盖率较低罢了。
这里我们以另外一个简单的代码示例来说明 gcov 的使用。
这段代码如下:
// test.c
#include <stdio.h>
int main (void) {
for (int i = 1; i < 10; i++) {
if (i % 3 == 0)
printf ("%d is divisible by 3\n", i);
if (i % 11 == 0)
printf ("%d is divisible by 11\n", i);
}
return 0;
}
这是一个仅仅包含了 main 函数的 c 语言代码,main 函数的逻辑也很简单。
我们将这段代码保存到文件 test.c 。
要通过 gcov 生成代码覆盖率。需要在编译时,增加参数 --coverage :
gcc --coverage test.c
--coverage 等同于编译参数 -fprofile-arcs -ftest-coverage 以及在链接时增加 -lgcov 。
此处的编译结果除了得到可执行文件 a.out ,还会得到一个 test.gcno 文件。该文件包含了代码与行号的信息,在生成覆盖率时会需要这个文件。
很显然,带 --coverage 编译参数得到的编译产物会比不带这个参数要包含更多的信息,因此编译产物会更大。所以这个参数只适合在需要生成代码覆盖率的时候才加上。对于正式发布的编译产物,不应该添加这个编译参数。
当我们执行上面编译出来的可执行文件 a.out 时,我们还会得到每个源码文件对应的 gcda 后缀的文件。由 test.gcno 和 test.gcda 这两个文件,便可以得到代码的覆盖率结果了。
关于这两个文件的说明请参见这里: Brief description of gcov data files( gcc.gnu 组织网/onlinedocs/gcc-4.1.2/gcc/gcov-Data-Files.html)
只需要通过 gcov 指定源文件的名称(不需要带后缀): gcov test ,便可以得到包含覆盖率的结果文件 test.c.gcov 了。
回顾一下我们刚刚的操作内容:
$ gcc --coverage test.c
$ ll
total 72
-rwxr-xr-x 1 Paul staff 26K 11 10 14:41 a.out
-rw-r--r-- 1 Paul staff 240B 11 10 14:41 test.c
-rw-r--r-- 1 Paul staff 720B 11 10 14:41 test.gcno
$ ./a.out
3 is divisible by 3
6 is divisible by 3
9 is divisible by 3
$ ll
total 80
-rwxr-xr-x 1 Paul staff 26K 11 10 14:41 a.out
-rw-r--r-- 1 Paul staff 240B 11 10 14:41 test.c
-rw-r--r-- 1 Paul staff 212B 11 10 14:42 test.gcda
-rw-r--r-- 1 Paul staff 720B 11 10 14:41 test.gcno
$ gcov test
File 'test.c'
Lines executed:85.71% of 7
test.c:creating 'test.c.gcov'
$ ll
total 88
-rwxr-xr-x 1 Paul staff 26K 11 10 14:41 a.out
-rw-r--r-- 1 Paul staff 240B 11 10 14:41 test.c
-rw-r--r-- 1 Paul staff 623B 11 10 14:42 test.c.gcov
-rw-r--r-- 1 Paul staff 212B 11 10 14:42 test.gcda
-rw-r--r-- 1 Paul staff 720B 11 10 14:41 test.gcno
我们可以 cat test.c.gcov 一下,查看覆盖率的结果:
-: 0:Source:test.c
-: 0:Graph:test.gcno
-: 0:Data:test.gcda
-: 0:Runs:1
-: 0:Programs:1
-: 1:// test.c
-: 2:
-: 3:#include <stdio.h>
-: 4:
-: 5:int main (void) {
-: 6:
20: 7: for (int i = 1; i < 10; i++) {
9: 8: if (i % 3 == 0)
3: 9: printf ("%d is divisible by 3\n", i);
9: 10: if (i % 11 == 0)
#####: 11: printf ("%d is divisible by 11\n", i);
9: 12: }
-: 13:
1: 14: return 0;
-: 15:}
这个结果应该还是很容易理解的,最左边一列描述了代码的覆盖情况:
gcov( gcc.gnu 组织网/onlinedocs/gcc/gcov.html) 得到的结果是本文形式的。但很多时候,我们可能希望得到更加美观和便于浏览的结果。
此时就可以使用 lcov( ltp.sourceforge 网络网/coverage/lcov.php) 了。
lcov 是 gcov 工具的图形前端。它收集多个源文件的 gcov 数据,并生成描述覆盖率的 HTML 页面。生成的结果中会包含概述页面,以方便浏览。
lcov 支持我们前面提到的所有四种覆盖率。
这个链接是 lcov 生成的报告样例: lcov - code coverage report( ltp.sourceforge 网络网/coverage/lcov/output/index.html) 。
lcov 并非包含在 GCC 中,因此需要单独安装。
brew install lcov
sudo apt install lcov
对于 lcov 的使用方法可以通过下面这条命令查询:
lcov --help
通过输出我们可以看到,这个命令的参数有简短(例如 -c )和完整(例如 --capture )两种形式,其作用是一样的。
这里主要关注的下面这几个参数:
另外还有需要说明的是:
最后, make_all.sh 脚本中包含的相关内容如下:
COVERAGE_FILE=coverage.info
REPORT_FOLDER=coverage_report
lcov --rc lcov_branch_coverage=1 -c -d build -o ${COVERAGE_FILE}_tmp
lcov --rc lcov_branch_coverage=1 -e ${COVERAGE_FILE}_tmp "*src*" -o ${COVERAGE_FILE}
genhtml --rc genhtml_branch_coverage=1 ${COVERAGE_FILE} -o ${REPORT_FOLDER}
这段代码从我们前面编译的结果中收集覆盖率结果,并将结果输出到 coverage.info_tmp 文件中。但是这里面会包含非项目源码的覆盖率(例如 google test),所以我们又通过另外一条命令来指定”src”文件夹进行过滤。最后,通过 genhtml 得到 html 格式的报告。
可以通过浏览器查看覆盖率报告的结果,像下面这样:

从这个报告的首页,我们已经可以看到代码的语句覆盖率(Lines),函数覆盖率(Functions)以及分支覆盖率(Branches)。而对于条件覆盖率可以从详细页面中看到。如下图所示:

在上面这张图中,我们可以看到哪些代码被覆盖了,哪些没有。而对于对于 if-else 之类的语句,也能很清楚的看到条件覆盖率的覆盖情况。例如,对于代码的 27 行,只覆盖了 if 成立时的情况,没有覆盖 if 不成立时的情况。
本文中,我们已经完整的完成了从编写单元测试到覆盖率生成的整个过程。
但实际上,对于这项工作我们还可以做得更多一些。例如下面这两项工作:
Google Mock 是 Google Test 的扩展,用于编写和使用 C++ Mock 类。
在面向对象的编程中, Mock 对象( en.wikipedia 组织网/wiki/Mock_object) 是模拟对象,它们以预先设定的方式模仿真实对象的行为。程序员通常会创建一个 Mock 对象来测试某个其他对象的行为,这与汽车设计师使用碰撞测试假人来模拟人类在车辆碰撞中的动态行为的方式非常相似。
关于 Google Mock 的更多内容请参见: Google Mock 的文档( github /google/googletest/blob/master/googlemock/README.md) 。
对于演示项目的覆盖率报告是通过手动执行脚本文件生成的。
而在实际的项目中,可能同时有很多人在开发同一个项目,每一天项目中都会有很多次的代码提交。我们不可能每次手动的执行编译和生成覆盖率报告结果。这时就可以借助一些 持续集成( en.wikipedia 组织网/wiki/Continuous_integration) 的工具,定时自动地完成项目的编译,测试和覆盖率报告结果的生成工作。
可以在持续集成工具中包含我们编写的脚本,然后将覆盖率报告的 html 结果发布到某个 Web 服务器上,最后再以邮件的形式将链接地址发送给大家。
这样就可以很方便的让整个团队看到所有模块的测试结果和覆盖率情况了。
完成了一整套这样的工作,可以非常好的提升整个项目的质量。

