通用makefile是如何炼成的(X)—— 导入单元测试

经过这么长的时间,我们的通用makefile已基本构建完成,下面使用一个具体的Examle,作为如何使用的说明。

我们这个例子为之前写的hello world编写单元测试。单元测试的工具使用gtest,比较简单嘛。

1. 导入gtest文件,生成libgtest.a

从官网上下载gtest源码包,解压后,里面有个fused-src目录,在里面就是gtest文件夹,包括1个头文件gtest.h, 两个源文件gtest_all.cc, gtest_main.cc。我们直接使用这三个文件,它与主src目录基本等效的,只是将各种头文件、源文件打包到一个文件中而已,也正是因为这样,使用gtest时,也更方便一些,不用include一堆的头文件。


话不多说,下面开始工作

第一步是重命名gtest_all.cc, gtest_main.cc为gtes_all.cpp, gtest_main.cpp. 因为我们通用makefile还没有那么聪明,目前只能识别.cpp后缀。

第二步,我们来编译一下这三个头文件,看看是否可以正常编译。

一般习惯的,我喜欢单独为测试文件建立一个独立的文件夹,这里我就直接src目录同等位置下新建一个test目录,当查看gtest_all.cpp, gtest_main.cpp中发现“include "gtest/gtest.h"”这样的语句,这就暗示我们需要将gtest_main.cpp, gtest_all.cpp 放在与gtest目录同等位置下。

然后添加一个module.mk,( 在build/templates目录下有module.mk的模板,拷贝一份到这里,免得写得辛苦)。修改MODULE_NAME, SRC_FILES, 以及最后的编译目标是可执行文件gtest.exe。然后记得在main.mk中 include test/moduel.mk

通用makefile是如何炼成的(X)—— 导入单元测试


于是我们的第一个应用,编译一个可执行文件已经成功搞定了,很简单,只要加入源文件,稍微改一下module.mk就可以了。

细心的同学可能已经发现了,编译起来有点慢,主要是gtest_all.cpp实在是有点大,所以接下来我们要将它变成静态lib库,以后编译test时只要链接一下lib库就可以了,不用再重新编译。

基本不用修改,只要增加一个module-gtestlib.mk

#module-gtestlib.mk
MODULE_PATH			:= $(call current_path)
MODULE_NAME			:= gtest

## 定义product定制文件
PRODUCT_SPECS 		:= #TODO
## 定义platform定制文件
PLATFORM_SPECS 		:= #TODO
## 定义target定制文件
TARGET_SPECS 		:= #TODO

## 生成产品、平台相关的信息,在此之前不要使用相关的变量,如CXX, CPPFLAGS,会直接覆盖。
include $(BUILD_CONFIGURE)

## 定义源文件列表
SRC_FILES:= $(MODULE_PATH)/gtest-all.cpp

## 调用目标编译规则
$(call build_target,$(BUILD_STATIC_LIBRARY),$(MODULE_PATH)/lib/libgtest.a)

注意到它与module.mk的不同,它的编译目标是生成静态库lib/libtest.a。同样将这个makefile include到main.mk中,就可以编译生成libgtest.a

为了可以测试上面编译生成的静态库是否有效,我们修改原先的module.mk, 改为直接链接libgtest.a

# module.mk

...
## 定义源文件列表
SRC_FILES:= $(MODULE_PATH)/gtest_main.cpp
LDLIBS += $(MODULE_PATH)/lib/libgtest.a

## 调用目标编译规则
$(call build_target,$(BUILD_EXECUTABLE),$(MODULE_PATH)/gtest.exe)

验证结果当然是ok啦,要不然哥纯粹是找砖拍

2. 在单独的test目录下做单元测试

从现在开始,我们在单元测试中将直接使用gtest.h, 和libgtest.a。下面就为helloworld编写一个测试用例。因为想不到什么好内容,所以干脆将makefile中产品信息和版本号打印出来,

// hello.cpp
std::string product_vendor()
{
	return PRODUCT_VENDOR;
}

std::string product_version()
{
	return PRODUCT_VERSION;
}
正确输出分别是Sample 和 1.0

在test目录下新增一个HelloTest.cpp,内容如下

#include "gtest/gtest.h"
#include <string>
#include "../src/hello/hello.h"

TEST(hello, product_vendor)
{
	ASSERT_TRUE(product_vendor() == std::string("Sample"));
}

TEST(hello, product_version)
{
	ASSERT_TRUE(product_version() == "1.0");
}

同时修改module.mk,将HelloTest.cpp, 以及他所依赖的源文件hello.cpp包含进去 (通用makefile是如何炼成的(X)—— 导入单元测试, 这里包括上面引用hello.h,着实比较丑陋,但个人表示也很无奈,有哪位大侠知道的话,还望指点迷津)

## 定义源文件列表
SRC_FILES:= $(MODULE_PATH)/gtest_main.cpp 			$(MODULE_PATH)/HelloTest.cpp 			$(MODULE_PATH)/hello.cpp
LDLIBS += $(MODULE_PATH)/lib/libgtest.a

## 调用目标编译规则
$(call build_target,$(BUILD_EXECUTABLE),$(MODULE_PATH)/gtest.exe)

编译后运行,就会提示2 test ok

 3. 将测试文件与源文件放在一起

可能也有同学喜欢将测试文件与源文件放在一起,青菜萝卜各有所爱,我们的makefile虽然简单,不过这样的需求总是要想办法满足的。

因为这时,gtest.h要被各个模块的测试文件引用,为了简单起见,我们把gtest/gtest.h放到专门的头文件目录inc,在makefile中使用"-Iinc"选项,保证可以使用以include <gtest/gtest.h>的形式引用。对libgtest.a, 我们也放到专门的lib目录下。最后在hello目录下新建HelloTest.cpp, 内容同刚才一样。 添加相应的makefile,详细内容看贴图

通用makefile是如何炼成的(X)—— 导入单元测试

我们的编译目标只是lib库,因为可能有很多模块,当然你也可以每个模块一个单元测试,这其实也是我所中意的形式。这里为了说一个现象,稍微复杂一点点,我们在src目录下同样新建一个module-test.mk,这个module-test.mk负责生成最终的可执行文件test.exe

# module.mk
#
# Created on: 2013-12-21
#     Author: lenovo

MODULE_PATH			:= $(call current_path)
MODULE_NAME			:= test

## 定义product定制文件
PRODUCT_SPECS 		:= #TODO
## 定义platform定制文件
PLATFORM_SPECS 		:= #TODO
## 定义target定制文件
TARGET_SPECS 		:= #TODO

## 生成产品、平台相关的信息,在此之前不要使用相关的变量,如CXX, CPPFLAGS,会直接覆盖。
include $(BUILD_CONFIGURE)

## 定义源文件列表
SRC_FILES:= $(MODULE_PATH)/gtest_main.cpp
CXXFLAGS += -Iinc
LDLIBS += lib/libgtest.a lib/hellotest.a

## 调用目标编译规则
$(call build_target,$(BUILD_EXECUTABLE))

编译运行,发现确实有test.exe生成,但是HelloTest.cpp中的两个测试并没有执行,这是为什么呢? 这是因为makefile好吃懒做,当他发现编程生成test.exe时,并不依赖于HelloTest.cpp中的任何内容时,就不会包含HelloTest.o,在这个例子中,也就是根本不会链接hellotest.a中的任何内容。知道原因之后,呵呵,那我们就只能给他下套了

/*
 * HelloTest.cpp
 *
 *  Created on: 2014-1-12
 *      Author: lenovo
 */

#include "gtest/gtest.h"
#include <string>
#include "hello.h"
// 增加诱导条件
void include_hellotest() {}
TEST(hello, product_vendor)
{
	ASSERT_TRUE(product_vendor() == std::string("Sample"));
}
。。。

// gtest_main.cpp

#include <iostream>

#include "gtest/gtest.h"
extern void include_hellotest();

GTEST_API_ int main(int argc, char **argv) {
  std::cout << "Running main() from gtest_main.cc\n";
  include_hellotest(); // 通过函数调用,强制引用HelloTest中的内容
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

再次编译运行,2 test ok


4. 同时编译生成test.exe 和产品

最后,可能有同学要问了,所有的单元测试文件,会构成一个个module-test.mk, 本身的源文件也有module.mk, 如果我想编译生成正常的产品以及对应的单元测试,那需要怎么做呢?

嗯,方法很多,

你可以在main.mk中按对添加 src/module.mk src/module-test.mk, src/hello/module.mk  src/hello/module-test.mk, 如此

可能觉得有麻烦,那也不要紧,修改每一个模块下的module.mk, 将原来的module.mk重命名为module-product.mk, 然后module.mk的内容修改为

include module-product.mk module-test.mk
当include module.mk时,相当于同时编译正常产品和单元测试。


每次都要两个module-xxx.mk, 能不能就一个module.mk, 也可以,直接把module-test.mk的内容添加到module.mk末尾就可以了。只是注意删除MODULE_PATH这一行

# module.mk
#
# Created on: 2013-12-21
#     Author: lenovo

MODULE_PATH			:= $(call current_path)
MODULE_NAME			:= hello

。。。
## 定义了如何生成静态库的通用规则
$(call build_target,$(BUILD_STATIC_LIBRARY), $(MODULE_PATH)/hello.a)

## 这里不能再使用$(call current_path),会出错。实际上您也没有必要重新定义,MODULE_PATH没有改变,还是上面的值。
#MODULE_PATH			:= $(call current_path)
MODULE_NAME			:= gtest

## 定义product定制文件
PRODUCT_SPECS 		:= #TODO
## 定义platform定制文件
PLATFORM_SPECS 		:= #TODO
## 定义target定制文件
TARGET_SPECS 		:= #TODO

。。。
## 定义源文件列表
SRC_FILES:= $(MODULE_PATH)/hello.cpp 			$(MODULE_PATH)/HelloTest.cpp
CXXFLAGS += -Iinc

## 调用目标编译规则
$(call build_target,$(BUILD_STATIC_LIBRARY),lib/hellotest.a)

一切就是这么简单,因为这是一个通用的makefile, simple but flexible

over





整个commonmakefile代码库已经完整的上传到github, 上面有完整详细的修改记录。

https://github.com/crylearner/CommonMakefile

其中gtest branch包含这个测试例子;默认分支是主干master


通用makefile是如何炼成的(X)—— 导入单元测试

上一篇:微机接口课程设计报告-模拟汽车控制系统


下一篇:高内聚,低耦合