简易测试框架
平时一直都是用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());
}
可以看到测试框架有三个基本的需求:
- 测试框架,用来运行所编写的测试样例
- 断言,用来判断测试结果是否符合预期
- 在出错时,可以输出相关错误信息,方便定位问题
根据这三个需求,我们来看看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;
}
- 首先定义了一个结构体Test,里面存放了类名,方法名,函数的指针(指向上面的_RunIt方法)
- 然后定义一个全局的数组,用来存放所有被注册的结构体。
- 实现注册方法,即根据提供的参数构造一个新的Test,并将其放入全局数组中
好,到这一步,可以看到测试框架基本已经成型了。简单的理解就是
- 定义一个宏来将每个测试样例定义为某个类的静态类方法
- 定义一个结构体,记录这个静态类方法的相关信息,包括类名,方法名,函数指针
- 定义一个全局结构体数组,记录所有的待测试静态方法,然后测试的时候遍历数组,调用这些方法即可 ,如下所示:
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,当然实现起来也简单,但是基本上也够用了。