VS2010 C++单元测试之gtest与OpenCppCoverage实践

Posted -飞鹤-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了VS2010 C++单元测试之gtest与OpenCppCoverage实践相关的知识,希望对你有一定的参考价值。

1. 前言

想减少开发过程中产生Bug,尤其是迭代开发过程中新引入的Bug。进行单元测试是一个非常的方法,可以用来减少开发过程的Bug,尤其是通过CI自动化,每次有新的修改,都进行回归测试,可以大大增强代码的鲁棒性。

1.1. 单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。“单元”一般情况定义为函数(包括类中的方法),因为函数是最小的可测试代码。跨模块的测试以及集成测试,各有各的优势,需要相互结合,而不是只用一个就可以解决所有问题。
代码覆盖率,用来衡量代码测试中的测试程序,主要包括语句覆盖、判定覆盖、条件覆盖、条件判定组合覆盖、多条件覆盖和路径覆盖。很多自动检测代码覆盖率的工具,主要还是针对的语句覆盖(即行覆盖)。有时,虽然行覆盖到了,但是一行中有多个判定逻辑的语句,多个判定逻辑不一定覆盖到了。如何提高条件覆盖,判定覆盖,除了自我注意以外,还可以通过CodeReview来协助。
VS2010自带的基于.NET的单元测试框架及代码覆盖率检查都非常好。但是这种测试框架绑定了VS2010,使其只能在Windows下使用。
gtest是Google开发的开源C++单元测试框架,被广泛使用,且其兼具跨平台的特性。另外,gtest+OpenCppCoverage也能很好地完成代码覆盖率的统计和展示。

2. gtest

2.1. 简介

gtest是Google开源的一套跨平台的C++单元测试框架。它提供了丰富的断言及其他判断,另外还有参数化及死亡测试等功能。

2.2. 编译

VS2017及之后的版本,可以直接安装gtest组件,但是VS2010需要自行编译生成相关库。只有V1.8.1版本只支持VS2010,下载地址:https://github.com/google/googletest/releases/tag/release-1.8.1。
解压之后,直接进入msvc\\2010目录,其中有gtest.sln和gtest-md.sln,两个sln代码一样,前者为静态运行时库版本,后者为动态运行时库版本。
编译会生成gtest.lib,gtest_prod_test.exe。前者是gtest的测试框架静态库,后者是测试示例。

2.3. 用法

2.3.1. 初始化

在main函数中添加下面的代码会初始化测试框架,并自动执行所有注册的测试单元。

int main(int argc, char **argv) 
  printf("Running main() from %s\\n", __FILE__);
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();

初始化的时候,还可以接受命令行参数。
常用的命令行参数有:
● --gtest_filter,测试案例过滤。
● --gtest_output,测试报告输出。
● --gtest_break_on_failure,异常时中断。
更多命令行参数见附录4.1.。

2.3.2. 断言

单元测试,必然有一个检测的过程,也即测试结果与实际期待值是否一致。如果不一致,触发断言,显示断言代码在哪个文件,哪一行,以方便调试问题。

2.3.2.1. 基本判断

gtest的基本判断断言主要有两类:

  1. ASSERT_*系列,其测试失败会立即退出当前测试函数。
  2. EXPECT_*系列,其测试失败会继续顺序执行。
    ● 例如:
    EXPECT_TRUE(false);
    ● 测试输出:
    error: Value of: false
    Actual: false
    Expected: true
    ● 附加输出信息:
    ASSERT_TRUE(false)<<“For test!”;
    测试失败时,不仅会输出上述信息,另外还会附加自定义信息。
    断言有判断真假的,有比较两个值的,比较两个字符串,还有比较浮点类型的,详见附录4.2.。

2.3.2.2. 标记成功失败

在一些多逻辑判断分支中,可以直接标记成功与失败。
● SUCCEED(),标记当前的判断是成功的。
● ADD_FAILURE(),标记当前判断失败,输出相关代码位置信息,继续往下执行。
● FAIL(),标记当前判断重大失败,退出当前函数。

2.3.2.3. 判断抛出异常

判断抛出的异常类型是否符合预期。
例如: EXPECT_THROW(Fun(0), int);
会判断Fun(0)函数是否抛出了int类型的异常。
更多见附录4.2.5.。

2.3.2.4. 参数名输出断言

在调用函数的同时,输出其参数值,相当于自定义判断。

ASSERT_PRED2(GreaterThan, a, b);

测试结果:

error: GreaterThanevaluates to false, where
a evaluates to 5
b evaluates to 6

ASSERT_PRED1,ASSERT_PRED3,ASSERT_PRED4,ASSERT_PRED2分别对应不同参数个数的版本。

2.3.2.5. 子过程中使用断言

SCOPED_TRACE来标记接下来的子函数,如果触发断言,会显示子过程前面设置的追踪标记,以方便确认是调用的哪个子函数。

void Sub(int n) 

	ASSERT_EQ(1, n);


TEST(SubTest, Test1) 

	SCOPED_TRACE("Step1");
	Sub(1);
	SCOPED_TRACE("Step2");
	Sub(3);

2.3.3. 测试用例的注册

gtest提供了非常多的宏用来注册测试用例。

2.3.3.1. TEST 宏

TEST宏主要针对一般的函数或类的测试。

  1. 解析TEST宏
# define TEST(test_case_name, test_name) GTEST_TEST(test_case_name, test_name)
  1. 示例
// Tests factorial of 0.
TEST(FactorialTest, Zero) 
  EXPECT_EQ(1, Factorial(0));


// Tests factorial of negative numbers.
TEST(FactorialTest, Negative) 
  EXPECT_EQ(1, Factorial(-5));
  EXPECT_EQ(1, Factorial(-1));
  EXPECT_GT(Factorial(-10), 0);

可以看出FactorialTest是作为测试示例名,Zero是作为测试示例下的一个子具体测试项名。
如果一个测试示例下有多个测试项,可以如上面的代码中,写多个相同测试示例名的TEST。

2.3.3.2. TEST_F宏

#define TEST_F(test_fixture, test_name)\\
  GTEST_TEST_(test_fixture, test_name, test_fixture, \\
              ::testing::internal::GetTypeId<test_fixture>())

通过宏定义可以看出,第一个宏参数必须是一个特定的类型,而不是任意的类型。每个测试实例其实都是test_fixture的实例的接口实现。多个相同类名的测试套件就有多个实例,并且它们是注册在同一个类名之下的。针对类的时候,常有初始化以及清理的操作,测试套件下有三种情况,分别为:每个测试实例单独初始化及清理,同名测试类共享初始化及清理,不同测试套件共享初始化及清理。

2.3.3.2.1. 测试实例单独初始化及清理

gtest要求test_fixture必须是从::testing::Test派生而来的。test_fixture类可以重写SetUp和TealDown两个函数,前者是为初始化测试实例,后者为测试结束前进行相关清理。

  1. 示例
class TestFixtures : public ::testing::Test 
public:
    TestFixtures() 
        printf("\\nTestFixtures\\n");
    ;
    ~TestFixtures() 
        printf("\\n~TestFixtures\\n");
    
protected:
    void SetUp() 
        printf("\\nSetUp\\n");
        data = 0;
    ;
    void TearDown() 
        printf("\\nTearDown\\n");
    
protected:
    int data;
;
 
TEST_F(TestFixtures, First) 
    EXPECT_EQ(data, 0);
    data =  1;
    EXPECT_EQ(data, 1);

 
TEST_F(TestFixtures, Second) 
    EXPECT_EQ(data, 0);
    data =  1;
    EXPECT_EQ(data, 1);

  1. 测试结果
    下面的测试结果可以看出,每个测试实例都是单独初始化及清理的。
[----------] 2 tests from TestFixtures
[ RUN      ] TestFixtures.First
TestFixtures
SetUp
TearDown
~TestFixtures
[       OK ] TestFixtures.First (9877 ms)
[ RUN      ] TestFixtures.Second
TestFixtures
SetUp
TearDown
~TestFixtures
[       OK ] TestFixtures.Second (21848 ms)
[----------] 2 tests from TestFixtures (37632 ms total)

2.3.3.2.2. 测试类初始化及清理

gtest要求test_fixture必须是从::testing::Test派生而来的,然后可以重写类静态函数完成类层面的初始化及清理。

  1. 示例
class TestFixturesS : public ::testing::Test 
    public:
    TestFixturesS() 
        printf("\\nTestFixturesS\\n");
    ;
    ~TestFixturesS() 
        printf("\\n~TestFixturesS\\n");
    
    protected:
    void SetUp() 
    ;
    void TearDown() 
    ;
    
    static void SetUpTestCase() 
        UnitTest& unit_test = *UnitTest::GetInstance();
        const TestCase& test_case = *unit_test.current_test_case();
        printf("Start Test Case %s \\n", test_case.name());
    ;
    
    static void TearDownTestCase() 
        UnitTest& unit_test = *UnitTest::GetInstance();
        const TestCase& test_case = *unit_test.current_test_case();
        int failed_tests = 0;
        int suc_tests = 0;
        for (int j = 0; j < test_case.total_test_count(); ++j) 
            const TestInfo& test_info = *test_case.GetTestInfo(j);
            if (test_info.result()->Failed()) 
                failed_tests++;
            
            else 
                suc_tests++;
            
        
        printf("End Test Case %s. Suc : %d, Failed: %d\\n", test_case.name(), suc_tests, failed_tests);
    ;
    
;

TEST_F(TestFixturesS, SUC) 
    EXPECT_EQ(1,1);


TEST_F(TestFixturesS, FAI) 
    EXPECT_EQ(1,2);

  1. 结果
    可以看到同名类的测试只有一个初始化及清理。
[----------] 2 tests from TestFixturesS
Start Test Case TestFixturesS
[ RUN      ] TestFixturesS.SUC
TestFixturesS
~TestFixturesS
[       OK ] TestFixturesS.SUC (2 ms)
[ RUN      ] TestFixturesS.FAI
TestFixturesS
..\\test\\gtest_unittest.cc(126): error:       Expected: 1
To be equal to: 2
~TestFixturesS
[  FAILED  ] TestFixturesS.FAI (5 ms)
End Test Case TestFixturesS. Suc : 1, Failed: 1
[----------] 2 tests from TestFixturesS (12 ms total)

2.3.3.2.3. 全局的初始化及清理

有时需要有一个全局统一的一个初始化及退出前的清理操作,此时可以从testing::Environment派生一个类,并重写Setup和TearDown。然后在main函数中调用AddGlobalTestEnvironment添加实例。

  1. 示例
namespace testing 
namespace internal 
class EnvironmentTest : public ::testing::Environment 
public:
    EnvironmentTest() 
        printf("\\nEnvironmentTest\\n");
    ;
    ~EnvironmentTest() 
        printf("\\n~EnvironmentTest\\n");
    
public:
    void SetUp() 
        printf("\\n~Start Test\\n");
    ;
    void TearDown() 
        UnitTest& unit_test = *UnitTest::GetInstance();
        for (int i = 0; i < unit_test.total_test_case_count(); ++i) 
            int failed_tests = 0;
            int suc_tests = 0;
            const TestCase& test_case = *unit_test.GetTestCase(i);
            for (int j = 0; j < test_case.total_test_count(); ++j) 
                const TestInfo& test_info = *test_case.GetTestInfo(j);
                // Counts failed tests that were not meant to fail (those without
                // 'Fails' in the name).
                if (test_info.result()->Failed()) 
                    failed_tests++;
                
                else 
                    suc_tests++;
                
            
            printf("End Test Case %s. Suc : %d, Failed: %d\\n", test_case.name(), suc_tests, failed_tests);
        
    ;
;


 
GTEST_API_ int main(int argc, char **argv) 
  printf("Running main() from gtest_main.cc\\n");
  ::testing::AddGlobalTestEnvironment(new testing::internal::EnvironmentTest);
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();

2.3.3.3. TEST_P宏

测试代码写好了之后,如果想添加新的验证数据,最好能够不修改原有的测试代码,有一种方法直接添加测试数据。gtest针对这种情况提供了TEST_P宏来将测试参数化,也即将测试参数独立出来。

2.3.3.3.1. 单一参数
  1. 代码

class IsPrimeParamTest : public testing::TestWithParam<int>

	public:
	bool IsPrime(int n) 
	
		if (n <= 1) return 
			false;
		if (n % 2 == 0)
			return n == 2;
		for (int i = 3; ; i += 2) 
		
			if (i > n / i)
				break;
			if (n % i == 0) 
				return false;
		
		return true;
	
	
;


TEST_P(IsPrimeParamTest, HandleTrueReturn)

	int n = GetParam();
	EXPECT_TRUE(IsPrime(n));


INSTANTIATE_TEST_CASE_P(TrueReturn, IsPrimeParamTest, testing::Values(3, 5, 11, 23, 17));
INSTANTIATE_TEST_CASE_P(TrueReturnEx, IsPrimeParamTest, testing::Values(2));
  1. 测试结果
Running main() from ..\\..\\src\\gtest_main.cc
[==========] Running 6 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 5 tests from TrueReturn/IsPrimeParamTest
[ RUN      ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/0
[       OK ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/0 (0 ms)
[ RUN      ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/1
[       OK ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/1 (0 ms)
[ RUN      ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/2
[       OK ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/2 (1 ms)
[ RUN      ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/3
[       OK ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/3 (0 ms)
[ RUN      ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/4
[       OK ] TrueReturn/IsPrimeParamTest.HandleTrueReturn/4 (0 ms)
[----------] 5 tests from TrueReturn/IsPrimeParamTest (1 ms total)

[----------] 1 test from TrueReturnEx/IsPrimeParamTest
[ RUN      ] TrueReturnEx/IsPrimeParamTest.HandleTrueReturn/0
[       OK ] TrueReturnEx/IsPrimeParamTest.HandleTrueReturn/0 (0 ms)
[----------] 1 test from TrueReturnEx/IsPrimeParamTest (0 ms total)

[----------] Global test environment tear-down
[==========] 6 tests from 2 test cases ran. (3 ms total)
[  PASSED  ] 6 tests.
2.3.3.3.2. 多参数
  1. 代码
class OddEvenTest : public testing::TestWithParam<::testing::tuple<bool, int> >

    protected:
    bool IsOdd(int n) 
    
        return (1 == n%2);
    
    
    bool IsEven(int n) 
    
        return (0 == n%2);
    
;

TEST_P(OddEvenTest, Case1)

    bool bOdd = ::testing::get<0>(GetParam());
    int nVal = ::testing::get<1>(GetParam());
    if (bOdd)
    
        EXPECT_TRUE(IsOdd(nVal));
    
    else
    
        EXPECT_TRUE(IsEven(nVal));
    



INSTANTIATE_TEST_CASE_P(TestBisValuesCombine, OddEvenTest, ::testing::Combine(::testing::Bool(), ::testing::Values(0, 1, 2, 3, 4)));

多参数时,会用到可变参数模板,VS2010不支持,VS2013才开始支持。

2.3.3.4. TYPED_TEST宏

针对模板,gtest提供了TYPED_TEST宏来测试其参数的兼容性。

  1. 代码
#include <list>
template <typename T>
class FooTest: public testing::Test 


public:
typedef std::list<T> List;

static T shared_;
T value_;
;

template <typename T> T FooTest<T>::shared_ ;

typedef testing::Types<char, int, unsigned int> MyTypes;
TYPED_TEST_CASE(FooTest, MyTypes);

TYPED_TEST(FooTest, DoesBlah) 
    // Inside a test, refer to the special name TypeParam to get the type
    // parameter.  Since we are inside a derived class template, C++ requires
    // us to visit the members of FooTest via 'this'.
    TypeParam n = this->value_;
    
    // To visit static members of the fixture, add the 'TestFixture::'
    // prefix.
    // 测试T类型的vlaue_和shared_的运算兼容性
    n += TestFixture::shared_;
    
    // To refer to typedefs in the fixture, add the 'typename TestFixture::'
    // prefix.  The 'typename' is required to satisfy the compiler.
    // 测试List的可用性
    typename TestFixture::List values;
    values.push_back(n);

  1. 结果
Running main() from ..\\..\\src\\gtest_main.cc
[==========] Running 3 tests from 3 test cases.
[----------] Global test environment set-up.
[----------] 1 

以上是关于VS2010 C++单元测试之gtest与OpenCppCoverage实践的主要内容,如果未能解决你的问题,请参考以下文章

Google开源C++单元测试框架Google Test

ROS系统C++代码测试之gtest

gTest&gMock learning

MFC程序使用GTest搭建测试框架

gtest变化量

如何将测试夹具传递给 C++ GTest 中的辅助函数