leveldb源码分析之单元测试实现
在util/env_test.cc中,我们跟踪TEST一个单元测试的实现。首先,在main函数中:
// 一个测试示例
int main(int argc, char** argv) {
return leveldb::test::RunAllTests();
}
点击RunAllTests(),查看源码:
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;
}
} // 遍历整个Tests
fprintf(stderr, "==== PASSED %d tests\n", num);
return 0;
}
首先获取LEVELDB_TESTS的环境变量:先不看 if (matcher != NULL)的流程,是打印出test.base和 test.name,然后执行test中的回调函数(*t.func)(),最后打印了成功执行的单元测试数目。我们看看,Test结构体的定义:
struct Test {
const char* base; //TEST宏中的base
const char* name; //
void (*func)(); // 回调
};
在 RunAllTests中出现的tests是std::vector
// EnvTest应该就是base, ReopenAppendableFile就是name
TEST(EnvTest, ReopenAppendableFile) {
std::string test_dir;
ASSERT_OK(env_->GetTestDirectory(&test_dir));
std::string test_file_name = test_dir + "/reopen_appendable_file.txt";
.....
}
在看看TEST定义的宏:实际上是一个宏:
#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()
这个宏比较长,对于每一个每一个TEST单元实际上都是声明了一个类。注意TCONCAT(TEST, name)也是一个宏,表示连接_TEST_ 和name,来给这个类取个独一无二的名字:
#define TCONCAT(a,b) TCONCAT1(a,b)
#define TCONCAT1(a,b) a##b
下面这个函数中的宏,定义了类t,并调用了其Run_()方法。
static void _RunIt() { \
TCONCAT(_Test_,name) t; \
t._Run(); \
}
而
bool TCONCAT(_Test_ignored_,name) = ::leveldb::test::RegisterTest(#base, #name, &TCONCAT(_Test_,name)::_RunIt);
这句一条语句,调用RegisterTest函数,并注册该类的_RunIt方法,将注册的值给了_Test_igonored, 最后是实现void TCONCAT(TEST, name)::_Run()的方法,实现的内容通过就是TEST(xxx, yyyy)大括号后面的内容。很精彩。RegisterTest应该猜得到,怎么实现,就是push一个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(xxxx, yyy) {} 来定义一段单元测试代码,思路是这样的,TEST(xxxx, yyy)其实定义的是一个宏 ,{}后面是要测试的代码,其实就是就把他当成一个函数,每声明这个宏就可以定义了一个函数并注册到全局的tests中去。
单元测试中少不了宏,就是判断ASSERT_TRUE, ASSERT_FALSE之类的,我们看看其定义了了哪些宏:
#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的临时对象对应的各个方法,我们看看该类的实现:
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& 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;
}
};