【C++】GoogleTest进阶之gMock
作者:互联网
gMock是什么
当我们去写测试时,有些测试对象很单纯简单,例如一个函数完全不依赖于其他的对象,那么就只需要验证其输入输出是否符合预期即可。
但是如果测试对象很复杂或者依赖于其他的对象呢?例如一个函数中需要访问数据库或者消息队列,那么要想按照之前的思路去测试就必须创建好数据库和消息队列的客户端实例,然后放在该函数内使用。很多时候这种操作是很麻烦的,此时Mock Object就能帮助我们解决这个问题。一个Mock Object实现与真实对象相同的接口,它可以替代真实对象去使用,而我们要做的就是制定好该Mock Object的行为(调用多少次、参数、返回值等等)
参考文档:
gMock官方文档
安装gMock#
gMock现在与gTest是组合使用的关系,因此在安装gTest时默认就会安装gMock,具体的安装方式见github上的官方说明
https://github.com/google/googletest/tree/main/googletest
使用gMock的基本思路#
- 首先,使用一些简单的gMock宏来描述想要模拟的接口,它们会实现你的mock类
- 然后,创建一些mock object然后使用gMock提供的语法指定好它们的行为
- 最后,运行需要使用这些mock object的代码,gMock会在mock object的行为不符合预期的时候发现并指出
gMock快速入门
假设我们在做一个用户的账户系统,一个用户会有一个账户,用户提供接口salary,账户提供接口add和getAccount,在用户的salary内会调用账户的add和getAccount接口
特别注意:此处的账户就是我们要mock的对象,它是用户的一个依赖。要想模拟它,它内部必须有虚析构函数,各个接口也建议是虚函数乃至纯虚函数。这里我的理解是,实际上mock object是对真实对象的代理/替换,在代理模式中比较常见的一种做法就是代理类和被代理类继承自同一个父类/接口
基本样例#
User#
Copy
#ifndef USER_H #define USER_H #include <iostream> #include "account.h" class User{ public: /// @brief User类的对象依赖于Account的对象 /// @param account Account实例,被User所依赖 User(Account *account){ account_ = account; } /// @brief 模拟发工资的场景 /// @param money 发的钱数 /// @return 账户余额 int salary(int money){ account_->add(money); return account_->getAccount(); } private: Account *account_; }; #endif //USER_H
Account#
Copy
#ifndef ACCOUNT_H #define ACCOUNT_H class Account { public: virtual ~Account() {} virtual void add(int money) = 0; virtual int getAccount() = 0; }; #endif //ACCOUNT_H
mock类编写#
我们要mock的是Account的一个对象,所以书写mock类实现Account接口
Copy
#ifndef MOCK_ACCOUNT_H #define MOCK_ACCOUNT_H #include "account.h" #include <gmock/gmock.h> class MockAccount : public Account { public: MOCK_METHOD(void, add, (int money), (override)); MOCK_METHOD(int, getAccount, (), (override)); }; #endif // MOCK_ACCOUNT_H
其中的关键部分在于MOCK_METHOD,很多老的教程中会使用MOCK_METHOD0、MOCK_METHOD1...这些宏,它们分别代表0参数、1参数、2参数的接口。在新的官方教程中没有这种写法,统一都是MOCK_METHOD,内部有四个参数
- 接口返回值类型
- 接口名
- 接口形参列表,注意,如果有泛型,需要多加一层括号,例如
MOCK_METHOD(void, funName, (int, (map<int,string>)),(override))
- 为生成的mock object的方法添加关键字(如果是override这个参数其实可以不写,但是如果接口是const的,就必须写const关键字了)
mock类放在哪#
按照google的建议,除非整个接口就是你自己持有的,否则mock类不要放在xx_test下,因为一旦Account接口被它的所有者改变,MockAccount也必须改变才能继续使用
一般来说,我们不应该mock不是自己持有的接口。如果真的需要mock不是自己持有的,mock对象的目录或者testing的子目录下创建一个.h文件和一个 cc_library with testonly=true,这样一来,每个人都可以使用同一个地方定义的mock类
mock的使用#
创建好mock类之后,要使用它一般分以下几步
- 创建Mock Object
- 规定Mock Object的预期行为
- 使用Mock Object测试业务代码,业务代码部分可以使用gTest的各种断言
- 一旦Mock Object的方法被调用的情况与前面规定的预期行为不符,测试就会不通过(在Mock Object被析构时也会再次检查)
其中比较核心代码有两部分:规定Mock Object的预期行为和业务代码测试,前者将会在下面详细展开,后者可以参考Google Test那篇文章
google test入门指南
样例#
user_test.cc文件
Copy
#include <gtest/gtest.h> #include <gmock/gmock.h> #include "user.h" #include "mock_account.h" using ::testing::AtLeast; using ::testing::Return; TEST(UserTest, SalaryIsOK) { MockAccount mAccount;//创建Mock Object EXPECT_CALL(mAccount, add(100)).Times(AtLeast(1)); EXPECT_CALL(mAccount, getAccount()).Times(AtLeast(1));//规范Mock Object的行为,此处是说该mock对象的getAccount()方法至少被调用1次 User user(&mAccount);//将Mock Obejct注入到user中使用(依赖注入) int res = user.salary(100);//测试User业务逻辑 ASSERT_GE(res, 0);//gTest的断言,res大于等于0则通过 }
编译运行
这里我使用CMake来做构建,注意gTest和gMock需要C++14及以上,在链接时直接链接gtest_main,这样就不需要自己写main方法了
CMakeLists.txt#
Copy
cmake_minimum_required(VERSION 3.14) project(user LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 14) enable_testing() find_package(GTest REQUIRED) add_executable(test_user "${PROJECT_SOURCE_DIR}/user_test.cc") target_link_libraries(test_user GTest::gtest_main gmock) include(GoogleTest) gtest_discover_tests(test_user)
运行结果
Copy
[==========] Running 1 test from 1 test suite. [----------] Global test environment set-up. [----------] 1 test from UserTest [ RUN ] UserTest.SalaryIsOK [ OK ] UserTest.SalaryIsOK (0 ms) [----------] 1 test from UserTest (0 ms total) [----------] Global test environment tear-down [==========] 1 test from 1 test suite ran. (0 ms total) [ PASSED ] 1 test.
测试通过了
设置预期行为
使用Mock最核心的点就在于给一个Mock Object规定好预期行为。这部分也是我们需要斟酌的地方。预期行为是设置的严格一点还是松一点全看需求。