目录
项目背景:
在项目升级优化中对于一些在重要模块建立必要的单元测试极为重要,如常用的计费模块,在优化代码迭代升级时保证计费前后的一致性及其重要,但是对于众多的计费规则,边界情况众多,每次升级都进行全面测试特别耗费开发时间;再此引入单元测试gTest,建立完备的边界单元测试用例,在每一次迭代后可以快速的检测是否优化迭代导致计费规则前后不一致问题,保证在项目提测前减少bug。
gtest的使用
开源框架:gtest是一个跨平台的C++单元测试框架,由google公司发布。gtest是为在不同平台上为编写C++测试而生成的。它提供了丰富的断言、致命和非致命判断、参数化、”死亡测试”等等。
断言:
在gtest中,是通过断言(assertion)来判断代码实现的功能是否符合预期。断言的结果分为success、non-fatal failture和fatal failture。
根据断言失败的种类,gtest提供了两种断言函数:
success:即断言成功,程序的行为符合预期,程序继续向下允许
non-fatal failure:即非致命断言失败,但是程序没有直接crash,而是继续向下运行。
fatal failure:断言失败,程序直接crash,后续的测试案例不会被运行。
gtest提供了宏函数ASSERT_XXX(expected, actual)。
在写单元测试时,更加倾向于使用EXPECT_XXX,因为ASSERT_XXX是直接crash退出的,可能会导致一些内存、文件资源没有释放,因此可能会引入一些bug。
具体的EXPECT_XXX、ASSERT_XXX函数及其判断条件,如下两个表。
| ASSER | EXPECT | Verifies |
| ASSERT_TRUE(condition); | EXPECT_TRUE(condition); | condition is true |
| ASSERT_FALSE(condition) | EXPECT_FALSE(condition) | condition is false |
| ASSERT | EXPECT | Condition |
| ASSERT_EQ(val1, val2); | EXPECT_EQ(val1, val2); | val1 == val2 |
| ASSERT_NE(val1, val2); | EXPECT_NE(val1, val2); | val1 != val2 |
| ASSERT_LT(val1, val2); | EXPECT_LT(val1, val2); | al1 < val2 |
| ASSERT_LE(val1, val2); | EXPECT_LE(val1, val2); | val1 <= val2 |
| ASSERT_GT(val1, val2); | EXPECT_GT(val1, val2); | val1 > val2 |
| ASSERT_GE(val1, val2); | EXPECT_GE(val1, val2); | val1 >= val2 |
- TEST 最基础的测试单元,无需考虑 fixture(测试夹具),直接编写测试代码。常用在小规模测试或独立函数的验证.
- TEST_F 基于测试夹具,可以在多个测试之间共享测试环境或公共逻辑。常用在 重复性测试场景,如有初始化/清理等相同需求
- TEST_P 参数化测试:可使用宏 INSTANTIATE_TEST_CASE_P 生成测试。常用在测试相同代码逻辑但不同输入数据(如功能性、边界性测试)
- TYPED_TEST 类型化测试:可针对多种类型以统一的测试逻辑进行验证。 常用在同时对多种类型或模板进行一致性验证(如容器、数值类型)
TEST
最基础的测试单元,无需考虑 fixture(测试夹具),直接编写测试代码。 小规模测试或独立函数的验证。 TEST(TestCaseName, TestName) TestCaseName测试用例名称,自定义通常取测试函数或类名,TestName测试名称,自定义。
namespace TEST_1
{
TEST(FactorialTest, Negative) {
// This test is named "Negative", and belongs to the "FactorialTest"
// test case.
EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
// <TechnicalDetails>
//
// EXPECT_EQ(expected, actual) is the same as
//
// EXPECT_TRUE((expected) == (actual))
//
// except that it will print both the expected value and the actual
// value when the assertion fails. This is very helpful for
// debugging. Therefore in this case EXPECT_EQ is preferred.
//
// On the other hand, EXPECT_TRUE accepts any Boolean expression,
// and is thus more general.
//
// </TechnicalDetails>
}
// Tests factorial of 0.
TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); }
// Tests factorial of positive numbers.
TEST(FactorialTest, Positive) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
}
// Tests IsPrime()
// Tests negative input.
TEST(IsPrimeTest, Negative) {
// This test belongs to the IsPrimeTest test case.
EXPECT_FALSE(IsPrime(-1));
EXPECT_FALSE(IsPrime(-2));
EXPECT_FALSE(IsPrime(INT_MIN));
}
// Tests some trivial cases.
TEST(IsPrimeTest, Trivial) {
EXPECT_FALSE(IsPrime(0));
EXPECT_FALSE(IsPrime(1));
EXPECT_TRUE(IsPrime(2));
EXPECT_TRUE(IsPrime(3));
}
// Tests positive input.
TEST(IsPrimeTest, Positive) {
EXPECT_FALSE(IsPrime(4));
EXPECT_TRUE(IsPrime(5));
EXPECT_FALSE(IsPrime(6));
EXPECT_TRUE(IsPrime(23));
}
//Test Vector
TEST(VectorTest, memberFun)
{
vector<int>vec{ 1,2,3,4,5,6,7,8,9,0 };
EXPECT_EQ(10, vec.size());
EXPECT_GT(vec[4], 4);
EXPECT_GE(vec[3], 4);
EXPECT_LT(6, vec[9]);
}
}
TEST_F
基于测试夹具,可以在多个测试之间共享测试环境或公共逻辑。 重复性测试场景,如有初始化/清理等相同需求。
TEST_F(TestFixtureName, TestName)用于测试需要共享测试数据的用例,需要测试夹具类TestFixtureName来管理测前的准备和测试后的清理工作等,夹具在gtest中的作用就是为每个TEST都执行一些同样的操作。它的第一个参数TestFixtureName是个类,需要继承testing::Test,同时根据需要实现以下两个虚函数(TestCase事件):
- virtual void SetUp():在TEST_F中测试案例之前运行;
- virtual void TearDown():在TEST_F之后运行。
可以类比对象的构造函数和析构函数。这样,同一个TestFixtureName下的每个TEST_F都会先执行SetUp,最后执行TearDwom。
此外,testing::Test还提供了两个static函数(TestSuite事件 ):
- static void SetUpTestSuite():在第一个TEST之前运行
- static void TearDownTestSuite():在最后一个TEST之后运行
测试类继承testing::Test,复写 TestCase事件和TestSuite事件。
class StlAlogrithm : public testing::Test { // 继承了 testing::Test
protected:
//TestSuite事件
static void SetUpTestSuite() {
std::cout << "run before first case..." << std::endl;
}
static void TearDownTestSuite() {
std::cout << "run after last case..." << std::endl;
}
//TestCase事件
//挂在每个案例执行前后的,需要实现的是SetUp方法和TearDown方法。SetUp方法在每个TestCase之前执行;TearDown方法在每个TestCase之后执行。
virtual void SetUp() override {
std::cout << "enter into SetUp()" << std::endl;
vec1.push_back(1);
vec1.push_back(2);
vec1.push_back(3);
vec1.push_back(4);
vec1.push_back(5);
vec1.push_back(6);
vec2.resize(vec1.size(), 0);
vec3.resize(vec1.size(), 0);
}
virtual void TearDown() override {
std::cout << "exit from TearDown" << std::endl;
}
vector<int> vec1;
vector<int> vec2;
vector<int> vec3;
};
TEST_F测试代码
namespace TEST_2
{
TEST_F(StlAlogrithm, copy)
{
//TEST_F中可以使用StlAlogrithm的成员函数和成员
std::copy(vec1.begin(), vec1.end(), vec2.begin());
EXPECT_EQ(vec1, vec2);
auto it = std::copy_if(vec1.begin(), vec1.end(), vec3.begin(), [](int n) { return (n & 1) == 0; });
vec3.resize(it - vec3.begin());// shrink container to new size
//断言宏中不能直接写lambda表达式
auto allEvenFunc = [&]() ->bool {
for (auto iter : vec3)
{
if ((iter & 1) != 0)
{
return false;
}
}
return true;
};
EXPECT_FALSE(allEvenFunc());
}
TEST_F(StlAlogrithm, transform)
{
std::transform(vec1.begin(), vec1.end(), vec2.begin(), [](int n) { return std::pow(n, 2); });
auto allPowFun = [=]() {
for (int i = 0; i < vec1.size(); ++i)
{
if (vec2[i] = std::pow(vec1[i], 2))
{
return false;
}
}
return true;
};
EXPECT_TRUE(allPowFun());
std::transform(vec1.begin(), vec1.end(), vec3.begin(), [](int n) { return -n; });
auto InverseFun = [=]() {
for (int i = 0; i < vec1.size(); ++i)
{
if (vec3[i] != -vec1[i])
{
return false;
}
}
return true;
};
EXPECT_TRUE(InverseFun());
}
}
TEST_P
参数化测试:可使用宏 INSTANTIATE_TEST_CASE_P 或新版接口生成测试。 测试相同代码逻辑但不同输入数据(如功能性、边界性测试)。
步骤:
1.创建一个类,继承testing::TestWithParam<T>,T是你需要参数化的参数类型,比如参数类型为int
2. 使用新宏TEST_P替代TEST。在TEST_P宏里,可以使用GetParam()获取当前的参数的具体值。
3. 使用INSTANTIATE_TEST_CASE_P宏来输入测试参数。
参数解释和表格直接贴上某个大佬的解释:
第一个参数是测试案例的前缀,可以任意取。
第二个参数是测试案例的名称,需要和之前定义的参数化的类的名称相同。
第三个参数是可以理解为参数生成器,上面的例子使用test::Values表示使用括号内的参数。Google提供了一系列的参数生成的函数:
| Range(begin, end[, step]) | 范围在begin~end之间,步长为step,不包括end |
| Values(v1, v2, ..., vN) | v1,v2到vN的值 |
| ValuesIn(container) and ValuesIn(begin, end) | 从一个C类型的数组或是STL容器,或是迭代器中取值 |
| Bool() | 取false 和 true 两个值 |
| Combine(g1, g2, ..., gN) | 这个比较强悍,它将g1,g2,...gN进行排列组合,g1,g2,...gN本身是一个参数生成器,每次分别从g1,g2,..gN中各取出一个值,组合成一个元组(Tuple)作为一个参数。 说明:这个功能只在提供了<tr1/tuple>头的系统中有效。gtest会自动去判断是否支持tr/tuple,如果你的系统确实支持,而gtest判断错误的话,你可以重新定义宏GTEST_HAS_TR1_TUPLE=1 |
输入int基本类型参数
class PowTestSuit :public ::testing::TestWithParam<int>
{
};
TEST_P(PowTestSuit, pow_test)
{
int num = GetParam();//获取参数生成器中的参数
std::cout << num << " " << std::pow(num,2);
}
//第一个参数测试案例的前缀,可以任意取
//第二个参数测试案例的名称,需要和之前定义的参数化的类的名称相同
//第三个参数参数生成器
INSTANTIATE_TEST_SUITE_P(PowTest, PowTestSuit, testing::Values(1, 2, 3, 4));
输入STL容器参数
class TestSortAndUniqueSuit :public ::testing::TestWithParam<vector<int>>
{
protected:
virtual void SetUp() override
{
numVec = std::move(GetParam());
std::cout << "排序去去重前:";
for (auto iter : numVec)
{
std::cout << iter << " ";
}
std::sort(numVec.begin(), numVec.end());
}
virtual void TearDown() override
{
std::cout << "排序去重后:";
for (auto iter : numVec)
{
std::cout << iter << " ";
}
}
vector<int>numVec;//只要非private TEST_P中可访问
};
TEST_P(TestSortAndUniqueSuit, sort_unique_test)
{
auto iter = std::unique(numVec.begin(), numVec.end());
numVec.erase(iter, numVec.end());
}
vector<int> numVec1{ 1,4,2,2,3,3,6,7,8,8,1,0 };
vector<int> numVec2{ 1,4,2,3,2,8,1,7,8,3,6,0 };
vector<int> numVec3{ 1,6,7,8,2,1,3,4,2,3,8,0 };
vector<int> numVec4{ 2,4,0,2,3,7,8,3,6,8,1,1 };
INSTANTIATE_TEST_SUITE_P(SortUniqueTest, TestSortAndUniqueSuit, testing::Values(numVec1, numVec2, numVec3, numVec4));
输入自定义类型参数
struct BodyParam
{
int age;
float height;
float weight;
std::string name;
};
class TestCustomzeData:public ::testing::TestWithParam<BodyParam>
{
protected:
virtual void SetUp() override
{
BodyParam BodyParam = GetParam();
std::cout<<"name:"<< BodyParam.name << "age:" << BodyParam.age << "height:" << BodyParam.height << "weight:" << BodyParam.weight;
}
virtual void TearDown() override
{
std::cout << "适合踢球:" << bBeautyBody;
}
static void SetUpTestSuite()
{
std::cout << std::boolalpha;
}
static void TearDownTestSuite()
{
std::cout << std::noboolalpha;
}
bool bBeautyBody;
};
TEST_P(TestCustomzeData, customzeData_Test)
{
BodyParam BodyParam = GetParam();
if (BodyParam.age > 20 && BodyParam.age < 30 &&
BodyParam.height > 175 && BodyParam.height < 183 &&
BodyParam.weight >60 && BodyParam.weight < 70)
{
bBeautyBody = true;
}
else
{
bBeautyBody = false;
}
}
vector<BodyParam>BodyVec{ { 24,176,70,"zhangsan" },
{ 28,180,80,"lisi" },
{ 27,178,65,"wangwu" },
{ 28,180,72,"luhou" }};
INSTANTIATE_TEST_SUITE_P(CustomzeDataTest, TestCustomzeData, testing::ValuesIn(BodyVec));
RUN_ALL_TESTS
gtest中会实现一个全局测试注册器,负责跟踪和管理所有测试用例, gtest会自动将TEST、TEST_F等宏将定义的测试用例册到全局测试注册器中,当调用RUN_ALL_TESTS()时gtest会遍历执行注册器中的所有用例,并输出结果。
int main(int argc,char*argv[])
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
项目中使用:
对于过车计费模块,各种基础的收费规则,根据不同的收费规则建立不通的测试用例,设计好不通的边界测试案例即可,不仅可以提高收费规则在软件升级中测试的便利性,同时可以测试升级后计费规则计费的耗时特性,保证计费模块对于各个规则前后计费一致,同时又可以保证计费效率,避免拖慢出场过车效率。

1万+

被折叠的 条评论
为什么被折叠?



