知乎上面一个讨论很多的问题,如何进行单元测试?同样,知乎上各路科学家讲了各种思路。大概的思路是:
- 使用某个单测的库(比如gtest)
- 设计一些科学或伪科学的单元测试case
- mock各种接口
- 提高单元测试覆盖率,尽可能的覆盖各种分支和边界
单元测试是通过在开发阶段进行白盒测试,把问题在开发阶段就发现,不至于传递到测试阶段,或者正式环境。通过单元测试,把一些琐碎且容易错的事情交给单测框架。让开发人员在修改代码之后能感到安心,踏实。只要跑一把单元测试,就能自动化验证程序逻辑的正确性,而无需在提交代码之前提心吊胆、担心会漏掉什么情况没有处理或者自己新加入的逻辑制造了bug。
1. 使用gtest进行单元测试
// 安装
$ git clone [https://github.com/google/googletest.git](https://github.com/google/googletest.git)
$ cd googletest
$ mkdir build && cd build
$ cmake ..
$ make && sudo make install
- 一个gtest简单的例子
#include
int sum(int a, int b) {
return a+b;
}
int multi(int a, int b) {
return a*b;
}
// 测试集为 MyTest,测试案例为 Sum
TEST(MytestSet, sum_func) {
EXPECT_EQ(sum(10,20), 30);
EXPECT_EQ(sum(0,0), 0);
}
TEST(MytestSet, multi_func) {
EXPECT_EQ(multi(10,20), 200);
EXPECT_EQ(multi(0,0), 0);
}
int main(int argc, char *argv[]) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
- 编译完成之后,使用make test执行单元测试:
gtest的框架还是比较简单的,使用了一个test的宏。交给用户定义单测的名称。定义如下:
TEST(test_suite_name, test_case_name)
{
// test body ...
}
在测试函数中,调用被测试的函数 对其返回的结果进行assert。gtest提供了EXPECT_和ASSERT_这两种风格的断言,区别在于ASSERT_在失败之后,会立刻返回,不会执行后面的逻辑。gtest在执行的时候,会提供一个比较直观的输出。可以看到那些case通过了,那些case没有通过。
TEST(MyTest, Add)
{
EXPECT_EQ(1 + 1, 2);
ASSERT_EQ(1 + 1, 2);
}
gtest 提供了8 个ASSERT_断言分别是:
ASSERT_TRUE()、ASSERT_FALSE()、ASSERT_EQ()、
ASSERT_NE()、ASSERT_LT()、ASSERT_LE()、ASSERT_GT()和ASSERT_GE()
EXPECT_的断言同样也有 8 个分别是:
ASSERT_TRUE()、ASSERT_FALSE()、EXPECT_EQ()、
EXPECT_NE()、EXPECT_LT()、EXPECT_LE()、EXPECT_GT()和EXPECT_GE()
2. mock测试
void do_something()
{
db_req();
// todo
}
db_req()在某些场景里面是一个很重的操作,或者有上下文的状态依赖(比如依赖本地有db,db中的数据是正确的等)。在这些场景中使用单测的话,就需要伪造一个db_req(),这个就是mock。
gmock是gtest中的一个模块,使用gmock可以实现对各种接口的mock。比如,下面的例子对一个get_from_rpc()的接口进行mock,指定一个调用的结果。
// FooInterface.h
#include
namespace seamless {
class FooInterface {
public:
virtual ~FooInterface() {}
public:
virtual std::string get_from_rpc() = 0;
std::string get_result()
{
std::string str = "reuslt is ";
return str + get_from_rpc();
}
};
}
// FooMock.h
#include
#include
#include "FooInterface.h"
namespace seamless {
class MockFoo: public FooInterface
{
public:
MOCK_METHOD0(get_from_rpc, std::string());
};
}
// FooMain.cc
TEST(mock_test, get_result)
{
MockFoo mockFoo;
string value = "Hello World!";
EXPECT_CALL(mockFoo, get_from_rpc())
.WillRepeatedly(Return(value));
string result1 = mockFoo.get_from_rpc();
string result2 = mockFoo.get_result();
cout << "get_result() return : " << result1 << endl;
cout << "get_result() return : " << result2 << endl;
EXPECT_EQ(value, result1);
EXPECT_EQ("reuslt is " + value, result2);
}
int main(int argc, char** argv)
{
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}
// g++ FooMain.cc -lpthread -lgtest -lgmock
更多的使用方法和api可以参考 gmock手册,gmock是通过纯虚函数 + 继承的方式实现mock的,对于非虚函数和普通函数gmock就不太好使了。
代码地址:
https://github.com/zhaozhengcoder/CoderNoteBook/tree/master/example_code/test/gmock
3. 查看代码覆盖率coverage
编译的时候,添加gcc --coverage参数
# 编译
gcc --coverage xxx_mian.c module.c
# 执行
./main
# 生成某个文件的代码覆盖结果
[linux@ src]~$gcov main.c
File 'main.c'
Lines executed:100.00% of 5
Creating 'main.c.gcov'
File '/usr/include/c++/8/iostream'
No executable lines
Removing 'iostream.gcov'
[linux@ src]~$gcov module_a.c
File 'module_a.c'
Lines executed:80.00% of 5
Creating 'module_a.c.gcov'
File '/usr/include/c++/8/iostream'
No executable lines
Removing 'iostream.gcov'
也可以具体查看文件覆盖的结果:
测试代码打包地址:
https://github.com/zhaozhengcoder/CoderNoteBook/tree/master/example_code/test/coverage
小结
写单测的原则:
1)测试用例能验证函数的正确性;
2)测试用例尽可能涵盖边界条件(例如遍历一个链表,头指针是空,只有一个节点,链表有N个节点,N是问题描述下允许的最大节点数等等);
3)一些异常和错误处理(例如往一个函数里传入空指针,传入空串,这个函数能否打印一些log,返回错误码,实现加法的Add函数如何检测和处理溢出等等)最理想的情况下,应该尽量多写测试用例,以保证代码功能的正确性符合预期,具有良好的容错性。如果代码较复杂,条件分支较多,测试用例最好能覆盖所有的分支路径。
4)单测持续集成,自动化运行
参考:
https://blog.jetbrains.com/rscpp/2015/09/01/unit-testing-google-test/
https://paul.pub/gtest-and-coverage/
https://blog.jetbrains.com/rscpp/2015/09/01/unit-testing-google-test/
https://www.zhihu.com/question/27313846/answer/130954707
http://senlinzhan.github.io/2017/10/08/gtest/
gmock:
https://github.com/google/googletest/blob/v1.8.x/googlemock/docs/CheatSheet.md
https://zhuanlan.zhihu.com/p/393954237