gtest是google的一套开源的c++单元测试框架,可以方便程序员对自己的代码进行单元测试。学习这套框架,除了墙外的官方文档之外,我强力推荐玩转Google开源C++单元测试框架Google Test系列 。
这系列的博客已经将gtest讲的十分详细了,所以我这篇博客就不再详细介绍gtest的基本使用方法了,而是通过一个简单的例子,介绍一下如何在实际的项目中使用它。同时也会通过分析gtest的源代码,使得大家能够更好的理解gtest的工作原理
需要测试的类 首先我实现了一个简单的类TwoDimensionalMark,它的功能相当于bool二维数组,可以将二维下标标记为true或者false。只不过它内部使用位运算,一个字节可以记录八个标记数据,可以大量节省内存。它的声明如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class TwoDimensionalMark { public : TwoDimensionalMark(int row, int col, bool flag = false ); TwoDimensionalMark(const TwoDimensionalMark& cpy); ~TwoDimensionalMark(); const TwoDimensionalMark& operator =(const TwoDimensionalMark& cpy); void clean(bool mark); void set (int x, int y, bool mark) ; bool check (int x, int y) const ; int getRow () ; int getCol () ; ... };
完整的测试代码 我先把完整的测试代码放在这里,可以先简单浏览一遍。
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 #include "TwoDimensionalMark.h" #include "gtest/gtest.h" #include <cstdlib> #include <vector> using namespace std ;struct MarkSize { MarkSize(int r, int c): row(r),col(c){} int row, col; }; class TestWithMarkSize : public testing::TestWithParam<MarkSize>{public : struct Coord { int x, y; }; vector <vector <bool > > CreateContrast(int row, int col, bool flag){ vector <vector <bool > > contrast(row); for (int i = 0 ; i < row; i++){ contrast[i] = vector <bool >(col, flag); } return contrast; } vector <Coord> CreateRandCoords(int row, int col){ int yRange = row * 3 ; int xRange = col * 3 ; int numRandCoord = row * col / 2 ; vector <Coord> coords; for (int i = 0 ; i < numRandCoord; i++){ int x = (rand() % xRange) - col; int y = (rand() % yRange) - row; coords.push_back({ x, y }); } return coords; } void SetFlag (TwoDimensionalMark* mark, vector <vector <bool > >* contrast, const MarkSize& size, const vector <Coord>& coords, bool flag) { for (auto c : coords){ mark->set (c.x, c.y, flag); if (c.x >= 0 && c.x < size.col && c.y >= 0 && c.y < size.row){ (*contrast)[c.y][c.x] = flag; } } } }; INSTANTIATE_TEST_CASE_P(RowLTCol, TestWithMarkSize, testing::Values(MarkSize(200 , 100 ), MarkSize(50 , 30 ))); INSTANTIATE_TEST_CASE_P(ColLTRow, TestWithMarkSize, testing::Values(MarkSize(10 , 100 ), MarkSize(3 , 10 ))); INSTANTIATE_TEST_CASE_P(RowEQCol, TestWithMarkSize, testing::Values(MarkSize(100 , 100 ), MarkSize(3 , 3 ))); TEST_P(TestWithMarkSize,testWithTrueClean){ MarkSize size = GetParam(); TwoDimensionalMark mark (size.row, size.col) ; mark.clean(true ); vector <vector <bool > > contrast = CreateContrast(size.row, size.col, true ); for (int i = 0 ; i < size.row; i++){ for (int j = 0 ; j < size.col; j++){ ASSERT_TRUE(mark.check(j, i)); ASSERT_TRUE(contrast[i][j]); } } vector <Coord> coords = CreateRandCoords(size.row, size.col); SetFlag(&mark, &contrast, size, coords, false ); for (auto c : coords){ ASSERT_FALSE(mark.check(c.x,c.y)); if (c.x >= 0 && c.x < size.col && c.y >= 0 && c.y < size.row){ ASSERT_FALSE(contrast[c.y][c.x]); } } for (int y = 0 ; y < size.row; y++) for (int x = 0 ; x < size.col; x++){ ASSERT_EQ(contrast[y][x], mark.check(x, y)); } } TEST_P(TestWithMarkSize, testWithFalseClean){ MarkSize size = GetParam(); TwoDimensionalMark mark (size.row, size.col) ; mark.clean(false ); vector <vector <bool > > contrast = CreateContrast(size.row, size.col, false ); for (int i = 0 ; i < size.row; i++){ for (int j = 0 ; j < size.col; j++){ ASSERT_FALSE(mark.check(j, i)); ASSERT_FALSE(contrast[i][j]); } } vector <Coord> coords = CreateRandCoords(size.row, size.col); SetFlag(&mark, &contrast, size, coords, true ); for (auto c : coords){ if (c.x >= 0 && c.x < size.col && c.y >= 0 && c.y < size.row){ ASSERT_TRUE(contrast[c.y][c.x]); ASSERT_TRUE(mark.check(c.x, c.y)); } else { ASSERT_FALSE(mark.check(c.x, c.y)); } } } int main (int argc, char * argv[]) { testing::InitGoogleTest(&argc, argv); int result = RUN_ALL_TESTS(); getchar(); return result; }
传入多个参数 为了覆盖各种大小的情况(行和列数量相等,行多于列,行少于列),需要定义多个不同行列数的TwoDimensionalMark。但如果每个测试用例都手动硬编码的话将会有许多的重复代码,这个时候就可以使用参数化的方法去将行列数作为参数传入各个测试用例中,详细的介绍看这里 。
这篇博客只写到了一个参数的处理。但我们这里需要传入行数和列数两个参数应该怎么办? 其实很方法也很简单,首先定义一个结构体MarkSize用于保存行数和列数
1 2 3 4 struct MarkSize { MarkSize(int r, int c): row(r),col(c){} int row, col; }
用它作为TestWithParam的模板参数,再声明一个类继承于它。
1 2 class TestWithMarkSize : public testing::TestWithParam<MarkSize>{}
然后传入参数,这里我定义了三组参数,每组两个。分别对应行数比较多,列数比较多,行数列数一样多三种情况:
1 2 3 4 5 6 INSTANTIATE_TEST_CASE_P(RowLTCol, TestWithMarkSize, testing::Values(MarkSize(200 , 100 ), MarkSize(50 , 30 ))); INSTANTIATE_TEST_CASE_P(ColLTRow, TestWithMarkSize, testing::Values(MarkSize(10 , 100 ), MarkSize(3 , 10 ))); INSTANTIATE_TEST_CASE_P(RowEQCol, TestWithMarkSize, testing::Values(MarkSize(100 , 100 ), MarkSize(3 , 3 )));
这样一来,只要是 test_case_name 为 TestWithMarkSize 的测试用例都可以使用GetParam方法获取传入的参数了:
1 2 3 4 5 6 7 8 9 TEST_P(TestWithMarkSize,testWithTrueClean){ MarkSize size = GetParam(); ... } TEST_P(TestWithMarkSize, testWithFalseClean){ MarkSize size = GetParam(); ... }
TEST_P源码解析 TEST_P 宏的定义如下
1 2 3 4 5 6 7 # define TEST_P(test_case_name, test_name) \ class GTEST_TEST_CLASS_NAME_(test_case_name, test_name) \ : public test_case_name { \ ... }; \ ... void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestBody()
GTEST_TEST_CLASS_NAME_又是个什么东西?其实它的功能十分简单,就是将类名拼接出来而已,它的定义如下:
1 2 #define GTEST_TEST_CLASS_NAME_(test_case_name, test_name) \ test_case_name##_##test_name##_Test
所以我们使用 TEST_P(TestWithMarkSize,testWithTrueClean){…} 实际上就是定义了一个 TestWithMarkSize 的子类 TestWithMarkSize_testWithTrueClean_Test。
而花括号里面实际上就是void TestWithMarkSize_testWithTrueClean_Test::TestBody()的实现。
也就是说,实际上我们的测试用例都是TestWithMarkSize的子类。所以我们可以将一些公共的方法和数据结构定义在TestWithMarkSize内,只要将他们声明为protected或者public,就能在TEST_P(TestWithMarkSize,testWithTrueClean){…} 和 TEST_P(TestWithMarkSize,testWithFalseClean){…} 内使用。
这样既可以提炼重复代码,又能将它们的作用范围限定在测试用例中,防止提炼出来的函数或者定义的数据结构放在全局影响实际的功能代码。
如我这里定义的 Coord 结构体和CreateContrast、CreateRandCoords 和 SetFlag 方法。
而GetParam()的定义如下:
1 2 3 4 5 6 7 8 static const ParamType* parameter_;const ParamType& GetParam () const { GTEST_CHECK_(parameter_ != NULL ) << "GetParam() can only be called inside a value-parameterized test " << "-- did you intend to write TEST_P instead of TEST_F?" ; return *parameter_; }
既然 const T* WithParamInterface::parameter_ 是有一个类静态成员变量,那就不难理解为什么所有继承于 TestWithMarkSize 的测试用例都能拿到同样的参数了。
测试用例分析 要知道 TwoDimensionalMark 实际上的功能是和bool二维数组基本一样的,所以我们就很自然的想到使用一个对比用的bool二维数组作为参照,去测试 TwoDimensionalMark 的功能究竟有没有bug。
TEST_P(TestWithMarkSize,testWithTrueClean) 的测试分下面三个步骤:
1.根据传入的数组大小,创建了一个 TwoDimensionalMark 和用两重 vector 实现的 bool 二维数组。
2.将它们全部用true填充,然后验证每一个下标的数据是否均为 true,如此去测试TwoDimensionalMark::clean(true) 的功能
3.随机生成一些坐标,将 TwoDimensionalMark 和 bool 二维数组对应这些坐标的数据都设为 false 。然后检测设置的位置的数据是否都为false,还有对比两者每个下标的数据是否相等。以测试 set 和 check 方法是否正确。同时因为这些下标有些是超出范围之外的,也能测试 TwoDimensionalMark 对超出范围的操作是否正确
代码如下:
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 TEST_P(TestWithMarkSize,testWithTrueClean){ MarkSize size = GetParam(); TwoDimensionalMark mark (size.row, size.col) ; mark.clean(true ); vector <vector <bool > > contrast = CreateContrast(size.row, size.col, true ); for (int i = 0 ; i < size.row; i++){ for (int j = 0 ; j < size.col; j++){ ASSERT_TRUE(mark.check(j, i)); ASSERT_TRUE(contrast[i][j]); } } vector <Coord> coords = CreateRandCoords(size.row, size.col); SetFlag(&mark, &contrast, size, coords, false ); for (auto c : coords){ ASSERT_FALSE(mark.check(c.x,c.y)); if (c.x >= 0 && c.x < size.col && c.y >= 0 && c.y < size.row){ ASSERT_FALSE(contrast[c.y][c.x]); } } for (int y = 0 ; y < size.row; y++) for (int x = 0 ; x < size.col; x++){ ASSERT_EQ(contrast[y][x], mark.check(x, y)); } }
TEST_P(TestWithMarkSize, testWithFalseClean) 和上面的差不多,只不过换成一开始用 false 填充,之后设置随机下标的数据为 true,读者可以自己查看代码,这里就不详细分析了。
测试结果 运行测试代码得到下面的结果:
全部测试均通过!
完整代码:https://github.com/bluesky466/GTestDemo (为了方便,我直接将实现写在了头文件里,好孩子不要学~)