前言

leveldb 是一个库,没有 main() 函数入口, 故非常难理清其中的代码逻辑。但好在库中有非常多的单元测试代码,帮助读者理解其中的各个模块的功能。然而,测试代码个人觉得一开始看时非常费解,特别是其中非常复杂的宏定义让人陷于云里雾里一般。研究 leveldb 的时间也有一段时间了,但一直都不想也不愿去弄懂。今天算是拿出破釜沉舟的勇气弄懂了这部分的原理,不能让谷歌大神辛苦写的测试代码没有发挥出它应有的价值。其实单元测试对于开发的作用非常重要的,这是毋庸置疑的。但我一直都没有去了解这部分的知识,平时自己写的代码都是只进行一些简单的调用测试就完事。

其实,单元测试的代码不好理解是因为代码里用了比较复杂的宏定义,如果对于宏定义的理解不够深刻的话理解起来非常困难。但如果耐心一点,将宏定义的代码全部进行字符替换,最后梳理起来其实非常简单。

源码分析

在 db/db_test.cc中,我们跟踪TEST一个单元测试的实现。首先,在 main() 函数中:

int main(int argc, char** argv) {
return leveldb::test::RunAllTests();
}

RunAllTests() 定义

int RunAllTests() {
int num = ;
if (tests != NULL) {
for (int i = ; 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 ;
}

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

leveldb单元测试之宏定义源码剖析的更多相关文章

  1. Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现

    声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程 ...

  2. Android源码剖析之Framework层升级版(窗口、系统启动)

    本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 看本篇文章之前,建议先查看: Android源码剖析之Framework层基础版 前面讲了frame ...

  3. 菜鸟nginx源码剖析 框架篇(一) 从main函数看nginx启动流程(转)

    俗话说的好,牵牛要牵牛鼻子 驾车顶牛,处理复杂的东西,只要抓住重点,才能理清脉络,不至于深陷其中,不能自拔.对复杂的nginx而言,main函数就是“牛之鼻”,只要能理清main函数,就一定能理解其中 ...

  4. 《STL源码剖析》相关面试题总结

    原文链接:http://www.cnblogs.com/raichen/p/5817158.html 一.STL简介 STL提供六大组件,彼此可以组合套用: 容器容器就是各种数据结构,我就不多说,看看 ...

  5. STL源码剖析——hashtable

    二叉搜索树具有对数时间的搜索复杂度,但是这样的复杂度是再输入数据有足够的随机性的假设上哈希表在插入删除搜索操作上也具有常数时间的表现,而且这种表现是以统计为基础,不需要依赖输入元素的随机性 hasht ...

  6. 面试题总结(三)、《STL源码剖析》相关面试题总结

    声明:本文主要探讨与STL实现相关的面试题,主要参考侯捷的<STL源码剖析>,每一个知识点讨论力求简洁,便于记忆,但讨论深度有限,如要深入研究可点击参考链接,希望对正在找工作的同学有点帮助 ...

  7. linux0.11内核源码剖析:第一篇 内存管理、memory.c【转】

    转自:http://www.cnblogs.com/v-July-v/archive/2011/01/06/1983695.html linux0.11内核源码剖析第一篇:memory.c July  ...

  8. Python开发【源码剖析】 List对象

    前言 本文探讨的Python版本为2.7.16,可从官网上下载,把压缩包Python-2.7.16.tgz解压到本地即可 需要基本C语言的知识(要看的懂) PyListObject对象 PyListO ...

  9. ucore 源码剖析

    lab1 源码剖析 从实模式到保护模式 初始化ds,es和ss等段寄存器为0 使能A20门,其中seta20.1写数据到0x64端口,表示要写数据给8042芯片的Output Port;seta20. ...

随机推荐

  1. 自制tomcat镜像

    1.编写dockerfile FROM centos MAINTAINER raygift@qq.com # 安装vim RUN yum -y install vim net-tools ENV BA ...

  2. <input type="file"> accept属性筛选文件类型

    如果你不希望用户上传任何类型的文件, 你可以使用 input 的 accept 属性. 设置支持 .doc / .docx / .xls / .xlsx / .pdf 格式: <input ty ...

  3. 【概率论】1-4:事件的的并集(Union of Events and Statical Swindles)

    title: [概率论]1-4:事件的的并集(Union of Events and Statical Swindles) categories: Mathematic Probability key ...

  4. ftp、sftp、vsftp、vsftpd、lftp以及一些网络客户端工具命令

    ftp 是File Transfer Protocol的缩写,文件传输协议,用于在网络上进行文件传输的一套标准协议,使用客户/服务器模式.它属于网络传输协议的应用层.了解更多ftp lftp :是一个 ...

  5. TCP首部的TimeStamp时间戳选项 转载

    TCP应该是以太网协议族中被应用最为广泛的协议之中的一个,这里就聊一聊TCP协议中的TimeStamp选项.这个选项是由RFC 1323引入的,该C建议提交于1992年.到今天已经足足有20个年头.只 ...

  6. window.innerHeight与document.documentElement.clientHeight与document.body.clientHeight区别

    window.innerHeight属于BOM(浏览器对象模型),获取的高度包含横向滚动条 document.documentElement.clientHeight属于文档对象模型,不包含横向滚动条 ...

  7. SVN的工作机制

    一.C/S结构 二.基本操作 1. 检出(Checkout)  把服务器端版本库内容完整下载到本地. 2. 更新(Update)  把服务器端相对于本地的新的修改下载到本地. 3. 提交(Comm ...

  8. intellj idea 常用快捷键

    1.command+shift+a 查找操作2.command+e 查找历史打开的文件3.command+n 查找类4.command+shift+n 查找文件

  9. [Tex学习笔记]让项目编号从4开始

    微信扫描如上二维码关注跟锦数学微信公众账号. 详情请见那里.

  10. 彻底搞清楚javascript中的require、import和export(js模块加载规范的前世今生)

    为什么有模块概念 理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块. 但是,Javascript不是一种模块化编程语言,在es6以前,它是不支持”类”(class),所以也 ...