精简版本的C++单元测试框架 ,通过编写这个简单的测试框架,将有助于我们理解gtest。
1. 目录
类型 | 文件 | 说明 |
文件 | ./CMakeLists.txt | 整体项目工程文件 |
目录 | ./debian | deb包打包脚本目录,未实现 |
目录 | ./rpm | rpm打包目录,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宏定义,表示执行所有的单元测试,执行循序如下:
1. UnitTest::Run()
2. UnitTestImpl::RunAllTests()
3. TestCase::Run()
4. Test::Run()
5. 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);
}
单元测试理论
宏测试
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); true
ASSERT_FALSE(condition); EXPECT_FALSE(condition); false
1.2 二值检查
致命的断言 非致命性断言 条件
ASSERT_EQ(v1, v2); EXPECT_EQ(v1, v2); v1== v2
ASSERT_NE(v1, v2); EXPECT_NE(v1, v2); v1 != v2
ASSERT_LT(v1, v2); EXPECT_LT(v1, v2); v1 < v2
ASSERT_LE(v1, v2); EXPECT_LE(v1, v2); v1 <= v2
ASSERT_GT(v1, v2); EXPECT_GT(v1, v2); v1 > v2
ASSERT_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 type
ASSERT_ANY_THROW(statement); EXPECT_ANY_THROW(statement); statement throws an exception of any type
ASSERT_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" ] ;thenecho "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为浏览器的命令模式启动。
1. 整体设计
使用最精简的设计,我们就用两个类,够简单吧:
1. TestCase类
包含单个测试案例的信息。
2. UnitTest类
负责所有测试案例的执行,管理。
1.1 TestCase类
TestCase类包含一个测试案例的基本信息,包括:测试案例名称,测试案例执行结果,同时还提供了测试案例执行的方法。我们编写的测试案例都继承自TestCase类。
class TestCase
{
public:TestCase(const char* case_name) : testcase_name(case_name){}// 执行测试案例的方法virtual void Run() = 0;int nTestResult; // 测试案例的执行结果 const char* testcase_name; // 测试案例名称
};
1.2 UnitTest类
我们的UnitTest类和gtest的一样,是一个单件。我们的UnitTest类的逻辑非常简单:
1. 整个进程空间保存一个UnitTest 的单例。
2. 通过RegisterTestCase()将测试案例添加到测试案例集合testcases_中。
3. 执行测试案例时,调用UnitTest::Run(),遍历测试案例集合testcases_,调用案例的Run()方法
class UnitTest
{
public:// 获取单例static UnitTest* GetInstance(); // 注册测试案例TestCase* RegisterTestCase(TestCase* testcase);// 执行单元测试int Run();TestCase* CurrentTestCase; // 记录当前执行的测试案例int nTestResult; // 总的执行结果int nPassed; // 通过案例数int nFailed; // 失败案例数
protected:std::vector<TestCase*> testcases_; // 案例集合
};
2. 实现
下面是UnitTest类的实现:
UnitTest* UnitTest::GetInstance()
{static UnitTest instance;return &instance;
}TestCase* UnitTest::RegisterTestCase(TestCase* testcase)
{testcases_.push_back(testcase);return testcase;
}int UnitTest::Run()
{nTestResult = 1;for (std::vector<TestCase*>::iterator it = testcases_.begin();it != testcases_.end(); ++it){TestCase* testcase = *it;CurrentTestCase = testcase;std::cout << green << "======================================" << std::endl;std::cout << green << "Run TestCase:" << testcase->testcase_name << std::endl;testcase->Run();std::cout << green << "End TestCase:" << testcase->testcase_name << std::endl;if (testcase->nTestResult){nPassed++;}else{nFailed++;nTestResult = 0;}}std::cout << green << "======================================" << std::endl;std::cout << green << "Total TestCase : " << nPassed + nFailed << std::endl;std::cout << green << "Passed : " << nPassed << std::endl;std::cout << red << "Failed : " << nFailed << std::endl;return nTestResult;
}
五、NTEST宏
接下来定一个宏NTEST,方便我们写我们的测试案例的类。
#define TESTCASE_NAME(testcase_name) \testcase_name##_TEST#define NANCY_TEST_(testcase_name) \
class TESTCASE_NAME(testcase_name) : public TestCase \
{ \
public: \TESTCASE_NAME(testcase_name)(const char* case_name) : TestCase(case_name){}; \virtual void Run(); \
private: \static TestCase* const testcase_; \
}; \
\
TestCase* const TESTCASE_NAME(testcase_name) \::testcase_ = UnitTest::GetInstance()->RegisterTestCase( \new TESTCASE_NAME(testcase_name)(#testcase_name)); \
void TESTCASE_NAME(testcase_name)::Run()#define NTEST(testcase_name) \NANCY_TEST_(testcase_name)
六、RUN_ALL_TEST宏
然后是执行所有测试案例的一个宏:
#define RUN_ALL_TESTS() \UnitTest::GetInstance()->Run();
七、断言的宏EXPECT_EQ
这里,我只写一个简单的EXPECT_EQ :
#define EXPECT_EQ(m, n) \if (m != n) \{ \UnitTest::GetInstance()->CurrentTestCase->nTestResult = 0; \std::cout << red << "Failed" << std::endl; \std::cout << red << "Expect:" << m << std::endl; \std::cout << red << "Actual:" << n << std::endl; \}
八、案例Demo
够简单吧,再来看看案例怎么写:
#include "nancytest.h"int Foo(int a, int b)
{return a + b;
}NTEST(FooTest_PassDemo)
{EXPECT_EQ(3, Foo(1, 2));EXPECT_EQ(2, Foo(1, 1));
}NTEST(FooTest_FailDemo)
{EXPECT_EQ(4, Foo(1, 2));EXPECT_EQ(2, Foo(1, 2));
}int _tmain(int argc, _TCHAR* argv[])
{return RUN_ALL_TESTS();
}
整个一山寨版gtest,呵。执行一下,看看结果怎么样:
2.性能测试
可运行文件内容:
static void benchmark_slamopttest(benchmark::State& state)
{SlamOptBoundaries slamtest(15, 4, 4, "/home/usrname/slamtest/CameraRoadFrame", "/home/usrname/slamtest/result.json");size_t index = state.range(0);for(auto _: state){slamtest.SaveSameBoundaries(index);}
}
BENCHMARK(benchmark_slamopttest)->Arg(10);BENCHMARK_MAIN();
测试函数1:
void SlamOptBoundaries::SaveSameBoundaries(size_t const index_b)
{VecLocalBoundary vec_same_boundary_l;size_t index_bl = index_b;for (; index_bl < vec_boundaries_in_frames_.size(); index_bl += b_){// for(int i = 0; i < 10; ++i)// {// int j = i;// }if(vec_boundaries_in_frames_[index_bl].size() == 1){continue;}vec_same_boundary_l.push_back(vec_boundaries_in_frames_[index_bl]);}
}
测试函数2:
void SlamOptBoundaries::SaveSameBoundaries(size_t const index_b)
{VecLocalBoundary vec_same_boundary_l;size_t index_bl = index_b;for (; index_bl < vec_boundaries_in_frames_.size(); index_bl += b_){for(int i = 0; i < 10; ++i){int j = i;}if(vec_boundaries_in_frames_[index_bl].size() == 1){continue;}vec_same_boundary_l.push_back(vec_boundaries_in_frames_[index_bl]);}
}
测试结果
测试函数1:
2022-01-25T16:13:26+08:00
Running ./run
Run on (16 X 4800 MHz CPU s)
CPU Caches:L1 Data 32 KiB (x8)L1 Instruction 32 KiB (x8)L2 Unified 256 KiB (x8)L3 Unified 16384 KiB (x1)
Load Average: 0.28, 0.47, 0.51
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
***WARNING*** Library was built as DEBUG. Timings may be affected.
-------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------
benchmark_slamopttest/50 152 ns 152 ns 4207340
152ns内执行了4207340次,平均1ns执行27679次。
测试函数2:
2022-01-25T16:14:00+08:00
Running ./run
Run on (16 X 4800 MHz CPU s)
CPU Caches:L1 Data 32 KiB (x8)L1 Instruction 32 KiB (x8)L2 Unified 256 KiB (x8)L3 Unified 16384 KiB (x1)
Load Average: 0.21, 0.43, 0.50
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
***WARNING*** Library was built as DEBUG. Timings may be affected.
-------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------
benchmark_slamopttest/50 151 ns 151 ns 4321895
151ns内执行了4321895次,平均1ns执行28621次。
结论
函数1性能更好,虽然结果肉眼可见。
九、总结
本篇介绍性的文字比较少,主要是我们在上一篇深入解析gtest时已经将整个流程弄清楚了,而现在编写的nancytest又是其非常的精简版本,所有直接看代码就可以完全理解。希望通过这个Demo,能够让大家对gtest有更加直观的了解。回到开篇时所说的,我们没有必要每个人都造一个轮子,因为gtest已经非常出色的为我们做好了这一切。如果我们每个人都写一个自己的框架的话,一方面我们要付出大量的维护成本,一方面,这个框架也许只能对你有用,无法让大家从中受益。