基于gtest、gmock、mockcpp和lcov的C语言LLT工程 —— LLT构造和lcov查看覆盖率实例
由于工作原因,之前在CI这一块一直是依照公司流程走的,LLT这一块都是照猫画虎,对于整体框架自己没有一个完整的概念,最近有时间,研究了一下整体的逻辑框架,在此记录一下。
关于gtest,gmock和mockcpp,这里不再细讲,知道gtest,gmock是google的一套用于C/C++ LLT的框架即可,要用到mockcpp是因为gmock其实只能用于对对象函数的mocker,不能对C代码中的一般函数进行mcoker,这个在后面的代码中可以看出来。
一、准备工作
1. googletest、googlemock
下载:https://github.com/google/googletest
2. mockcpp
https://code.google.com/archive/p/mockcpp/downloads
3. 编译mockcpp
最新的2.6版本的mockcpp只需要解压然后进入根目录,然后:
cmake . //生成Makefile
make install
这里需要把libmockcpp.a保存下来,后面会用到;
/usr/local/include/文件夹下面的所有文件也需要保存下来;
4. 编译googletest
直接在下载地根目录进行
cmke .
make
即可得到gmock, gtest编译结果和静态库:
保存libgtest.a和libgmock.a后面用于静态链接;
二、建立测试工程
1. 创建GtestLearn工程,这里是我们需要测试的代码,目录结构如下:
mian.c
1 #include <stdio.h> 2 #include "func.h" 3 4 int main(int argc, char **argv) { 5 int ret = 0; 6 struct test_t test; 7 8 ret = add(1, 2); 9 printf("Get add result: %d\n", ret); 10 11 test.a = 10; 12 test.b = 12; 13 ret = add_struct(&test); 14 printf("Get add struct result: %d\n", ret); 15 16 test.p_func = NULL; 17 ret = test_struct_func(&test); 18 printf("Get test struct result: %d\n", ret); 19 20 ret = test_stub_func(); 21 printf("Get test stub func result: %d\n", ret); 22 23 return 0; 24 }
func.h
#ifndef GTESTLEARN_FUNC_H #define GTESTLEARN_FUNC_H struct test_t { int a; int b; int (*p_func)(struct test_t *test); }; int add(int a, int b); int multi(int a, int b); int add_struct(struct test_t *test); int test_struct_func(struct test_t *test); int test_stub_func(); #endif //GTESTLEARN_FUNC_H
func.c
#include <stdio.h> #include "ex_func.h" #include "func.h" int add(int a, int b) { printf("start to compute the sum of a %d and b %d\n", a, b); return a + b; } int multi(int a, int b) { printf("start to compute the multi of a %d and b %d\n", a, b); return a * b; } int add_struct(struct test_t *test) { int sum; int multi_v; printf("start to compute the sum of a %d and b %d\n", test->a, test->b); sum = test->a + test->b; multi_v = multi(test->a, test->b); if (sum > multi_v) { return sum; } return multi_v; } int test_struct_func(struct test_t *test) { if (test->p_func == NULL) { printf("get null func pointer\n"); return 0xFFFF; } printf("start ro run test_struct_func with a %d b %d\n", test->a, test->b); return test->p_func(test); } int test_stub_func() { int ret; int a = 0; ret = ex_get_value(&a); if (ret == 0xFFFF) { printf("get extern value failed, ret %d\n", ret); return ret; } printf("get extern value succeed, ex value %d\n", a); return ret; }
ex_func.h
#ifndef GTESTLEARN_EX_FUNC_H #define GTESTLEARN_EX_FUNC_H int ex_get_value(int *a); #endif //GTESTLEARN_EX_FUNC_H
ex_func.c
#include "ex_func.h" int ex_get_value(int *a) { *a = 101010; return 0; }
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(GtestLearn C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_FLAGS "${CAMKE_C_FLAGS} -fprofile-arcs -ftest-coverage")
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/)
set(SOURCE_FILES
main.c
include/func.h
func.c
include/ex_func.h
include/ex_func.c)
add_executable(GtestLearn ${SOURCE_FILES})
target_link_libraries(GtestLearn ${LIBRARIES})
这里保证通过
cmake .
make
然后执行,预期正确。
2. 创建测试工程GtestLearnLLT
独立一个工程是因为在正式开发环境中,做LLT的代码往往是与业务代码分离的,更好的模拟真实使用环境。
目录结构:
这里将googlemock,googletest和mockcpp放在third_party文件夹,都是由之前下载的源码解压而来,其中:
mockcpp只需要保留3rdparty文件夹,include文件用之前编译的头文件代替(准备工作第3步);lib目录里为之前编译产生的静态库;
stubs文件夹保存的是桩函数;
googlemock和googletest新建lib目录,存放之前编译的静态库;
main.cpp
#include <stdio.h> #include "gtest/gtest.h" GTEST_API_ int main(int argc, char **argv) { printf("Running main() from gtest_main.cc\n"); testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
gtest_ut.cpp
extern "C" { #include <stdio.h> #include <stdlib.h> #include "func.h" } #include <limits.h> #include <mockcpp/mockcpp.hpp> #include "gmock/gmock.h" #include "gtest/gtest.h" // using namespace std; using namespace testing; class GtestUt : public testing::Test { protected: void SetUp() override { std::cout << "--Gtest_Ut SetUP--" << std::endl; } void TearDown() override { std::cout << "--Gtest_Ut TearDown--" << std::endl; } }; class Mock_FOO { public: MOCK_METHOD1(mock_test_struct_func, int(struct test_t *test)); }; Mock_FOO mocker; int mock_test_struct_func(struct test_t *test) { return mocker.mock_test_struct_func(test); } TEST_F(GtestUt, ut_add_01) { int ret; ret = add(1, 2); EXPECT_EQ(3, ret); } TEST_F(GtestUt, ut_add_02) { int ret; struct test_t test; test.a = 1; test.b = 1; MOCKER(multi) .expects(atMost(20)) .will(returnValue(100)); ret = add_struct(&test); EXPECT_EQ(ret, 100); GlobalMockObject::verify(); } TEST_F(GtestUt, ut_add_03) { int ret; struct test_t test; test.a = 10; test.b = 11; MOCKER(multi) .expects(atMost(20)) .will(returnValue(20)); ret = add_struct(&test); EXPECT_EQ(ret, 21); GlobalMockObject::verify(); } TEST_F(GtestUt, ut_add_04) { int ret; int a, b; struct test_t test; test.a = 10; test.b = 11; test.p_func = mock_test_struct_func; EXPECT_CALL(mocker, mock_test_struct_func(&test)).WillRepeatedly(Return(10)); ret = test_struct_func(&test); EXPECT_EQ(ret, 10); GlobalMockObject::verify(); } TEST_F(GtestUt, ut_add_05) { int ret; int ex_value; ret = test_stub_func(); EXPECT_EQ(ret, 1011); }
my_stubs.c
#include <stdio.h> #include "ex_func.h" int ex_get_value(int *a) { if (a == NULL) { printf("get null pointer %p\n", a); return 0xFFFF; } *a = 1011; printf("run stub func, get value %d\n", *a); return *a; }
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(GtestLearnLLT)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CAMKE_CXX_FLAGS} -std=c++11 -pthread -fprofile-arcs -ftest-coverage")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_LLT -fprofile-arcs -ftest-coverage")
set(THIRD_PARTY_PATH ${CMAKE_CURRENT_SOURCE_DIR}/third_party)
link_directories(${THIRD_PARTY_PATH}/mockcpp/lib
${THIRD_PARTY_PATH}/googlemock/lib
${THIRD_PARTY_PATH}/googletest/lib)
#${THIRD_PARTY_PATH}/googletest/
include_directories(${THIRD_PARTY_PATH}/googletest/include/
${THIRD_PARTY_PATH}/googlemock/include/
${THIRD_PARTY_PATH}/mockcpp/include/
${THIRD_PARTY_PATH}/mockcpp/3rdparty/
${THIRD_PARTY_PATH}/googlemock/
${CMAKE_CURRENT_SOURCE_DIR}/../GtestLearn/include/
${CMAKE_CURRENT_SOURCE_DIR}/stubs/)
#${THIRD_PARTY_PATH}/googletest/src/gtest-all.cc
set(SRC_FILES
${CMAKE_CURRENT_SOURCE_DIR}/../GtestLearn/func.c
${CMAKE_CURRENT_SOURCE_DIR}/../GtestLearn/main.c
${CMAKE_CURRENT_SOURCE_DIR}/stubs/my_stubs.c
${THIRD_PARTY_PATH}/googlemock/src/gmock-all.cc
gtest_ut.cpp
main.cpp)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin")
add_executable(GtestLearnLLT ${SRC_FILES})
target_link_libraries(GtestLearnLLT libmockcpp.a libgmock.a libgtest.a)
编译执行:
cmake .
make
执行结果:
注意:这里还是在编译的时候编译了gmock而不是直接链接,是因为直接使用静态库,会导致mock对象泄露的问题,目前没有定位到原因:
三、代码分析
1. 不需要打桩的普通函数LLT:
TEST_F(GtestUt, ut_add_01) { int ret; ret = add(1, 2); EXPECT_EQ(3, ret); }
这个比较简单,没有什么说的,依靠gtest框架,检查函数功能的正确性;
2. 使用mockcpp来mocker一般C函数:
TEST_F(GtestUt, ut_add_02) { int ret; struct test_t test; test.a = 1; test.b = 1; MOCKER(multi) .expects(atMost(20)) .will(returnValue(100)); ret = add_struct(&test); EXPECT_EQ(ret, 100); GlobalMockObject::verify(); }
使用mockcpp提供的MOCKER来对需要验证的目标函数add_struct中的multi来进行mocker,进行打桩,常用来覆盖不同的异常分支。
3. 使用自定义桩函数
int ex_get_value(int *a)
{
if (a == NULL) {
printf("get null pointer %p\n", a);
return 0xFFFF;
}
*a = 1011;
printf("run stub func, get value %d\n", *a);
return *a;
}
TEST_F(GtestUt, ut_add_05)
{
int ret;
int ex_value;
ret = test_stub_func();
EXPECT_EQ(ret, 1011);
}
对test_stub_func里面调用的ex_get_value来进行打桩,来实现自己想要完成的代码逻辑;
4. 使用gmock来打桩对象类函数,本例中的对象类函数是放在结构体test_t中的:
struct test_t { int a; int b; int (*p_func)(struct test_t *test); };
使用gmock打桩:
class Mock_FOO { public: MOCK_METHOD1(mock_test_struct_func, int(struct test_t *test)); }; Mock_FOO mocker; int mock_test_struct_func(struct test_t *test) { return mocker.mock_test_struct_func(test); }
用Mock_FOO类的mock_test_struct_func函数来mocker结构体test_t的成员函数p_func:
TEST_F(GtestUt, ut_add_04) { int ret; int a, b; struct test_t test; test.a = 10; test.b = 11; test.p_func = mock_test_struct_func; EXPECT_CALL(mocker, mock_test_struct_func(&test)).WillRepeatedly(Return(10)); ret = test_struct_func(&test); EXPECT_EQ(ret, 10); GlobalMockObject::verify(); }
代码中 test.p_func = mock_test_struct_func; 使用Mock_FOO的函数来替代原结构体中的成员函数,通过 EXPECT_CALL 在test_struct_func函数调用结构体成员函数p_func的时候使用Mock_FOO的 mock_test_struct_func 来代替,达到打桩的目的。
四、使用Lcov来查看LLT覆盖率
1、lcov下载与安装
wget http://downloads.sourceforge.net/ltp/lcov-1.9.tar.gz
tar -zxvf lcov-1.9.tar.gz
cd lcov-1.9
make install
2、LLT工程CMakeLists.txt修改
添加 -fprofile-arcs -ftest-coverage 为lcov的工作生成.gcda .gcno文件
set(CMAKE_CXX_FLAGS "${CAMKE_CXX_FLAGS} -std=c++11 -pthread -fprofile-arcs -ftest-coverage") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_LLT -fprofile-arcs -ftest-coverage")
3、lcov需要代码运行之后才能生成.gcda和.gcno文件
编译运行LLT工程之后可以看到生成的文件:
可以看到我们需要进行LLT的代码main.c和func.c的gcda和gcno文件再CMakeFiles文件下;
4、使用lcov获取代码被执行次数的信息:
lcov -d ./CMakeFiles/GtestLearnLLT.dir/home/xxxx/GtestLearn/ -t bin/GtestLearnLLT -o test.info -b . -c
-d 指向你需要进行统计覆盖率代码所在的目录
-t 为测试工程可执行文件所在目录
-o 生成的输出文件
-b 相对路径,之前的路径基于此路径
-c 获取覆盖率信息
5、使用genhtml生成网页
genhtml -o result/ test.info
-o 指向生成的html信息存放的目录
test.info是之前生成的包含覆盖率信息的文件
6、查看生成的目录
7、使用浏览器打开index.html
点开一个文件查看LLT覆盖情况:
遇到的问题:
*.gcda:stamp mismatch with notes file
解决方法:
当.gcda文件比.gcno文件更新时,您可能会收到“戳记不匹配”.
使用 hexdump -e '"%x\n"' -s8 -n4 main.c.gcda 查看时间戳,会发现gcda文件和gcno文件时间戳不一致;
它主要有两个原因:
1.您可能在运行测试之后和跟踪文件生成之前重新构建代码.
2.二进制文件可以在一台机器上构建,测试在其他机器上运行,其时间早于构建机器.
我的解决方法:
rm -rf CMakeFiles/ 彻底删除这个文件夹,删除所有.gcda和 .gcno文件
然后重新编译和执行LLT工程就可以了