欢迎您的访问
专注于Java技术系列文章的Java技术分享网站

C++ 单元测试之 gtest & gmock

一、综述

这篇文章介绍C++单元测试工具gtestgmock,以及自己在前段时间做单元测试时的一些方式方法。

二、单元测试浅谈

1、为什么要做单元测试

单元测试不但可以增加开发者对于所完成代码的自信,同时,好的单元测试用例往往可以在回归测试的过程中,很好地保证之前所发生的修改没有破坏已有的程序逻辑。因此,单元测试不但不会成为开发者的负担,反而可以在保证开发质量的情况下,加速迭代开发的过程。

2、好的单元测试应具备的特点

  • 独立,单元测试用例的测试结果不应该受其他测试的影响;
  • 当测试用例失败时,提供尽可能多的有效信息,方便定位和Debug。比如下文会说到的gtest,在EXPECT_*之后打印出已知信息,例如:EXPECT_EQ(10, add(p1, p2)) << "p1:" << p1 << " p2:" << p2;

3、C++单元测试框架

C++的单元测试框架,我见过最常多的就是gtest了。除此之外,boost也提供了一个用于单元测试的框架,boost仅在学校时使用过,印象中与gtest使用方式大同小异。

Mockcpp与gtest和boost不同,Mockcpp是C++的Mock框架,而后两者是单元测试框架(单元测试与Mock是相关但没有必然关联的两个东西)。Mockcpp的优点是可以MockC函数静态成员函数,下文有涉及到。

4、gtest & gmock基础

(1) gtest

单元测试框架通常会对一些变量或函数设置期望,若变量值或返回值符合预期,就认为单元测试用例通过。gtest也提供了下面一些断言:

ASSERT_* 系列的断言,当检查点失败时,立即退出单元测试; EXPECT_* 系列的断言,当检查点失败时,单元测试还是会继续执行,但结束后会标记所有ECPECT_*失败的用例; EXPECT_CALL 设置函数调用之后期望的实现,比如直接返回某一个值。该断言后面没有.Times()时,无论函数有没有调用都不会导致失败,如果有.Times()时,不满足.Times()设置的次数时就会导致期望失败;

(2) gmock

有时候对于一些接口,比如向服务器发送请求。但单元测试中有没有可用于测试的服务器,这个时候就需要mock这个请求接口。 mock工具的作用是指定函数的行为(模拟函数的行为)。可以对入参进行校验,对出参进行设定,还可以指定函数的返回值。

Mock的基本使用方法是:

1、 继承某一个类;
2、 实现重写类中的某个或某些虚方法
3、 创建Mock对象,设置重写方法的实现(大部分是直接返回,对于返回值是内置类型,即使不设置调用后的期望幸会,gmock也会设置默认返回值);
4、 调用被测接口,Mock对象调用重写方法,期望满足,测试通过。

三、单元测试中的一些方式方法

(1) Mock C函数和C++静态成员函数

因为Mock是基于多态实现的,gmock是不支持Mock全局函数或者静态成员函数的。对于这些全局函数, 比较传统的做法是创建一个Wrapper, 用虚方法对这些静态函数进行包裹. 在测试的时候对Wrapper进行Mock便可控制被包裹的静态函数的行为。

更详细的Mock方式可以参考这篇 文章。里面不仅介绍如何使用MockcppMock静态成员函数,也对Mockcpp ‘Mock静态成员函数的一些缺陷’使用gmock解决了。

(2) 测试具有依赖关系的case

单元测试的case不应该有直接的依赖关系,每一个case在SetUp之后应该达到可以直接测试的条件,在TearDown之后不应该残留任何状态。这里所说的[测试具有依赖关系的case]指的是:一个case测试的条件是另一个case执行正常路径之后的状态。说的有点绕,举个例子:

cese1:测试登录接口,没有前提,直接访问登录接口即可。 case2:测试添加商品到购物车,前提是已经登录成功; case3: 测试结账接口,前提是已经添加了若干商品到购物车;

对于这三个case,测试的时候代码该怎么写呢?难道测case2的时候要把case1的正常路径写到case2的开头?测case3的时候要把case1和case2的正常路径写到case3的开头?这代码得有多臃肿?如果还有case4、case5呢?

对于这种情况,应该充分利用SetUp和TearDown。我会这样写:

登录接口测试头文件:AuthenticateTest.h

class AuthenticateTest : public testing::Test    // 继承gtest的测试类
{
public:
    virtual void SetUp()
    {
        // 什么都不写,假如登录不需要前提条件
    }

    virtual void TearDown()
    {
    }

protected:
    void authenticate_success()
    {
        // 正常case,可以使用EXPECT_*等条件
    }

    // 如果接下来的case需要一些公共变量
};

登录接口测试源文件:AuthenticateTest.cpp

TEST_F(AuthenticateTest authen_success)
{
    authenticate_success();
}

TEST_F(AuthenticateTest, authen_failed)
{
    // 错误case。这个就没必要封装函数了,因为其他地方也不会用到这个case
}

添加商品到购物车测试头文件:AddTest.h

class AddTest : public AuthenticateTest    // 注意:这里要继承AuthenticateTest,而不是Test,因为AuthenticateTest类中有我们测试添加商品的条件:成功登陆
{
public:
    virtual void SetUp()
    {
        AuthenticateTest::SetUp();

        authenticate_success();  // 成功登陆

        // 其他前提条件
    }

    virtual void TearDown()
    {
        // 清理环境

        AuthenticateTest::TearDown();  // 父类的清理函数 
    }

protected:
    void add_success()
    {
        // 正常case,可以使用EXPECT_*等条件
    }

    // 如果接下来的case需要一些公共变量
};

添加商品到购物车测试源文件:AddTest.cpp

TEST_F(AddTest add_success)
{
    add_success();
}

TEST_F(AddTest, add_failed)
{
    // 错误case。这个就没必要封装函数了,因为其他地方也不会用到这个case
}

结账测试头文件:PayTest.h

class PayTest : public AddTest    // 注意:这里要继承AddTest,而不是Test,因为AddTest类中有我们测试添加商品的条件:成功添加商品到购物车
{
public:
    virtual void SetUp()
    {
        AddTest::SetUp();

        add_success();  // 成功添加商品

        // 其他前提条件
    }

    virtual void TearDown()
    {
        // 清理环境

        AddTest::TearDown();  // 父类的清理函数 
    }
};

结账测试源文件:PayTest.cpp

TEST_F(PayTest pay_success)
{
}

TEST_F(PayTest, pay_failed)
{
}

(3) 依赖注入

假如Mock类已经写好,那如何把实例化出来的Mock对象传入被测方法呢?(有时候被Mock的类或对象可能在被测接口内部使用)举个例子: 需要Mock的类:

class MT
{
public:
    virtual void connectDB()
    {
        // 一些还未实现的,或不好用于测试的操作。比如访问数据库
    }
};

被Mock的对象处于被测类内部

class Wrapper
{
public:
    virtual void init()    // 如果init方法不是virtual,为了单元测试也要加上virtual。为什么要virtual,下文说
    {
        _mt = new MT;      // 难测点:待Mock类处于函数内,无法通过参数传入

        // do something
    }

    virtual void toBeTestFunc()  // 被测对象
    {
        _mt->func();

        // other operator
    }

private:
    MT* _mt;
};

Mock:

class MockMT : public MT
{
public:
    MOCK_METHOD0(connectDB, void());
}

很关键的依赖注入部分,对待测类也进行Mock:

class MockWrapper : public Wrapper    // 依然需要Mock
{
public:
    MOCK_METHOD0(init, void());       // 这里就是为什么Wrapper::init是virtual的原因,需要Mock  

    void realToBeTestFunc()           // 定义一个函数,但是调用父类的接口,原因在单元测试case中说
    {
        Wrapper::toBeTestFunc();
    }
};

单元测试case:

TEST_F(XXX, success)
{
    MT* mockMT = new MockMT;                   // 创建Mock对象
    Wrapper* mockWrapper = new MockWrapper;    // 创建待测对象

    // 依赖注入
    EXPECT_CALL(mockWrapper, init()).Times(1).WillOnce(Invoke([this](){
        _mt = mockMT;    // 仅仅在这里修改传入Mock对象,其他任何操作都要保持原样

        // do something
    }));

    // 设置Mock对象连接数据库的预期操作:直接返回,不做任何操作
    EXPECT_CALL(mockMT, connectDB()).Times(1).WillOnce(Return());

    // Wrapper::toBeTestFunc是我们要测的方法。但现在我们没有使用Wrapper,而是用了MockWrapper,那gmock就会对MockWrapper::toBeTestFunc进行默认操作,比如直接返回,这样就测不了Wrapper::toBeTestFunc了。
    // 可以通过设置MockWrapper::toBeTestFunc的默认操作来调用Wrapper::toBeTestFunc来解决。这也是MockWrapper::realToBeTestFunc存在的意义。
    EXPECT_CALL(mockWrapper, toBeTestFunc()).Times(1).WillRepeatedly(Invoke(mockWrapper, &MockWrapper::realToBeTestFunc));

    // 执行调用逻辑
    mockWrapper->init();
    mockWrapper->toBeTestFunc();
    // some EXPECT_EQ

    // 对于new出来的堆对象,你可能需要下面两行。否则EXPECT_CALL可能会失败。感兴趣的话,原因可以自行查阅。
    Mock::VerifyAndClearExpectations(mockMT);
    Mock::VerifyAndClearExpectations(mockWrapper);
}

有时候Mock一个类,并不是实际意义的Mock,而是为了实现依赖注入,比如Wrapper类。这样的类实例化之后更多的是要执行正常逻辑(此时应该忽略掉它是Mock的)。对于这样的类,需要做一些特殊处理:

  • 作为依赖注入时,在调用mock对象之前,使用Invoke修改函数,传入已经创建好的Mock对象,函数其余逻辑保持原样
  • 作为正常测试类时,在调用mock对象之前,使用Invoke修改函数调用父类的实现,比如MockWrapper::realToBeTestFunc

一些待读的书:

  • 【修改代码的艺术】如何把代码重构到”可测试”的状态

文章永久链接:https://tech.souyunku.com/?p=43609

赞(65) 打赏



版权归原创作者所有,任何形式转载请联系作者;搜云库 » C++ 单元测试之 gtest & gmock

本站:免责声明!

评论 抢沙发

一个专注于Java技术系列文章的技术分享网站

觉得文章有用就打赏一下文章作者

微信扫一扫打赏

微信扫一扫打赏