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 的一个专属的测试用例类,而用宏定义实现这个过程主要是为了减少代码的冗余。因为测试用例非常多,如果对每个测试用例都专门编码定义一个类,那代码的冗余是让人无法接受的。其实这种做法也可以借鉴到其他地方来达到减少代码冗余的效果。

 

posted @ 2019-12-05 17:04  evenleo  阅读(460)  评论(0编辑  收藏  举报