不会用gtest写测试用例?
不知道如何看自己的代码覆盖率?
一文全搞定。
1.Gtest安装
github下载gtest地址:
git clone https://github.com/google/googletest.git
2.Gtest编译
Cmake方式编译:
mkdir ~/gtest/gtest-build-x86
cd gtest-build-x86
cmake ../googletest-main -DCMAKE_INSTALL_PREFIX=./_install
make && make install
3.Example
一个简单的测试用例:
#include <iostream>
#include <gtest/gtest.h>
int sum(int a, int b) {
return a+b;
}
TEST(sum, testSum) {
EXPECT_EQ(5, sum(2, 3)); // 求合2+3=5
EXPECT_NE(3, sum(3, 4)); // 求合3+4 != 3
}
记得在cmake链接一下
add_executable(main main.cpp)
target_link_libraries(main gtest gtest_main)
多个测试用例记得添加:
add_test(NAME main COMMAND main --gtest_output=xml)
这样就能使用ctest一次性启动所有用例
一些小技巧也在这里添加,比如设置用例的时间限制:
add_test(
NAME main
COMMAND main --gtest_output=xml
TIMEOUT 10 # 设置时间限制为 10 秒
)
add_test常用参数
- NAME <testName>:指定测试的名称。
- COMMAND <command>:指定要运行的测试命令。
- CONFIGURATIONS <config>:指定要测试的配置。
- WORKING_DIRECTORY <dir>:指定测试的工作目录。
- ARGUMENTS <args>:指定传递给测试的参数。
- FAIL_REGULAR_EXPRESSION <regex>:指定一个正则表达式,如果测试的输出匹配该正则表达式,则认为测试失败。
- PASS_REGULAR_EXPRESSION <regex>:指定一个正则表达式,如果测试的输出匹配该正则表达式,则认为测试通过。
- EXCLUDE_FROM_ALL:指定该测试不会被包含在默认构建目标中。
- LABELS <label1> ...:指定测试的标签,用于组织和过滤测试。
- TIMEOUT <seconds>:指定测试的超时时间。
- RESOURCE_LOCK <resource>:指定测试所需的资源锁。
关于main函数问题:
在链接库的时候如果链接了gtest_main则无需写main函数,框架会自动帮你调用:
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
否则则需主动写出main函数,如果有必须在main函数里init的函数,检查一下链接gtest_main后是否会发生冲突。
4.gtest检查函数
前缀有两种:
ASSERT_ : 断言, 不通过检查则中断测试, 当在测试外使用时要求函数返回void.
EXPECT_ : 期望, 不通过检查并不中断测试.
test中对数组检查的测试后缀:
后缀 | 参数 | 条件 |
TRUE | c | c == true |
FALSE | c | c == false |
EQ | (a, b) | a == b |
NE | (a, b) | a != b |
LT | (a, b) | a < b |
LE | (a, b) | a <= b |
GT | (a, b) | a > b |
GE | (a, b) | a >= b |
FLOAT_EQ | (a, b) | float型 a ≈ b |
DOUBLE_EQ | (a, b) | double型 a ≈ b |
NEAR | (a, b, e) | abs(a - b) <= e |
HRESULT_SUCCEEDED | (h) | SUCCEEDED(h) == true |
HRESULT_FAILED | (h) | FAILED(h) == true |
对字符串的检查后缀:
后缀 | 参数 | 条件 |
STREQ | (a, b) | C字符串相等 |
STRNE | (a, b) | C字符串不相等 |
STRCASEEQ | (a, b) | C字符串忽略大小写相等 |
STRCASENE | (a, b) | C字符串忽略大小写不相等 |
可以通过<<操作符将一些重要信息输出:
for (int i = 0; i < x.size(); ++i)
{
EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;
}
输出结果中会在测试不通过时,定位打印在数值错误的后面,方便观看:
gtestdemo.cpp(25): error: Value of: y[i]
Actual: 4
Expected: x[i]
Which is: 3
Vectors x and y differ at index 2
5.事件机制
5.1 全局事件
要实现全局事件,必须写一个类,继承testing::Environment类,实现里面的SetUp和TearDown方法。
1. SetUp()方法在所有案例执行前执行
2. TearDown()方法在所有案例执行后执行
3. 类中变量所有用例都可访问
class FooEnvironment : public testing::Environment
{
public:
virtual void SetUp()
{
std::cout << "Foo FooEnvironment SetUP" << std::endl;
}
virtual void TearDown()
{
std::cout << "Foo FooEnvironment TearDown" << std::endl;
}
};
需要在main函数中通过testing::AddGlobalTestEnvironment方法将事件注册进来,也就是说,可以写很多个这样的类,然后将他们的事件都注册执行。
int _tmain(int argc, _TCHAR* argv[])
{
testing::AddGlobalTestEnvironment(new FooEnvironment);
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
5.2 TestSuite事件
写一个类,继承testing::Test,然后实现静态方法:
1. SetUpTestCase() 方法在第一个TestCase之前执行
2. TearDownTestCase() 方法在最后一个TestCase之后执行
3. 类中变量所有用例都可访问
class FooTest : public testing::Test {
protected:
static void SetUpTestCase() {
shared_resource_ = new ;
}
static void TearDownTestCase() {
delete shared_resource_;
shared_resource_ = NULL;
}
// 所有使用TestSuit的Test Case都可以直接访问这些变量和方法
static T* shared_resource_;
};
**编写测试案例时,我们需要使用TEST_F这个宏,第一个参数必须是我们上面类的名字,代表一个TestSuite,否则上述方法和变量均无效,用例只会被视为一个普通的case。
5.3 TestCase事件
TestCase事件是挂在每个案例执行前后的,实现方式和上面的几乎一样,不过需要实现的是SetUp方法和TearDown方法:
1. SetUp():在每个使用了TestSuite类的Test Case运行开始前,都会调用 SetUp,这里可以初始化
2. TearDown():在每个使用了TestSuite类的Test Case运行结束后,都会调用 TearDown
3. 类中变量所有用例都可访问
class FooCalcTest:public testing::Test
{
protected:
virtual void SetUp()
{
m_foo.Init();
}
virtual void TearDown()
{
m_foo.Finalize();
}
FooCalc m_foo;
};
TEST_F(FooCalcTest, HandleNoneZeroInput)
{
EXPECT_EQ(4, m_foo.Calc(12, 16));
}
TEST_F(FooCalcTest, HandleNoneZeroInput_Error)
{
EXPECT_EQ(5, m_foo.Calc(12, 16));
}
**编写测试案例时,同样需要使用TEST_F这个宏,否则无效。
6.参数化
添加一个类,继承testing::TestWithParam<T>,其中T就是你需要参数化的参数类型:
class IsPrimeParamTest : public::testing::TestWithParam<int>
{
};
拿到参数的值后,具体做些什么样的测试
TEST_P(IsPrimeParamTest, HandleTrueReturn)
{
int n = GetParam();
EXPECT_TRUE(IsPrime(n));
}
告诉gtest测试的参数范围是什么;
使用INSTANTIATE_TEST_CASE_P这宏来告诉gtest你要测试的参数范围:
INSTANTIATE_TEST_CASE_P(TrueReturn, IsPrimeParamTest, testing::Values(3, 5, 11, 23, 17));
7.运行gtest用例
在build下执行ctest即可
# execute test
echo "execute test"
cd $CUR_DIR/build
ctest
echo "test success"
只是不知道为什么这样启动信息就不会打印在控制台上了......
要看调试信息的话,在build/Testing/Temporary下会生成三个文件,LastTest.log里就是控制台所有的打印信息,LastTestsFailed.log记录着最后一次运行中失败的测试用例。
8.lcov工具
常用的cmake用法:
add_compile_options(-fprofile-arcs -ftest-coverage)
set(ALTERNATE_LIBS "gcov")
target_link_libraries(
main PUBLIC
${ALTERNATE_LIBS}
)
统计覆盖率信息:
# get coverage info
echo "generate coverage"
rm -rf $CUR_DIR/cov
mkdir -p $CUR_DIR/cov
cd $CUR_DIR/cov
lcov \
-d ../build \
--exclude '/usr/include/*' \
--rc branch_coverage=1 \
--rc lcov_excl_br_line='LOG*' \
--rc lcov_excl_line='LOG*' \
-c \
--ignore-errors mismatch \
--ignore-errors unused \
-o test_coverage.info
- -d ../build: 指定要分析的目录为 ../build,即构建目录。
- --exclude '/usr/include/*': 排除路径中包含 /usr/include/ 的文件。
- --rc branch_coverage=1: 设置分支覆盖率为开启状态。
- --rc lcov_excl_br_line='LOG*': 排除以 LOG* 开头的分支覆盖率行。
- --rc lcov_excl_line='LOG*': 排除以 LOG* 开头的覆盖率行。
- -c: 执行代码覆盖率分析。
- --ignore-errors mismatch: 忽略不匹配的错误。
- --ignore-errors unused: 忽略未使用的错误。
- -o test_coverage.info: 将生成的覆盖率信息保存到 test_coverage.info 文件中。
--ignore-errors mismatch和--ignore-errors unused看情况添加。
生成的test_coverage.info还需要处理一下成可视化,使用 genhtml 工具将之前生成的测试覆盖率报告 test_coverage.info 转换为 HTML 格式的可视化报告
genhtml test_coverage.info -o output/ --rc branch_coverage=1
--rc branch_coverage=1: 设置分支覆盖率为开启状态,确保生成的报告中包含分支覆盖率信息。