zoukankan      html  css  js  c++  java
  • 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
    ", t.base, t.name);
          (*t.func)();
          ++num;
        }
      }
      fprintf(stderr, "==== PASSED %d tests
    ", 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 的一个专属的测试用例类,而用宏定义实现这个过程主要是为了减少代码的冗余。因为测试用例非常多,如果对每个测试用例都专门编码定义一个类,那代码的冗余是让人无法接受的。其实这种做法也可以借鉴到其他地方来达到减少代码冗余的效果。

  • 相关阅读:
    git
    oracle object_id和data_object_id的区别
    statspack系列8
    statspack系列7
    statspack系列6
    statspack系列5
    statspack系列4
    statspack系列3
    statspack系列2
    MySQL源码之两阶段提交
  • 原文地址:https://www.cnblogs.com/evenleee/p/11990028.html
Copyright © 2011-2022 走看看