以前写过一个 TDD, 当你的用例全部通过了测试, 你的代码写就写完了.
如果你只是一个人编码或者只有少数的测试用例, 那么就不会觉得一个个测试去维护的痛苦; 而一个小组的人, 所有人提交上来的代码, 都要先跑一下相关的测试用例, 才能上传, 此时就需要一个自动化的测试框架.
这方面有CppUnit, Gtest, 不过Gest优势明显. 本文主要总结Gtest(gooel mock暂时不涉及, 文本也不会教你如何去写测试用例). (尽管gtest-runner这种带有GUI的框架, 但是Gtest是其根本)
引子
Google test:
下面简称 gtest, 比较老牌的用于自动化继承测试的框架, 全平台支持.
采用成熟框架的好处是不用维护测试框架.
测试框架, 可以结合CLog系统一起使用吗.
使用gtest的成熟项目有:
- Chromium
- LLVM
- Protocol Buffers
- Open CV
使用gtest应该不少于使用cmake的.
repo地址: https://github.com/google/googletest
正文
介绍
框架
测试框架帮我们提供了 case 的管理,执行,断言集,运行参数,全局事件工作,所有的这些使得我们只需关注:于对于特定的输入,被测对象的返回是否正常. (但是编写测试用例还是我们自己做). 当然结合单元测试, 还有一些开发方法(Approach to Unit Testing):
- Test-driven development
- Extreme programming
架构
该部分参考wiki.
一般的测试框架的基本架构:(下图中先忽略TestSuite, 下图并不太完整)
如上图所示,单测框架中通常包括TestRunner, Test, TestResult, TestCase, TestSuite, TestFixture六个组件。
- TestRuner:负责驱动单元测试用例的执行,汇报测试执行的结果, 从而简化测试(主调程序)
- TestFixture:以测试套件的形式提供setUp()和tearDown()方法, 保证两个test case之间的执行是相互独立, 互不影响的.(前置条件)
- TestResult:这个组件用于收集每个test case的执行结果
- Test:作为TestSuite和TestCase的父类暴露run()方法为TestRunner调用
- TestCase:暴露给用户的一个类,用户通过继承TestCase,编写自己的测试用例逻辑
- TestSuite:提供suite功能管理testCase
- Assertions: 断言预测.(预言正确,继续执行; 语言错误,抛出异常,终止执行)
- Test Execution: 独立测试子程序或者套件程序
- Test Result Formatter: 产生人可读的规整日志还是XML.
正因为相似的体系结构,所以大多数单元测试框架都提供了类似的功能和使用方法。
那么在单测中引入单元测试框架会带来什么好处,在现有单元测试框架下还会存在什么样不能解决的问题呢?
优劣势
优势
降低编写单元测试的难度:
在单元测试中引入单测框架使得编写单测用例时,不需要再关注于如何驱动case的执行,如何收集结果,如何管理case集,只需要关注于如何写好单个测试用例即可;同时,在一些测试框架中通过提供丰富的断言集,公用方法,以及运行参数使得编写单个testcase的过程得到了最大的简化。
gtest在xUnit架构的基础上增加了很多特性:1
2
3
4
5
6
71. Test discovery
2. A rich set of assertions (including User-defined)
3. Death tests
4. Fatal and non-fatal failures
5. value/type parameterized tests
6. various options for running the tests
7. XML test report generation
当我没有用过的时候, 我也不知道它到底在说啥, 不过用多了就好了.
(你可以参考一下docs目录下的相关文档)
劣势
单元测试本身不能解决如何编写的问题: (反而增加了系统复杂度)
我在单元测试框架中写一个TestCase,与我单独写一个cpp文件在main()方法里写测试代码有什么本质却别吗?用了单元测试框架,并没有解决我在对复杂系统做单测时遇到的问题。
没错,对于单个case这两者从本质上说是没有区别的。单元测试框架本身并没有告诉你如何去写TestCase,在这一点上他是没有提供任何帮助的。所以对于一些复杂的场景,只用单元测试框架是有点多少显得无能为力的。
适用场景有限:
使用单元测试框架往往适用于以下场景的测试:单个函数,一个class,或者几个功能相关class的测试,对于纯函数测试,接口级别的测试尤其适用。但是,对于一些复杂场景, 如:
- 被测对象依赖复杂,甚至无法简单new出这个对象
- 对于一些failure场景的测试
- 被测对象中涉及多线程合作
- 被测对象通过消息与外界交互的场景
(gtest只是一定程度上解决了)
单纯依赖单测框架是无法实现单元测试的,而从某种意义上来说,这些场景反而是测试中的重点。以分布式系统的测试为例,class 与 function级别的单元测试对整个系统的帮助不大,当然,这种单元测试对单个程序的质量有帮助;分布式系统测试的要点是测试进程间的交互:一个进程收到客户请求,该如何处理,然后转发给其他进程;收到响应之后,又修改并应答客户;同时分布式系统测试中通常更关注一些异常路径的测试,这些场景才是测试中的重点,也是难点所在。(Mock方法的引入通常能帮助我们解决以上场景中遇到的难题)
安装
Linux平台
下载源码, 编译安装:1
2
3
4$ git clone https://github.com/google/googletest
$ cmake .
$ make
$ sudo make install
当你下载完源码, 进入 googletest目录
, 在linux平台下, 只有4个目录是需要的:
- include
- make
- samples
- src
安装日志大致如下: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
35Install the project...
-- Install configuration: ""
-- Installing: /usr/local/lib/libgtest.a
-- Installing: /usr/local/lib/libgtest_main.a
-- Installing: /usr/local/include/gtest
-- Installing: /usr/local/include/gtest/gtest-typed-test.h
-- Installing: /usr/local/include/gtest/gtest-death-test.h
-- Installing: /usr/local/include/gtest/gtest-message.h
-- Installing: /usr/local/include/gtest/gtest-param-test.h
-- Installing: /usr/local/include/gtest/gtest-test-part.h
-- Installing: /usr/local/include/gtest/gtest-printers.h
-- Installing: /usr/local/include/gtest/gtest_pred_impl.h
-- Installing: /usr/local/include/gtest/gtest.h
-- Installing: /usr/local/include/gtest/gtest-param-test.h.pump
-- Installing: /usr/local/include/gtest/gtest_prod.h
-- Installing: /usr/local/include/gtest/internal
-- Installing: /usr/local/include/gtest/internal/gtest-param-util-generated.h.pump
-- Installing: /usr/local/include/gtest/internal/gtest-tuple.h.pump
-- Installing: /usr/local/include/gtest/internal/gtest-internal.h
-- Installing: /usr/local/include/gtest/internal/gtest-filepath.h
-- Installing: /usr/local/include/gtest/internal/custom
-- Installing: /usr/local/include/gtest/internal/custom/gtest-printers.h
-- Installing: /usr/local/include/gtest/internal/custom/gtest.h
-- Installing: /usr/local/include/gtest/internal/custom/gtest-port.h
-- Installing: /usr/local/include/gtest/internal/gtest-type-util.h
-- Installing: /usr/local/include/gtest/internal/gtest-linked_ptr.h
-- Installing: /usr/local/include/gtest/internal/gtest-death-test-internal.h
-- Installing: /usr/local/include/gtest/internal/gtest-param-util-generated.h
-- Installing: /usr/local/include/gtest/internal/gtest-tuple.h
-- Installing: /usr/local/include/gtest/internal/gtest-string.h
-- Installing: /usr/local/include/gtest/internal/gtest-port-arch.h
-- Installing: /usr/local/include/gtest/internal/gtest-param-util.h
-- Installing: /usr/local/include/gtest/internal/gtest-type-util.h.pump
-- Installing: /usr/local/include/gtest/internal/gtest-port.h
-- Installing: /usr/local/include/gtest/gtest-spi.h
主要是安装:/usr/local/lib/
库文件 libgtest.a 和 /usr/local/include/gtest
相关头文件.
window平台
window平台更加简单, 而且不用你编写makefile文件, 你需要关心两件事儿:
- 如果把gtest编译成静态库
- 如果在你的单元测试项目中引用
请去下载 release 版本: https://github.com/google/googletest/releases
下面给出两种方法(都是编译静态库), 它们针对性不同.
一、 直接利用源码目录中的 msvc 工程
首先把源码加压到一个合适的位置, 因为里面的源文件要被引用到的, 打开 D:\googletest-release-1.7.0\msvc\gtest.sln
solution文件.
可以看到, 当你打开这个工程的时候, 里面的4个项目全局是静态库, 话不多说, 编译吧, debug&release版本各一份(用于项目工程的时候, 两份lib都要拷贝). 很幸运, 没有报错.
(如果报错 error C2977 "std::tuple" too many template arguments
解决方法就是在每隔工程(project)的属性中的C++ –> Preprocessor (预处理)–> preprocessor defination (预处理定义)中增加 _VARIADIC_MAX=10
)
把编译出的release版本的gtest.lib, gtest_main.lib和debug版本的gtestd.lib, gtest_maind.lib 都放到gtest根目录的lib文件夹下(lib和include位于同一级)
为了方便使用, 配置环境变量 GTEST = D:\Program Files\gtest-1.6.0
; 当然你不配置, 一会儿引用的时候, 选择正确的路径就可以了. 也就是说, 这个工程, 完全就是为了创建这个库的. 后面你再创建项目, 然后引用gtest的头文件和库即可.
(这一种也是比较推荐的)
二、把gtest作为静态库子项目包含进入你的正式项目
这么做, 可以在unit test项目里面直接引用gtest静态库项目, 和你的主项目, 并且编译release或者debug版本都很方便, 灵活.
下面我以第一种方式的基础, 测试一下, 是否安装成功.
- 新建一个win32控制台项目(解决方案), 选择空项目(不要预编译头), 例如叫”MainProject”, 这个项目里的源码是要被测试的
然后在”MainProject”中添加相关的自己的源码, 下面我给很简单的例子:
1
2
3
4
5
6
7
8
9
10
11
12//sample.h
int fun(int a, int b);
/*--------------------*/
//sample.cpp
int fun(int a, int b)
{
return (a-b);
}很简单的俩文件.
- 修改你的MainProject, 不管它原来是哪种类型的项目;测试的时候修改为静态库,并且运行库设置为MTd
项目属性 -> 配置属性 -> C/C++ -> 代码生成 -> 运行库
为静态库多线程调试(/MTd)
- 然后在该解决方案里, 再添加一个项目”GTest”,也是win32控制台项目(不要预编译头), 空项目;
- 配置项目”Gtest”的相关头文件和库文件路径, 并引用项目”MainProject”(在添加->引用里面可以引用).
当然如果你配置了Gtest环境变量, 可以直接使用$(Gtest)
进行目录引用 - 配置GTest运行库
项目属性 -> 配置属性 -> C/C++ -> 代码生成 -> 运行库
为静态库多线程调试(/MTd)
- 在Gtest中添加相关的测试用例文件吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//包含头文件的时候, 一定注意, 当前是在Gtest的目录下
TEST(fun, case1)
{
EXPECT_LT(-2, fun(1, 2));
EXPECT_EQ(-1, fun(1, 2));
ASSERT_LT(-2, fun(1, 2));
ASSERT_EQ(-1, fun(1, 2));
}
int main(int argc, char* argv[])
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
运行一下:
补充VStudio的坑:
在一个vs解决方案中,一个可执行项目依赖多个lib项目的情况下,经常出现此类问题,原因在默认运行时库的引用上。一般情况下,我会首先打开各个项目的属性查看如下选项:项目–属性–配置属性–C/C++–代码生成–运行时库(有/MT,/MTd,/MD,/MDd四个选项, M代表Multi-thead, T代表static, D代表Dynamic)必须查看所有项目使用的库都是相同的,不同的话就修改成相同的吧
下面的讲解, 都是基于Linux平台了.
Linux下配置
跑一下原生的sample, 然后配置自己的环境.
原生sample
1 | $ cd make |
发现这个Makefile是用来编译sample目录下 TESTS = sample1_unittest
的, 所以可以跑一下这个demo:
1 | $ make |
运行一下可执行文件即可 ./sample1_unittest
: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$ ./sample1_unittest
Running main() from gtest_main.cc
[==========] Running 6 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 3 tests from FactorialTest
[ RUN ] FactorialTest.Negative
[ OK ] FactorialTest.Negative (0 ms)
[ RUN ] FactorialTest.Zero
[ OK ] FactorialTest.Zero (0 ms)
[ RUN ] FactorialTest.Positive
[ OK ] FactorialTest.Positive (0 ms)
[----------] 3 tests from FactorialTest (0 ms total)
[----------] 3 tests from IsPrimeTest
[ RUN ] IsPrimeTest.Negative
[ OK ] IsPrimeTest.Negative (0 ms)
[ RUN ] IsPrimeTest.Trivial
[ OK ] IsPrimeTest.Trivial (0 ms)
[ RUN ] IsPrimeTest.Positive
[ OK ] IsPrimeTest.Positive (0 ms)
[----------] 3 tests from IsPrimeTest (1 ms total)
[----------] Global test environment tear-down
[==========] 6 tests from 2 test cases ran. (1 ms total)
[ PASSED ] 6 tests.
配置环境
下面配置自己的开发环境, 以便跑自己的用例, 操作步骤如下:
- 创建自己的测试目录
- 书写自己的makefile文件
- 书写自己的测试用例
创建自己的测试工程目录:1
2
3$ mkdir -p test/gtest_test
$ cd test/gtest_test
$ mkdir src out
其中src目录是书写测试用例的目录, out目录是中间产物编译输出目录; 其中当前目录的 run_test
是唯一的(执行所有测试的)可执行文件.
创建并编写makefile:
(比源码中的makefile精简很多, 因为没有必要重新编译 libgtest_main.a)1
2$ cd test/gtest_test
$ touch makefile
makefile 源码如下: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
36USER_DIR = .
OUT_DIR = $(USER_DIR)/out
GTEST_DIR_INCLUDE = /usr/local/include
CPPFLAGS += -isystem -I$(GTEST_DIR_INCLUDE)
CXXFLAGS += -g -Wall -Wextra -pthread
TESTS = run_test
#gtest lib: libgtest_main.a
GTEST_LIB = /usr/local/lib/libgtest.a
## our src file ./src/
USER_SRCS =$(foreach d,$(USER_DIR)/src,$(wildcard $(d)/*.cpp))
OBJS =$(patsubst %.cpp,%.o,$(USER_SRCS)) # %.o
#$(warning $(USER_SRCS))
#$(warning $(OBJS))
###################################################
all : $(TESTS)
$(TESTS) : $(OBJS)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) $^ -lpthread $(GTEST_LIB) -o $@
mv $^ $(OUT_DIR)
# OBJS
#$(OBJS) : $(USER_SRCS)
./src/%.o : ./src/%.cpp
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@
.PHONY : clean
clean :
rm -f $(TESTS) $(OUT_DIR)/*.o $(OBJS)
在 src 目录下建立自己的测试用例:
建立相关的文件1
2$ cd src
$ touch sqrt.h sqrt.cpp sqrt_unittest.cpp
下面依次编写相关代码:
sqrt.h1
2
3
int sqrt(int x);
sqrt.cpp1
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
//不借助库函数
//利用 x*y, 仅当x=y是最大.
int sqrt(int x)
{
if(x<=0) return 0;
if(x==1) return 1;
int small = 0, large = x, tmp = x/2;
/* small----(samll+large)/2----large*/
while(small < large) {
int a = x/tmp;
int b = x/(tmp+1);
if(a==tmp) return a;
if(b==temp+1) return b;
if(tmp<a && tmp+1>b) {
return tmp;
} else if(tmp<a && tmp+1<b) {
small = tmp + 1;
tmp = (small + large)/2;
} else {
large = tmp;
tmp = (small + large)/2;
}
}
return -1;
}
sqrt_unittest.cpp
(注意文件名, xxx_unittest.cpp
这个文件名并不是固定, 取成别的也无所谓, 只是这样更容易辨认哪一个文件是在测哪个函数而已)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
TEST(SQRTTest, Zero)
{
EXPECT_EQ(0, sqrt(0));
}
TEST(SQRTTest, Positive)
{
EXPECT_EQ(100, sqrt(10000));
EXPECT_EQ(1000, sqrt(1000008));
EXPECT_EQ(99, sqrt(9810));
}
TEST(SQRTTest, Negative)
{
int i = -1;
EXPECT_EQ(0, sqrt(i));
}
int main(int argc, char* argv[])
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
编译运行一下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21$ make
g++ -isystem -I/usr/local/include -g -Wall -Wextra -pthread -c src/sqrt.cpp -o src/sqrt.o
g++ -isystem -I/usr/local/include -g -Wall -Wextra -pthread -c src/sqrt_unittest.cpp -o src/sqrt_unittest.o
g++ -isystem -I/usr/local/include -g -Wall -Wextra -pthread src/sqrt.o src/sqrt_unittest.o -lpthread /usr/local/lib/libgtest.a -o run_test
mv src/sqrt.o src/sqrt_unittest.o ./out
$ ./run_test
[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 3 tests from SQRTTest
[ RUN ] SQRTTest.Zero
[ OK ] SQRTTest.Zero (0 ms)
[ RUN ] SQRTTest.Positive
[ OK ] SQRTTest.Positive (0 ms)
[ RUN ] SQRTTest.Negative
[ OK ] SQRTTest.Negative (0 ms)
[----------] 3 tests from SQRTTest (0 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 1 test case ran. (0 ms total)
[ PASSED ] 3 tests.
下面开始进行主要内容介绍.
主要内容
主函数
在包含gtest/gtest.h
的源文件中, 要想让Runner运行测试用例, 一般都是这样的套路:1
2
3
4
5
6
7
8
9
10
11
//若干测试
//主函数
int main(int argc, char* argv[])
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
“::testing::InitGoogleTest(&argc, argv)” : 初始化命令行参数(gtest的测试案例允许接收一系列的命令行参数).
“RUN_ALL_TESTS()” : 运行所有测试案例.
关键是要调用 宏函数 RUN_ALL_TEST()
.
断言宏
断言的宏可以理解为分为两类:
- ASSERT_* 系列的断言,当检查点失败时,退出当前函数(注意:并非退出当前案例)
- EXPECT_* 系列的断言,当检查点失败时,继续往下执行
例如:1
2// int型比较,预期值:3,实际值:Add(1, 2)
EXPECT_EQ(3, Add(1, 2));
假如你的Add(1, 2) 结果为4的话,会在结果中输出:1
2
3error: Value of: Add(1, 2)
Actual: 4
Expected:3
如果想输出更具体的信息, 可以在断言宏的调用后面, 直接输出.1
2
3
4
5for (int i = 0; i < x.size(); ++i)
{
//出错是, 可以知道x[i], y[i] 但是无法知道 i, 所以添加 << 输出信息
EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;
}
输出1
2
3
4
5error: Value of: y[i]
Actual: 4
Expected: x[i]
Which is: 3
Vectors x and y differ at index 2
下面详细介绍各种宏:
布尔值检查1
2
3Fatal assertion Nonfatal assertion Verifies
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition is true
ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition is false
我通常用true的这一组.
带参数的谓词断言宏:
在布尔检查的时候, 没有传入参数, 下面这一组可以传入参数.1
2
3
4
5//pred1(val1) returns true
ASSERT_PRED1(pred1, val1); EXPECT_PRED1(pred1, val1);
//pred2(val1, val2) returns true
ASSERT_PRED2(pred2, val1, val2); EXPECT_PRED2(pred2, val1, val2);
(一般最多能传入5个参数)
可以看到, 传入参数时, 这里的输出信息也会比较丰富了.
使用案例:1
2
3
4
5
6
7
8
9
10
11bool MutuallyPrime(int m, int n)
{
return Foo(m , n) > 1;
}
TEST(PredicateAssertionTest, Demo)
{
int m = 5, n = 6;
EXPECT_PRED2(MutuallyPrime, m, n);
}
当失败时,返回错误信息:1
2
3error: MutuallyPrime(m, n) evaluates to false, where
m evaluates to 5
n evaluates to 6
对于这样的宏, 输出信息仍不满意可以使用 XXX_PRED_FORMAT1
, 例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15testing::AssertionResult
AssertFoo(const char* m_expr, const char* n_expr, const char* k_expr,
int m, int n, int k)
{
if (Foo(m, n) == k)
return testing::AssertionSuccess();
testing::Message msg;
msg << m_expr << " 和 " << n_expr << " 的最大公约数应该是:" << Foo(m, n) << " 而不是:" << k_expr;
return testing::AssertionFailure(msg);
}
TEST(AssertFooTest, HandleFail)
{
EXPECT_PRED_FORMAT3(AssertFoo, 3, 6, 2);
}
运行失败时:1
error: 3 和 6 的最大公约数应该是:3 而不是:2
数值型数据检查:1
2
3
4
5
6
7Fatal assertion Nonfatal assertion Verifies
ASSERT_EQ(expected, actual); EXPECT_EQ(expected, actual); 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
就是通常所说的比较值.
更加进阶的是 浮点数检查:1
2
3
4
5//the two float values are almost equal
ASSERT_FLOAT_EQ(expected, actual); EXPECT_FLOAT_EQ(expected, actual);
//the two double values are almost equal
ASSERT_DOUBLE_EQ(expected, actual); EXPECT_DOUBLE_EQ(expected, actual);
字符串检查:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18the two C strings have the same content:
ASSERT_STREQ(expected_str, actual_str);
EXPECT_STREQ(expected_str, actual_str);
the two C strings have different content:
ASSERT_STRNE(str1, str2);
EXPECT_STRNE(str1, str2);
//忽略大小写的一般不常用.
the two C strings have different content, ignoring case:
ASSERT_STRCASENE(str1, str2);
EXPECT_STRCASENE(str1, str2);
the two C strings have the same content, ignoring case:
ASSERT_STRCASEEQ(expected_str, actual_str);
EXPECT_STRCASEEQ(expected_str, actual_str);
STREQ和STRNE同时支持char和wchar_t类型的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21TEST(StringTest, StringCmpTest)
{
char* pszCoderZh = "CoderZh";
wchar_t* wszCoderZh = L"CoderZh";
std::string strCoderZh = "CoderZh";
std::wstring wstrCoderZh = L"CoderZh";
EXPECT_STREQ("CoderZh", pszCoderZh);
EXPECT_STREQ(L"CoderZh", wszCoderZh);
EXPECT_STRNE("CnBlogs", pszCoderZh);
EXPECT_STRNE(L"CnBlogs", wszCoderZh);
//忽略大小写的
EXPECT_STRCASEEQ("coderzh", pszCoderZh);
//EXPECT_STRCASEEQ(L"coderzh", wszCoderZh); 不支持
EXPECT_STREQ("CoderZh", strCoderZh.c_str());
EXPECT_STREQ(L"CoderZh", wstrCoderZh.c_str());
}
直接返回的宏:
- 直接返回成功:SUCCEED();
- FAIL(); //assert 返回失败
- ADDFAILURE(); //expect 返回失败 (EXPECT*即使返回失败, 也继续执行)
异常检查:1
2
3
4
5
6
7
8//抛出指定异常:
ASSERT_THROW(statement, exception_type); EXPECT_THROW(statement, exception_type);
//抛出任何异常:
ASSERT_ANY_THROW(statement); EXPECT_ANY_THROW(statement);
//不抛出异常:
ASSERT_NO_THROW(statement); EXPECT_NO_THROW(statement);
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int Foo(int a, int b)
{
if (a == 0 || b == 0)
{
throw "don't do that";
}
int c = a % b;
if (c == 0)
return b;
return Foo(b, c);
}
TEST(FooTest, HandleZeroInput)
{
EXPECT_ANY_THROW(Foo(10, 0));
EXPECT_THROW(Foo(0, 5), char*);
}
当然还有其他宏, 不过没有必要一个个列举, 具体可以参考文末的参考资料.
事件化机制
事件化机制, 一般就是在 Test Case 之前, 或者之后做一些操作.
总结一下gtest的事件一共有3种:
- 全局的, 所有案例执行前后
- TestSuite级别的, 在某一批案例中第一个案例前, 最后一个案例执行后
- TestCase级别的, 每个TestCase前后
(TestSuite和TestCase都是用TEST_F宏)
下面一个个讲解一下:
全局事件.
要实现全局事件, 必须写一个类, 继承 testing::Environment
类, 实现里面的SetUp和TearDown方法.
- SetUp()方法在所有案例执行前执行
- TearDown()方法在所有案例执行后执行
但是, 我们还需要告诉gtest添加这个全局事件, 我们需要在main函数中通过 testing::AddGlobalTestEnvironment
方法将事件注册进来(当然可以多个).
案例:
1 |
|
运行结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
Foo FooEnvironment SetUP
[----------] 2 tests from TestSuite
[ RUN ] TestSuite.True
[ OK ] TestSuite.True (1 ms)
[ RUN ] TestSuite.False
src/globalpara_unittest.cpp:33: Failure
Value of: returnTest(0)
Actual: false
Expected: true
[ FAILED ] TestSuite.False (0 ms)
[----------] 2 tests from TestSuite (1 ms total)
[----------] Global test environment tear-down
Foo FooEnvironment TearDown
[==========] 2 tests from 1 test case ran. (1 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] TestSuite.False
1 FAILED TEST
特别注意, 继承的是 testing::Environment
, 实现 SetUp()
和 TearDown()
.
TestSuite事件
这种事件是针对特定的TestSuit里面的TestCase的. 需要写一个类,继承testing::Test,然后实现两个 静态
方法:
- static void SetUpTestCase() 方法在第一个TestCase之前执行
- static void TearDownTestCase() 方法在最后一个TestCase之后执行
1 | //类的名字就是TestSuite的名字 |
因为是针对一个TestSuite里面的所有用例, 所以不用具体的宏, 而是使用的 TEST_F
宏, 但是你可以在TEST_F
里面, 再使用具体的宏.
最后一个 TestCase事件.
TestCase事件是挂在每个案例执行前后的, 实现方式和上面的几乎一样, 不过需要实现的是SetUp方法和TearDown方法:
- SetUp()方法在每个TestCase之前执行
- TearDown()方法在每个TestCase之后执行
也就是说, 其实它还是绑定在 TestSuite 类里面, 即 TestSuite 的那个类要继承 testing::Test
, 只不过执行时机是每一个用例跑起来的时候, 执行的不再是相关的静态方法了, 而是Setup
和TearDown
.
因为具体的执行还是和TestSuite类有关, 所以当你写测试用例的时候, 还是要写 TEST_F
宏:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class FooCalcTest:public testing::Test
{
protected:
virtual void SetUp()
{
m_foo.Init();
}
virtual void TearDown()
{
m_foo.Finalize();
}
FooCalc m_foo;
};
TEST_F(FooCalcTest, HandleNoneZeroInput)
{
EXPECT_EQ(4, m_foo.Calc(12, 16));
}
TEST_F(FooCalcTest, HandleNoneZeroInput_Error)
{
EXPECT_EQ(5, m_foo.Calc(12, 16));
}
注意, 上面demo中 TEST_F
里面的测试并没有太多实在的意义.
所有的事件, 一般都是做一些资源的分配, 共享, 释放等工作的, 或者共享执行的某一个方法; 只不过它们针对的测试对象, 时间不太一样. (关于事件的使用技巧, 见下文)
补充:
TEST与TEST_F之间的区别:
首先注意, TestSuite事件和TestCase事件都会使用TEST_F宏, 但是需要重写的virtual方法不同:
- SetUp(), TearDown() 适用于TestCase事件(也就是需要在个TestCase执行之前以及之后做事情, 才需要实现这两个方法)
- SetUpTestCase()和TearDownTestCase()针对TestSuite, 也就是只在第一个TestCase之前和最后一个TestCase之后才执行.
TEST_F比TEST强一些的地方在于TEST_F实际上会生成一个新类(类名用于TEST_F的第一个参数), 该类有SetUp和TearDown函数, SetUpTestCase和TearDownTestCase函数; 根据需求不同, 实现不同的函数.
同一个TestCase文件中不能混合使用TEST与TEST_F, 也就是说排他.
For each TestCase defined with TEST_F(), Google Test will:
- Create a fresh test fixture at runtime (test fixture针对TestSuite中每一个独立的TestCase)
- Immediately initialize it via SetUp()
- Run the test
- Clean up by calling TearDown()
- Delete the test fixture. Note that different tests in the same test case have different test fixture objects, and Google Test always deletes a test fixture before it creates the next one. Google Test does not reuse the same test fixture for multiple tests. Any changes one test makes to the fixture do not affect other tests.
参数化
同一个函数, 多个不同的参数进行调用, 如果没有参数化测试机制, 你的调用可能是这样的.
1 | TEST(IsPrimeTest, HandleTrueReturn) |
即同一个函数 IsPrime
多次进行调用, 就必须写相应的语句, 但是一旦需要测试的参数范围太大, 那么这么做显然不是了.
gtest就提供了参数化调用机制, 有了参数化机制, 你的调用可以用一个语句, 完成上面的多次调用.
首先添加一个类, 继承 testing::TestWithParam<T>
, 其中T就是你需要参数化的参数类型, 比如上面的例子, 我需要参数化一个int型的参数:1
2
3
4
5//继承的时候, 要实例化模板参数
class IsPrimeParamTest : public::testing::TestWithParam<int>
{
};
然后, 再用宏 TEST_P
进行测试: (p代表parameterized)1
2
3
4
5
6
7TEST_P(IsPrimeParamTest, HandleTrueReturn)
{
//拿到具体的参数
int n = GetParam();
//进行具体的测试
EXPECT_TRUE(IsPrime(n));
}
然后在主函数初始化的时候, 传入参数列表(范围):1
2
3//testing::Values()生成参数
INSTANTIATE_TEST_CASE_P(TrueReturn, IsPrimeParamTest,
testing::Values(3, 5, 11, 23, 17));
其中第一个参数 TrueReturn
可以任取, 代表参数化测试的名称, 或称之为prefix(实际名称是:prefix/test_case_name.test.name/index, 其中index是参数的编号, 从0开始); 但是第二个必须是参数化的类名; 第三个参数是参数生成器.
参数生成函数还有:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//注意名字空间 testing::
Range(begin, end[, step]): 范围在begin~end之间, 步长为step, 不包括end
Values(v1, v2, ..., vN): v1,v2到vN的值
ValuesIn(container) 或 ValuesIn(begin, end) : 从一个C类型的数组或是STL容器或是迭代器中取值
Bool() : 取false 和 true 两个值
Combine(g1, g2, ..., gN) :
它将g1,g2,...gN进行排列组合.
g1,g2,...gN本身是一个参数生成器, 每次分别从g1,g2,..gN中各取出一个值,
组合成一个元组(Tuple)作为一个参数.
说明:这个功能只在提供了<tr1/tuple>头的系统中有效.
gtest会自动去判断是否支持tr/tuple, 如果你的系统确实支持;
而gtest判断错误的话, 你可以重新定义宏GTEST_HAS_TR1_TUPLE=1.
类型参数
不过对于不同数据类型的, 同一调用. (相对同类型不同值的测试, 这个类型化参数复杂一点儿).
首先定义一个模版类,继承testing::Test,1
2
3
4
5
6
7
8
9template <typename T>
class FooTest : public testing::Test {
public:
typedef std::list<T> List;
static T shared_;
T value_;
};
接着我们定义需要测试到的具体数据类型, 比如下面定义了需要测试 char, int 和 unsigned int :
1 | typedef testing::Types<char, int, unsigned int> MyTypes; |
然后使用 TYPED_TEST
宏进行测试案例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16TYPED_TEST(FooTest, DoesBlah) {
// Inside a test, refer to the special name TypeParam to get the type
// parameter. Since we are inside a derived class template, C++ requires
// us to visit the members of FooTest via 'this'.
TypeParam n = this->value_;
// To visit static members of the fixture, add the 'TestFixture::'
// prefix.
n += TestFixture::shared_;
// To refer to typedefs in the fixture, add the 'typename TestFixture::'
// prefix. The 'typename' is required to satisfy the compiler.
typename TestFixture::List values;
values.push_back(n);
}
上面的例子看上去也像是类型的参数化,但是还不够灵活,因为需要事先知道类型的列表。gtest还提供一种更加灵活的类型参数化的方式,允许你在完成测试的逻辑代码之后再去考虑需要参数化的类型列表,并且还可以重复的使用这个类型列表. 下面也是官方的例子:
1 | template <typename T> |
然后使用的是 TYPED_TEST_P
完成具体的测试用例:1
2
3
4
5
6
7
8
9
10TYPED_TEST_P(FooTest, DoesBlah) {
// Inside a test, refer to TypeParam to get the type parameter.
TypeParam n = 0;
}
TYPED_TEST_P(FooTest, HasPropertyA)
{
//...
}
接着使用REGISTER_TYPED_TEST_CASE_P宏注册:1
2// 第一个参数是TestSuite的名称, 也是相关类的名称, 后面的参数是TestCase
REGISTER_TYPED_TEST_CASE_P(FooTest, DoesBlah, HasPropertyA);
接着指定需要的类型列表: (相当于参数生成器)1
2
3
4typedef testing::Types<char, int, unsigned int> MyTypes;
//第一个参数是类型参数化测试的名称, 相当于Prefix, 第二个是参数化类的名称;
//第三个参数是自定义的参数生成器
INSTANTIATE_TYPED_TEST_CASE_P(My, FooTest, MyTypes);
总结起来, 这个类型参数化测试是结合的在testing::Test的子类中, 指定的具体的模板参数的特化, 从而在宏 TYPED_TEST_P
完成具体的类型参数测试.
(相比类型化参数测试, 不同值的参数化测试更加重要一些)
死亡测试
某些情况下, 程序会执行奔溃, 但是即使死亡(奔溃), 也应该按照我们预定的方式, 走相应的流程, 这个测试就是看能够在奔溃的时候能否按照预期执行.
相关的宏:
1 | statement crashes with the given error: |
上面的宏涉及到的正则表达式, 在Linux平台下, 都是POSIX风格的; 而在window下, 则是gtest自己实现的简单正则(意思是少了很多功能). gtest定义两个宏, 用来表示当前系统支持哪套正则表达式风格:
- POSIX风格:GTEST_USES_POSIX_RE = 1
- Simple风格:GTEST_USES_SIMPLE_RE=1
下面分别说明一下:XXX_DEATH(statement, regex)
statement是被测试的代码语句, regex是一个正则表达式, 用来匹配异常时在stderr中输出的内容; 例如下面的例子:
1 | void Foo() |
注意, 凡是涉及到死亡测试的, 名字后缀都要带上 “DeathTest”, 就像本例中的 FooDeathTest
. 并且gtest会优先运行死亡测试案例.
( XXX_DEATH 其实是对 XXX_EXIT 进行的一次包装,XXX_DEATH的 predicate 判断进程是否以非0退出码退出或被一个信号杀死)
XXX_EXIT(statement, predicate, regex)
statement是被测试的代码语句, predicate 在这里必须是一个委托接, 收int型参数并返回bool, 并且只有当返回值为true时, 死亡测试案例才算通过. regex是一个正则表达式,用来匹配异常时在stderr中输出的内容.
gtest提供了一些常用的 predicate :1
2
3
4
5//如果程序正常退出并且退出码与exit_code相同则返回 true
testing::ExitedWithCode(exit_code)
//如果程序被signal_number信号kill的话就返回true
testing::KilledBySignal(signal_number) // Windows下不支持
1 | TEST(ExitDeathTest, Demo) |
XXX_DEBUG_DEATH
, debug版本下的死亡测试:
1 |
|
可以看到, 在Debug版和Release版本下, XXX_DEBUG_DEATH的定义不一样. 因为很多异常只会在Debug版本下抛出, 而在Realease版本下不会抛出, 所以针对Debug和Release分别做了不同的处理. 例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int DieInDebugElse12(int* sideeffect)
{
if (sideeffect) *sideeffect = 12;
GTEST_LOG_(FATAL, "debug death inside DieInDebugElse12()");
return 12;
}
TEST(TestCase, TestDieOr12WorksInDgbAndOpt)
{
int sideeffect = 0;
// Only asserts in dbg.
EXPECT_DEBUG_DEATH(DieInDebugElse12(&sideeffect), "death");
// opt-mode has sideeffect visible.
EXPECT_EQ(12, sideeffect);
// dbg-mode no visible sideeffect.
EXPECT_EQ(0, sideeffect);
}
死亡测试的运行方式:
- testing::FLAGS_gtest_death_test_style = “fast”; (默认方式)
- testing::FLAGS_gtest_death_test_style = “threadsafe”;
(可以为单个测试设置, 或者main函数中为所有的死亡测试设置) 例如:
1 | TEST(MyDeathTest, TestOne) { |
总的来说, 死亡测试主要是对奔溃结果进行检查; 借助XXX_DEATH(statement, regex)
, XXX_EXIT(statement, predicate, regex)
可以很简单的编写相关的死亡测试.
运行参数
通过, 命令行给相关测试传递运行参数(毕竟最后生成的就是一个可行性文件), 或者运行测试的时候给定命令行选项(例如以XML形式输出测试结果)等. gtest为我们提供了一系列的运行参数(环境变量、命令行参数或代码里指定), 使得我们可以对案例的执行进行一些有效的控制.
前面提到,对于运行参数,gtest提供了三种设置的途径:
- 系统环境变量
- 命令行参数
- 代码中指定FLAG
优先级原则是:1
命令行参数 > 代码中指定FLAG > 系统环境变量
(其实是由于后设置的会覆盖最开始的; 所以产生了上面的优先级顺序)
测试程序的入口, 都会处理这些命令行参数.1
2
3
4
5int main(int argc, char* argv[])
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
也可以使用 可以使用testing::GTEST_FLAG
这个宏来设置相关的FLAG, 比如相对于命令行参数 --gtest_output
, 可以使用 testing::GTEST_FLAG(output) = "xml:"
来设置.
同时推荐将这句放置 InitGoogleTest 之前, 这样就可以使得对于同样的参数, 命令行参数优先级高于代码中指定, 即:1
2
3
4
5
6
7int main(int argc, char* argv[])
{
testing::GTEST_FLAG(output) = "xml:";
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
指定系统环境变量的方式.
如果设置系统环境变量给gtest参数, 必须注意的是:
- 系统环境变量全大写, 比如对于–gtest_output,响应的系统环境变量为: GTEST_OUTPUT
- 有一个命令行参数例外, 那就是–gtest_list_tests, 它是不接受系统环境变量的.(只是用来罗列测试案例名称)
(个人不建议, 使用环境变量的方式; 这样会扰乱开发环境)
三种方式中, 最推荐运行时指定, 其他方式总会有这样或者那样的问题(比如代码中设置FLAG, 有些异常就是捕获不到).
参数列表
好比我现在已经编译了一个google test的测试执行文件, 那么给定相关的参数会得到不同的效果, 其中可以使用的参数如下: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./run_test --help
This program contains tests written using Google Test. You can use the
following command line flags to control its behavior:
Test Selection:
--gtest_list_tests 将不会执行里面的测试案例,而是输出一个案例的列表
List the names of all tests instead of running them. The name of
TEST(Foo, Bar) is "Foo.Bar".
--gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS] 根据给定的表达式,选择运行的案例
Run only the tests whose name matches one of the positive patterns but
none of the negative patterns. '?' matches any single character; '*'
matches any substring; ':' separates two patterns.
--gtest_also_run_disabled_tests 禁止的测试是指, 名称中添加DISABLED前缀
Run all disabled tests too. 例如:
// Tests that Foo does Abc.
TEST(FooTest, DISABLED_DoesAbc) { }
class DISABLED_BarTest : public testing::Test { };
// Tests that Bar does Xyz.
TEST_F(DISABLED_BarTest, DoesXyz) { }
Test Execution:
--gtest_repeat=[COUNT] 设置案例重复运行次数
Run the tests repeatedly; use a negative count to repeat forever.
--gtest_shuffle
Randomize tests' orders on every iteration.
--gtest_random_seed=[NUMBER]
Random number seed to use for shuffling test orders (between 1 and
99999, or 0 to use a seed based on the current time).
Test Output:
--gtest_color=(yes|no|auto) 默认是彩色输出
Enable/disable colored output. The default is auto.
--gtest_print_time=0 输出命令行时是否打印每个测试案例的执行时间, 默认是不打印的
Don't print the elapsed time of each test.
--gtest_output=xml[:DIRECTORY_PATH/|:FILE_PATH]
Generate an XML report in the given directory or with the given file
name. FILE_PATH defaults to test_details.xml.
--gtest_stream_result_to=HOST:PORT
Stream test results to the given server.
Assertion Behavior:
--gtest_death_test_style=(fast|threadsafe)
Set the default death test style.
--gtest_break_on_failure 调试模式下,当案例失败时停止,方便调试
Turn assertion failures into debugger break-points.
--gtest_throw_on_failure 当案例失败时以C++异常的方式抛出
Turn assertion failures into C++ exceptions.
--gtest_catch_exceptions=0 是否捕捉异常(默认是不捕捉异常的)
不捕捉的话, 可能会弹出一个对话框(阻碍测试);
所以有时候还是要捕捉一下, 就设置一个非0值.
Do not report exceptions as test failures. Instead, allow them
to crash the program or throw a pop-up (on Windows).
Except for --gtest_list_tests, you can alternatively set the corresponding
environment variable of a flag (all letters in upper-case). For example, to
disable colored text output, you can either specify --gtest_color=no or set
the GTEST_COLOR environment variable to no.
For more information, please read the Google Test documentation at
https://github.com/google/googletest/. If you find a bug in Google Test
(not one in your own code or tests), please report it to
<googletestframework@googlegroups.com>.
XML 报告的输出格式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="3" failures="1" errors="0" time="35" name="AllTests">
<testsuite name="MathTest" tests="2" failures="1"* errors="0" time="15">
<testcase name="Addition" status="run" time="7" classname="">
<failure message="Value of: add(1, 1) Actual: 3 Expected: 2" type=""/>
<failure message="Value of: add(1, -1) Actual: 1 Expected: 0" type=""/>
</testcase>
<testcase name="Subtraction" status="run" time="5" classname="">
</testcase>
</testsuite>
<testsuite name="LogicTest" tests="1" failures="0" errors="0" time="5">
<testcase name="NonContradiction" status="run" time="5" classname="">
</testcase>
</testsuite>
</testsuites>
即使使用--gtest_filter
参数过滤时, 输出的xml报告中还是会包含所有测试案例的信息, 只不过那些不被执行的测试案例的status值为“notrun”.
内部实现
内部的关键技术是什么?
主要宏注册.一个没有反射(reflection)的测试框架, 必须多做一些工作, 让框架知道相关测试类&用例的存在.那就是宏注册, 以往的CppUnit, CxxTest要么是人工写(每添加一个测试, 就要进行相应的宏注册), 要么是写专门的(Python,Perl)脚本, 通过这个脚本扫描自己编写的文件, 生成一些新的文件. 但是boost::test和gtest则是通过展开宏进行宏注册, 也就是放在预处理阶段.
主要是宏注册, 然后继承testing::Test类, 之后通过工厂方法创建TestInfo, 并注册给框架gtest.cc里的MakeAndRegisterTestInfo()
有一句 GetUnitTestImpl()->AddTestInfo(xx)
, 之后让TestRunner进行调用.
1 | //namespace internal: |
简单说一下.
先写一个简单的测试文件:1
2
3
4
5
6
TEST(TestSuite, Negative)
{
EXPECT_EQ(1,1);
}
然后用g++展开一下,1
g++ -E main.cpp -o main.i -I/usr/local/include
得到结果如下(展开的代码非常多, 6万多行, 找到TestSuite_Negative_Test; 最好过滤掉带井号的行)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# 2 "main.cpp" 2
class
# 3 "main.cpp"
TestSuite_Negative_Test
# 3 "main.cpp" 3
: public ::testing::Test { public:
# 3 "main.cpp"
TestSuite_Negative_Test
# 3 "main.cpp" 3
() {} private: virtual void TestBody(); static ::testing::TestInfo* const test_info_ __attribute__ ((unused));
# 3 "main.cpp"
TestSuite_Negative_Test
# 3 "main.cpp" 3
(
# 3 "main.cpp"
TestSuite_Negative_Test
# 3 "main.cpp" 3
const &); void operator=(
# 3 "main.cpp"
TestSuite_Negative_Test
# 3 "main.cpp" 3
const &);};::testing::TestInfo* const
# 3 "main.cpp"
TestSuite_Negative_Test
# 3 "main.cpp" 3
::test_info_ = ::testing::internal::MakeAndRegisterTestInfo(
# 3 "main.cpp"
"TestSuite"
# 3 "main.cpp" 3
,
# 3 "main.cpp"
"Negative"
# 3 "main.cpp" 3
, __null, __null, ::testing::internal::CodeLocation("main.cpp", 3), (::testing::internal::GetTestTypeId()), ::testing::Test::SetUpTestCase, ::testing::Test::TearDownTestCa\
se, new ::testing::internal::TestFactoryImpl<
# 3 "main.cpp"
TestSuite_Negative_Test
# 3 "main.cpp" 3
差不多已经看到我说的那个样子了(当然你也可以尝试定义自己的宏函数, 宏类; 个人觉得意义不大, 还不如直接去看别人的源码写分析).
关于gtest源码内容一览:
- 预处理技术分析(我已经简单分析了)
- 自动调度机制
- 结果统计机制
- 监听机制
- 断言使用方法和解析
- 自定义输出技术
- 死亡测试技术
- 私有属性-方法测试
- 参数填充技术
- 模板类测试技术
以后有时间, 单独开贴再写(主要是在预处理部分, .i
文件), Gtest源码剖析
搞懂了Gtest内部实现, 其实自己也可以尝试着写相关的测试框架(这部分纯属扩展).
研发测试
在Google, 质量并不等于测试. “质量不是被测试出来的” 这句老话是再正确不过了. 虽然质量并不是测试出来的, 但我们有同样的证据表明, 没有测试, 你不可能开发出任何有质量的东西. 一个人怎么可能在没有测试的情况下认定你的工程具有高质量?
对于这种难题,最简单的解决办法就是: 禁止对开发工作开方便之门,以独立自由之精神进行测试。测试和开发工作需要同步进行
. 编写一点, 测试一点. 再编写一点, 再测试一点. 更好的方法是制定测试计划或者你开发之前先把计划做好. (但是这对开发人员的要求很高
)
测试并不是一个独立的工作, 它是开发工作的一部分, 伴随着整个开发过程. 质量不等于测试, 为了质量, 需要你把开发工作和测试结合到一起, 搅拌它们, 直到分不清你我为止.
在Google,这是我们明确的目标:把开发和测试融合,你不能单独进行任何一项工作。做一点,测试一点。再做一点,再测试一点。关键就是看谁在做测试。因为在Google,专职测试人员是出奇的少,所以唯一可行的方法就是使用开发人员。还有比这些实际开发了这些程序的人员更合适做测试的吗? 还有比程序的作者更适合去发现bug的吗? 是谁具有更多的愿望在程序第一次写出时避免bug? Google之所以安排这么少的专职测试人员的原因就是,开发者负责质量。事实上,坚持使用大型测试机构的团队通常会开发出有问题的东西。测试机构庞大,这是一个信号表明编码/测试工作的融和有问题。增加测试人员并不能解决任何问题。这就是说,质量措施更多的应该是一种预防行为,而不是一种发现过程。
质量属于开发问题,而不是测试问题
。通过把测试工作一定程度的融合到开发过程中,我们极大的降低了一些本来被认为会写很多有问题的代码的人的出错机会。我们不仅避免了大量的客户方的问题,我们还非常有效的降低了测试人员提出的、其实不是bug的bug。在Google,测试工作的目标就是检查这些预防工作是否在有效的运行。测试工程部一直在寻找这种作为bug创造者的软件工程师和作为bug预防者的测试软件工程师之间的联合能达到的效果的证据,一旦这个方法出现问题,他们就会拉响警笛。这种开发和测试的结合随处可见,从代码审查注释上写的“你的测试呢?”到厕所里的给开发者的最好的测试实践方法的宣传画——这是我们臭名昭著的厕所测试指导方针。测试是开发工作中是必不可少的,开发和测试的联姻是孕育质量的过程。软工就是测试者,测试软工就是测试者,测试工程师就是测试者。
个人点评: 关于作者所说的, “这是我们明确的目标:把开发和测试融合,你不能单独进行任何一项工作。做一点,测试一点。再做一点,再测试一点。关键就是看谁在做测试”, 我想这是一个非常理想的情况, 实际上当前的许多大公司也没有做到, 其实也很难做到. 但是有理想总是好的, 慢慢靠近也不错.
其实可以参考一下TDD和XP或者敏捷等开发方法, 里面也有涉及到相关内容.
尾巴
当初玩gtest也是因为tdd的原因, 毕竟一个认真负责的开发者, 就应该保证自己代码的质量, 编码和测试是分不开的.
本文又花了很大的篇幅, 记录了整个Gtest的探索过程(当然还有源码剖析一部分没有写后续补上), 希望这些时间没有白费.
当然最后也强烈推荐一下 测试驱动开发方法
, 即 tdd .
(btw: gtest中有很重要的一部分, 是和google mock相关的, 这里没有谈到; 以后专门再说)
参考资料
- 玩转Google开源C++单元测试框架Google Test系列 (不过它是window平台, 我选用的是linux平台)
- 官方文档(docs目录): Primer.md , AdvancedGuide.md , FAQ.md
- http://baidutech.blog.51cto.com/4114344/743740/