leveldb单元测试之宏定义源码剖析
前言
leveldb 是一个库,没有 main() 函数入口, 故非常难理清其中的代码逻辑。但好在库中有非常多的单元测试代码,帮助读者理解其中的各个模块的功能。然而,测试代码个人觉得一开始看时非常费解,特别是其中非常复杂的宏定义让人陷于云里雾里一般。研究 leveldb 的时间也有一段时间了,但一直都不想也不愿去弄懂。今天算是拿出破釜沉舟的勇气弄懂了这部分的原理,不能让谷歌大神辛苦写的测试代码没有发挥出它应有的价值。其实单元测试对于开发的作用非常重要的,这是毋庸置疑的。但我一直都没有去了解这部分的知识,平时自己写的代码都是只进行一些简单的调用测试就完事。
其实,单元测试的代码不好理解是因为代码里用了比较复杂的宏定义,如果对于宏定义的理解不够深刻的话理解起来非常困难。但如果耐心一点,将宏定义的代码全部进行字符替换,最后梳理起来其实非常简单。
源码分析
在 db/db_test.cc中,我们跟踪TEST一个单元测试的实现。首先,在 main() 函数中:
int main(int argc, char** argv) { return leveldb::test::RunAllTests(); }
RunAllTests() 定义
int RunAllTests() { int num = 0; if (tests != NULL) { for (int i = 0; i < tests->size(); i++) { const Test& t = (*tests)[i]; fprintf(stderr, "==== Test %s.%s\n", t.base, t.name); (*t.func)(); ++num; } } fprintf(stderr, "==== PASSED %d tests\n", num); return 0; }
Test 定义
struct Test { const char* base; const char* name; void (*func)(); };
看其中最简单的一个 TEST 的代码
TEST(DBTest, Empty) { ASSERT_TRUE(db_ != NULL); ASSERT_EQ("NOT_FOUND", Get("foo")); }
看 TEST 定义,是一个较为复杂的宏定义
#define TCONCAT(a,b) TCONCAT1(a,b) #define TCONCAT1(a,b) a##b #define TEST(base,name) \ class TCONCAT(_Test_,name) : public base { \ public: \ void _Run(); \ static void _RunIt() { \ TCONCAT(_Test_,name) t; \ t._Run(); \ } \ }; \ bool TCONCAT(_Test_ignored_,name) = \ ::leveldb::test::RegisterTest(#base, #name, &TCONCAT(_Test_,name)::_RunIt); \ void TCONCAT(_Test_,name)::_Run() // Register the specified test. Typically not used directly, but // invoked via the macro expansion of TEST. extern bool RegisterTest(const char* base, const char* name, void (*func)());
在宏定义中 # 表示将后面的参数替换成字符串,如:#abc 为 "abc"。 ## 表示粘连符,即将 a 和 b 连成一个字符串,比如:a = abc, b = 123, a##b 为 abc123。#define 为预处理命令,定义了一个标识符及一个串,在源程序中每次遇到该标识符时,均以定义的串代换它。预编译期间进行宏替换:
class _Test_Empty : public DBTest { public: void _Run(); static void _RunIt() { _Test_Empty t; t._Run(); } }; bool _Test_ignored_name = ::leveldb::test::RegisterTest("DBTest", "Empty", &_Test_Empty::_RunIt); // 为全局变量,在 main() 函数运行前执行 void _Test_Empty::_Run() { ASSERT_TRUE(db_ != NULL); ASSERT_EQ("NOT_FOUND", Get("foo")); }
RegisterTest() 定义,这个是注册测试用例的作用,tests 是一个全局 std::vector<Test>指针,存储测试用例。
bool RegisterTest(const char* base, const char* name, void (*func)()) { if (tests == NULL) { tests = new std::vector<Test>; } Test t; t.base = base; t.name = name; t.func = func; tests->push_back(t); return true; }
以上的代码实现其实非常巧妙,主要目的是用 TEST(xxx, yyy) {} 来定义一段单元测试代码 。思路是这样的,TEST(xxx, yyy)其实定义的是一个宏 ,{} 后面是要测试的代码,其实就是就把他当成一个函数,每声明一个宏就可以定义了一个函数并注册到全局的 tests 中去。这个过程中会定义一个 xxxyyy 的一个专属的测试用例类,而用宏定义实现这个过程主要是为了减少代码的冗余。因为测试用例非常多,如果对每个测试用例都专门编码定义一个类,那代码的冗余是让人无法接受的。其实这种做法也可以借鉴到其他地方来达到减少代码冗余的效果。