最近在写so库相关的项目,由于so是针对接口的代码编程,因此需要写大量的测试代码,使用gtest来进行测试自己的代码是很好的一个方式,确保自己的代码在逻辑上尽量减少错误,如果配合着代码覆盖率一起那么对自己的程序有比较好的检查。一般的开发人员在自测上面都做得不是很充分,单元测试可以尽可能的减少代码错误以及奔溃。当然测试还可以使用valgrind 测试代码中是否有内存泄露。总之很爽。
操作系统为Debian操作系统。
需要安装gtest
sudo apt-get install libtest-dev
安装lcov代码覆盖率统计。
sudo apt-get install lcov
单元测试实操
项目构建
项目目录如下:
文件以及目录说明
类型 文件说明文件./CMakeLists.txt整体项目工程文件目录./debiandeb包打包脚本目录,未实现目录./rpmrpm打包目录,rpm打包的详细内容可以看链接目录./src源码目录,所有的源码都放在该目录下。文件./src/arvapi.h动态链接库的头文件。 文件./src/arvapi.cpp动态链接库的源文件。文件./src/CMakeLists.txt 动态链接库的g工程文件目录./tests所有的测试文件都放入这个目录中文件./tests/CMakeLists.txt测试工程文件,其中有可能包含模糊测试等。目录./tests/UnitTest单元测试目录目录./tests/UnitTest/src与源程序对应的目录文件./tests/UnitTest/src/CMakeLists.txt单元测试项目文件文件./tests/UnitTest/main.cpp单元测试主程序文件./tests/UnitTest/src/ut_arvapi.cpp
与原程序中的源文件一致。以ut开头一一对应源文件
例如:源文件名称为test.cpp,单元测试的源文件命名为ut_test.cpp
文件内容
源程序文件列表及说明
./CMakeLists.txt
该工程的文件的目的为了包含各个子工程文件,对于通用的环境变量测试也可以写在这里。
cmake_minimum_required(VERSION 3.9.5)
add_subdirectory(src)
add_subdirectory(tests)
**./src/CMakeLists.txt **
源程序项目工程文件。
# 设置变量,给LIB_NAME赋值为arv
set(LIB_NAME arv)
# 设定项目名称
project(${LIB_NAME})
# 设置编译源文件。
file(GLOB_RECURSE c_files RELATIVE ${PROJECT_SOURCE_DIR} *.cpp)
# 设置编译头文件
file(GLOB_RECURSE h_files RELATIVE ${PROJECT_SOURCE_DIR} *.h)
# 将源文件和头文件编译成共享库文件。${LIB_NAME}为库文件的文件名。
add_library(${LIB_NAME} SHARED ${h_files} ${c_files})
./src/arvapi.h
动态链接库的头文件。该文件定义了动态链接库的头文件。
#ifndef ARVAPI_H
#define ARVAPI_H
#ifdef __cplusplus
extern "C"{
#endif
int arv_add(int a,int b);
#ifdef __cplusplus
}
#endif
#endif // ARVAPI_H
./src/arvapi.cpp
源程序文件。
#include "arvapi.h"
#ifdef __cplusplus
extern "C"{
#endif
int arv_add(int a,int b){
return a+b;
}
#ifdef __cplusplus
}
#endif
单元测试测试文件列表及说明
./tests/CMakeLists.txt
该工程文件包含了所有的测试工程,本工程中只有单元测试,所以只是添加了单元测试,如果是还有模糊测试等其他类型的测试可以在tests目录下新建更多的测试工程。
add_subdirectory(UnitTest)
./tests/UnitTest/main.cpp
main函数包含文件,其实该文件不写gtest也可以运行。
#include <gtest/gtest.h>
int main(int argc,char * argv[]){
testing::InitGoogleTest(&argc,argv);
return RUN_ALL_TESTS();
}
RUN_ALL_TESTS宏定义,表示执行所有的单元测试,执行循序如下:
- UnitTest::Run()
- UnitTestImpl::RunAllTests()
- TestCase::Run()
- Test::Run()
- Test::TestBody()
如果main函数不适用return RUN_ALL_TESTS(); 直接使用RUN_ALL_TESTS();可能会出现警告如下图。
6: warning: ignoring return value of ‘int RUN_ALL_TESTS()’, declared with attribute warn_unused_result [-Wunused-result]
RUN_ALL_TESTS();
~~~~~~~~~~~~~^~
如果testing::InitGoogleTest(&argc,argv);没有调用会出现一下错误提示.
IMPORTANT NOTICE - DO NOT IGNORE:
This test program did NOT call testing::InitGoogleTest() before calling RUN_ALL_TESTS(). This is INVALID. Soon Google Test will start to enforce the valid usage. Please fix it ASAP, or IT WILL START TO FAIL.
** ./tests/UnitTest/CMakeLists.txt**
单元测试项目文件。
# 设置变量,给EXE_NAME 赋值为arv-test
set(EXE_NAME arv-test)
# 设置项目名称为${EXE_NAME}
project(${EXE_NAME})
# 引用Gtest项目
find_package(GTest REQUIRED)
# 引入Gtest项目的头文件
include_directories(${GTEST_INCLUDE_DIRS})
# 设置代码覆盖率的相关参数
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -fprofile-arcs -ftest-coverage -lgcov")
# 设置源文件目录地址,对于本项目地址的相对地址
set(SRCELATIVEPATH ../../)
include_directories(${PROJECT_SOURCE_DIR})
include_directories(${PROJECT_SOURCE_DIR}/${SRCELATIVEPATH}/src)
# 设置编译源文件,其中包含了源程序文件,以及单元测试的源文件
file(GLOB_RECURSE c_files RELATIVE ${PROJECT_SOURCE_DIR} main.cpp src/*.cpp ${SRCELATIVEPATH}/src/*.cpp)
# 设置编译头文件,其中包含了源程序的文件,以及单元测试的头文件。
file(GLOB_RECURSE h_files RELATIVE ${PROJECT_SOURCE_DIR} ${SRCELATIVEPATH}/src/*.h)
# 生成单元测试克执行程序
add_executable(${EXE_NAME} ${h_files} ${c_files})
# gtest需要依赖于gtest动态链接库。
target_link_libraries(
${EXE_NAME}
${GTEST_LIBRARIES}
${GTEST_MAIN_LIBRARIES}
)
./tests/UnitTest/src/ut_arv.cpp
单元测试文件,该文件针对与arv.cpp文件的单元测试用例集合。
如果还有其他的源文件可以按照命名要求进行设置。例如,XXX.cpp的源文件,单元测试文件可以为ut_XXX.cpp
#include "arvapi.h"
#include <gtest/gtest.h>
TEST(arv_add,add_h_a){
int res =arv_add(5,6);
EXPECT_EQ(res, 11);
}
源码地址 https://gitcode.net/arv002/qt/-/tree/master/arvapi
单元测试理论
宏测试
1.1 TEST
TEST宏的作用是创建一个简单测试。他的源码如下:
#define GTEST_TEST(test_case_name, test_name)\
GTEST_TEST_(test_case_name, test_name, \
::testing::Test, ::testing::internal::GetTestTypeId())
// Define this macro to 1 to omit the definition of TEST(), which
// is a generic name and clashes with some other libraries.
#if !GTEST_DONT_DEFINE_TEST
# define TEST(test_case_name, test_name) GTEST_TEST(test_case_name, test_name)
#endif
test_case_name:测试套件,通常用来定义一个行数名称。因为一个函数的测试可能不止一个那么我们可以通过定义套件的方式进行区分
test_name:测试名称。
例如:
我们测试一下函数 strcp(char * str1,char* str2);
// 测试字符串1为空的情况。
TEST(strcp,str1_is_null)
{
char * str1= nullptr;
char * str2 = "asdf";
strcp(str1,str2);
}
// 测试字符串2为空的情况。
TEST(strcp,str2_is_null)
{
char * str1= "asdf";
char * str2 = nullptr;
strcp(str1,str2);
}
密码套件将测试进行分类。
最终的测试名称为
strcp.str1_is_null
strcp.str2_is_null
1.2 TEST_F
TEST_F(test_fixture, test_name)
test_fixture:为测试的类名,并且他也会作为测试套件的名字,该类必须继承 testing::Test。
test_name:测试名称。
// Defines a test that uses a test fixture.
//
// The first parameter is the name of the test fixture class, which
// also doubles as the test case name. The second parameter is the
// name of the test within the test case.
//
// A test fixture class must be declared earlier. The user should put
// the test code between braces after using this macro. Example:
//
// class FooTest : public testing::Test {
// protected:
// 虚函数SetUp将在所有测试用例之前调用,当有变量需要初始化时,应当定义这个函数
// virtual void SetUp() { b_.AddElement(3); }
//
// Foo a_;
// Foo b_;
// };
//
// TEST_F(FooTest, InitializesCorrectly) {
// EXPECT_TRUE(a_.StatusIsOK());
// }
//
// TEST_F(FooTest, ReturnsElementCountCorrectly) {
// EXPECT_EQ(a_.size(), 0);
// EXPECT_EQ(b_.size(), 1);
// }
#define TEST_F(test_fixture, test_name)\
GTEST_TEST_(test_fixture, test_name, test_fixture, \
::testing::internal::GetTypeId<test_fixture>())
testing::Test提供接口SetUp,虚函数SetUp将在所有测试用例之前调用,当有变量需要初始化时,应当定义这个函数
断言
Gtest中,断言的宏可以理解为分为两类,一类是ASSERT系列,一类是EXPECT系列。
ASSERT_* 系列的断言(致命的断言),当检查点失败时,退出当前函数(注意:并非退出当前案例)。
EXPECT_* 系列的断言(非致命性断言),当检查点失败时,继续执行下一个检查点(每一个断言表示一个测试点)。
1.1 布尔型检查
致命的断言非致命性断言条件ASSERT_TRUE(condition);EXPECT_TRUE(condition);trueASSERT_FALSE(condition);EXPECT_FALSE(condition);false
1.2 二值检查
致命的断言非致命性断言条件ASSERT_EQ(v1, v2);EXPECT_EQ(v1, v2);v1== v2ASSERT_NE(v1, v2);EXPECT_NE(v1, v2);v1 != v2ASSERT_LT(v1, v2);EXPECT_LT(v1, v2);v1 < v2ASSERT_LE(v1, v2);EXPECT_LE(v1, v2);v1 <= v2ASSERT_GT(v1, v2);EXPECT_GT(v1, v2);v1 > v2ASSERT_GE(v1, v2);EXPECT_GE(v1, v2);v1 >= v2
1.3字符串检查
致命的断言非致命性断言条件ASSERT_STREQ(str1, str2);EXPECT_STREQ(str1,str2);
str1和str2两个C字符串有相同的内容
ASSERT_STRNE(str1, str2);EXPECT_STRNE(str1, str2);str1和str2两个C字符串有不同的内容ASSERT_STRCASEEQ(str1, str2);EXPECT_STRCASEEQ(str1, str2);两个内容在忽略大小写的前提下相等。ASSERT_STRCASENE(str1, str2);EXPECT_STRCASENE(str1, str2);两个内容在不忽略的大小写的前提下不相等。
*STREQ和*STRNE同时支持char和wchar_t类型的,STRCASEEQ和STRCASENE却只接收char***
1.4异常检查
致命的断言非致命性断言条件ASSERT_THROW(statement, exception_type);EXPECT_THROW(statement, exception_type);statement throws an exception of the given typeASSERT_ANY_THROW(statement);EXPECT_ANY_THROW(statement);statement throws an exception of any typeASSERT_NO_THROW(statement);EXPECT_NO_THROW(statement);statement doesn't throw any exception
1.5浮点检查
expected: 期望的浮点值
actual:最终的浮点值
致命的断言非致命性断言条件ASSERT_FLOAT_EQ(expected, actual);EXPECT_FLOAT_EQ(expected, actual);两浮点数相等ASSERT_DOUBLE_EQ(expected, actual);EXPECT_DOUBLE_EQ(expected, actual);想浮点数不等
在对比数据方面,我们往往会讨论到浮点数的对比。因为在一些情况下,浮点数的计算精度将影响对比结果,所以这块都会单独拿出来说。GTest对于浮点数的对比也是单独的
1.6相近值检查
致命的断言非致命性断言条件ASSERT_NEAR(val1, val2, abs_error);EXPECT_NEAR(val1, val2, abs_error);val1和val2之间的差值不超过给定的绝对误差
代码覆盖率
编译参数
# 设置代码覆盖率的相关参数
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -fprofile-arcs -ftest-coverage -lgcov")
统计脚本
#!/bin/bash
build_dir=$1
if [ -z "$build_dir" ] ;then
echo "error!请输入build目录,如果没有编译请先编译。"
echo "例如:$0 /home/uos/Desktop/build-tools-unknown-Debug"
exit 0
fi
cd $build_dir
lcov -d ./ -c -o init.info
lcov -a init.info -o total.info
lcov --remove total.info '*/usr/include/*' '*/usr/lib/*' '*/usr/lib64/*' '*/src/log/*' '*/tests/*' '*/usr/local/include/*' '*/usr/local/lib/*' '*/usr/local/lib64/*' '*/third/*' 'testa.cpp' -o final.info
genhtml -o cover_report --legend --title "lcov" --prefix=./ final.info
browser cover_report/index.html &
注意 : browser为浏览器的命令模式启动。
可以清楚的看到哪段代码没有被执行,以及代码覆盖率。
版权归原作者 三雷科技 所有, 如有侵权,请联系我们删除。