今天开始读leveldb代码系列一

平时一直都是用gtest/gmock来测试程序,但是没有好好研究过这个测试框架的原理,后来读到leveldb的代码,才知道原来是这么回事,高手的代码真的是读起来颇有收益,下面就记录一下实现
代码在util/testharness.h util/testharness.cc里

测试框架的需求

平时使用gtest时,需要创建一个类继承自::testing::Test类,在这里面可以使用setup和teardown做一下测试初始化和收尾的操作,并且之后的TEST_F宏都需要将该类名作为参数传进去。例如:

class ClientTests : public ::testing::Test {
public:
    void SetUp() {
    // 每个测试开始设置操作
    }
   
    void TearDown() {
    // 每个测试结束时清理操作
    }
    
private:
    base::EndPoint _local_addr;
};

/**
 * Test Cases
 */

TEST_F(ClientTests, init_test) {
    ASSERT_EQ(0, Client::init());
}

可以看到测试框架有三个基本的需求:

  1. 测试框架,用来运行所编写的测试样例
  2. 断言,用来判断测试结果是否符合预期
  3. 在出错时,可以输出相关错误信息,方便定位问题

根据这三个需求,我们来看看leveldb里自己实现的测试框架代码。

leveldb测试框架实现

先来看点简单的,leveldb如何实现断言的。

// An instance of Tester is allocated to hold temporary state during
// the execution of an assertion.
class Tester {
 private:
  bool ok_;
  const char* fname_;
  int line_;
  std::stringstream ss_;

 public:
  Tester(const char* f, int l)
      : ok_(true), fname_(f), line_(l) {
  }

  ~Tester() {
    if (!ok_) {
      fprintf(stderr, "%s:%d:%s\n", fname_, line_, ss_.str().c_str());
      exit(1);
    }
  }

  Tester& Is(bool b, const char* msg) {
    if (!b) {
      ss_ << " Assertion failure " << msg;
      ok_ = false;
    }
    return *this;
  }

  Tester& IsOk(const Status& s) {
    if (!s.ok()) {
      ss_ << " " << s.ToString();
      ok_ = false;
    }
    return *this;
  }

#define BINARY_OP(name,op)                              \
  template <class X, class Y>                           \
  Tester& name(const X& x, const Y& y) {                \
    if (! (x op y)) {                                   \
      ss_ << " failed: " << x << (" " #op " ") << y;    \
      ok_ = false;                                      \
    }                                                   \
    return *this;                                       \
  }

  BINARY_OP(IsEq, ==)
  BINARY_OP(IsNe, !=)
  BINARY_OP(IsGe, >=)
  BINARY_OP(IsGt, >)
  BINARY_OP(IsLe, <=)
  BINARY_OP(IsLt, <)
#undef BINARY_OP

  // Attach the specified value to the error message if an error has occurred
  template <class V>
  Tester& operator<<(const V& value) {
    if (!ok_) {
      ss_ << " " << value;
    }
    return *this;
  }
};

#define ASSERT_TRUE(c) ::leveldb::test::Tester(__FILE__, __LINE__).Is((c), #c)
#define ASSERT_OK(s) ::leveldb::test::Tester(__FILE__, __LINE__).IsOk((s))
#define ASSERT_EQ(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsEq((a),(b))
#define ASSERT_NE(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsNe((a),(b))
#define ASSERT_GE(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsGe((a),(b))
#define ASSERT_GT(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsGt((a),(b))
#define ASSERT_LE(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsLe((a),(b))
#define ASSERT_LT(a,b) ::leveldb::test::Tester(__FILE__, __LINE__).IsLt((a),(b))

如代码所示,用一个类Tester来记录测试过程中的状态,比如失败或者成功。我们看看它内部成员:

bool ok_;   // 用来记录本次测试是否全部通过
const char* fname_; // 用来记录当前正在被测试的文件名
int line_;   // 记录错误行数
std::stringstream ss_;  // 记录错误信息

而BINARY_OP宏用来声明一些比较操作方法,可以用来实现断言作用。后面是我们常用的一些断言,都是基于Tester类来实现的。因此,leveldb实际上是将需求2和3绑定在一个类里,我们调用类的方法来实现断言功能,如果有错误,就将错误信息记录到类的成员变量里,打印输出。当然其实也可以使用一些全局变量来记录这些信息,然后断言就是普通的判断操作符,在失败时,更新下全局变量,并打印也是可以的。

需求1怎么实现的呢,我们来看看leveldb的代码

#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)());

它是通过宏TEST来定义一个新的类,继承自自己定义的测试类(这样可以复用一些公告资源,比如文件,数据),并定义两个公开方法_Run和_RunIt,其中_RunIt是用来将执行测试程序的,_Run是写的测试程序。
这个宏还定义了两个方法,分别是用来注册该测试程序的方法,以及_Run方法的定义。

再来看看是怎么注册测试程序的:

struct Test {
  const char* base;
  const char* name;
  void (*func)();
};
std::vector<Test>* tests;
}

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;
}

  1. 首先定义了一个结构体Test,里面存放了类名,方法名,函数的指针(指向上面的_RunIt方法)
  2. 然后定义一个全局的数组,用来存放所有被注册的结构体。
  3. 实现注册方法,即根据提供的参数构造一个新的Test,并将其放入全局数组中

好,到这一步,可以看到测试框架基本已经成型了。简单的理解就是

  1. 定义一个宏来将每个测试样例定义为某个类的静态类方法
  2. 定义一个结构体,记录这个静态类方法的相关信息,包括类名,方法名,函数指针
  3. 定义一个全局结构体数组,记录所有的待测试静态方法,然后测试的时候遍历数组,调用这些方法即可 ,如下所示:
int RunAllTests() {
  const char* matcher = getenv("LEVELDB_TESTS");

  int num = 0;
  if (tests != NULL) {
    for (size_t i = 0; i < tests->size(); i++) {
      const Test& t = (*tests)[i];
      if (matcher != NULL) {
        std::string name = t.base;
        name.push_back('.');
        name.append(t.name);
        if (strstr(name.c_str(), matcher) == NULL) {
          continue;
        }
      }
      fprintf(stderr, "==== Test %s.%s\n", t.base, t.name);
      (*t.func)();
      ++num;
    }
  }
  fprintf(stderr, "==== PASSED %d tests\n", num);
  return 0;
}

小结

leveldb的测试框架实现非常简短精悍,可以看到跟gtest这样的大型框架比起来还有很多不足,比如不支持setup和teardown,当然实现起来也简单,但是基本上也够用了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值