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

#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

#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接口

#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 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文件

#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

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)

运行结果

[==========] 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规定好预期行为。这部分也是我们需要斟酌的地方。预期行为是设置的严格一点还是松一点全看需求。

一般语法

在gMock中使用EXPECT_CALL()这个断言宏去设置一个Mock Object的预期行为

EXPECT_CALL(mock_object, mock_method(params))...

其中有两个核心参数,第一个是mock_object,第二个是mock_object中的方法,如果有参数同时要把参数传进去,注意,不同参数的mock_method可以认为是不同的预期行为

...部分可以填写很多链式调用的逻辑来指定该对象该方法的调用运行情况

using ::testing::Return;
...
EXPECT_CALL(mock_object, mock_method(params)).Times(5).WillOnce(Return(100)).WillOnce(Return(150)).WillRepeatedly(Return(200));

在以上的栗子中,为该对象的该方法指定了四个预期行为:

首先它会被调用5次,第一次返回100,第二次返回150,之后的每次都返回200

关于方法的参数params

不确定参数值

很多时候我们不想让参数值变得固定,这个时候可以使用::testing::_来表示任意参数值

using ::testing::_;
...
EXPECT_CALL(mock_object, mock_method(_))...

如果参数有多个,而且全部都是不确定参数值,我们可以这样写:

EXPECT_CALL(mock_object, mock_method)...

参数值需要满足某种条件

对于传入确切参数的情况,相当于是使用Eq(100),以下的前两个写法是等价的

EXPECT_CALL(mock_object, mock_method(100))...
EXPECT_CALL(mock_object, mock_method(Eq(100)))...
EXPECT_CALL(mock_object, mock_method(Ge(50)))...//参数大于等于50的所有情况

那么除了Eq之外,gMock还提供了其他的一些,可以自行探索

预期调用的次数

在预期行为部分,我们可以手动写上Times(3)来指定它需要被调用3次,多或少都会导致测试不通过。

AtLeast()是在次数预期里比较常用的一个方法,如果是Times(3),那方法必须调用且只能调用3次,但是如果是Times(AtLeast(3)),那么就是至少调用3次的意思了。

我们也可以省略Times(),此时gMock会默认根据我们写的链式调用情况添加Times(),具体规则见下面的部分。

关于次数的预期,核心的方法有两个,分别是WillOnce()和WillRepeatedly(),前者表示调用一次,后者表示重复调用,它们可以组合使用,使用的具体规则如下:

  • 如果没有WillOnce和WillRepeatedly(),则默认添加Times(1)
  • 如果有n个WillOnce,没有WillRepeatedly(),则默认添加Times(n)
  • 如果有n个WillOnce,有一个WillRepeatedly(),则默认添加Times(AtLeast(n)),这意味着WillRepeatedly可以匹配调用0次的情况

预期发生的行为

一个mock object的所有方法中都没有具体的实现体,那么它的返回值情况是怎么样设定预期的呢?

默认情况下我们如果不设定返回值预期,也会有默认的返回值(只是我们不使用而已),bool会返回false,int等等的会返回0.

如果需要它有指定的预期返回值,我们可以在次数预期中加入返回值预期

using ::testing::Return;
...
EXPECT_CALL(mock_object, mock_method(params))
.Times(5)
.WillOnce(Return(100))
.WillOnce(Return(150))
.WillRepeatedly(Return(200));

在以上的栗子中,为该对象的该方法指定了四个预期行为:

首先它会被调用5次,第一次返回100,第二次返回150,之后的每次都返回200

如果去掉Times(5),那就是第一次返回100,第二次返回150,之后每次都返回200,调用次数不少于2次(WillRepeatedly可以调用0次)

预期发生顺序

默认情况下,我们设定好一个mock对象的多个预期行为时,是不关心它们的发生顺序的。例如以下代码中,先调用PenDown()或者先调用了Forward(100)都是无所谓的,都能通过测试:

EXPECT_CALL(turtle, PenDown());
EXPECT_CALL(turtle, Forward(100));

那么如果我们想指定预期发生顺序,我们需要创建InSequence对象,该对象创建处的代码块(scope)内的所有预期行为都必须按照声明顺序发生。

using ::testing::InSequence;
...
TEST(FooTest, DrawsLineSegment) {
...
{
InSequence seq; EXPECT_CALL(turtle, PenDown());
EXPECT_CALL(turtle, Forward(100));
EXPECT_CALL(turtle, PenUp());
}
Foo();
}

一些需要注意的点

预期行为的一次性写入

EXPECT_CALL()的链式调用中所有预期都会一次性写入,这意味着不要在链式调用中写运算,可能不会满足预期需求。举个栗子,以下并不能匹配返回100,101,102...而是只匹配返回100的情况,因为++是在预期行为被设定好之后才发生

using ::testing::Return;
...
int n = 100;
EXPECT_CALL(turtle, GetX())
.Times(4)
.WillRepeatedly(Return(n++));

mock对象方法的预期行为多重定义

在前面,我们看到的都是单对象单方法仅有1种预期行为定义的情况,如果定义了多个呢?例如:

using ::testing::_;
...
EXPECT_CALL(turtle, Forward(_)); // #1
EXPECT_CALL(turtle, Forward(10)) // #2
.Times(2);

假如我们在后面调用了三次Forwar(10),那么测试会报错不通过。如果调用了两次Forward(10),一次Forward(20),那么测试会通过。

预期行为粘连问题

gMock中的预期行为默认是粘连的,它们会一直保持存活状态(哪怕它所规定的预期行为已经完全被匹配过了)

例如以下的情况可能会出错,这种写法下可能最初想的是返回50、40、30、20、10的调用各一次,但是发生调用时就报错了(例如第一次调用返回10,而第二次调用返回20时,预期返回10的那个也还存活着会报错(不满足Once了))

using ::testing::Return;
...
for (int i = 5; i > 0; i--) {
EXPECT_CALL(turtle, GetX())
.WillOnce(Return(10*i));
}

我的理解:所谓预期行为(Expectations),它所针对的是一个Mock对象的一个方法在某一种参数情况下的行为,如果不显式的声明让它在被满足后退休,它会一直存活,一直干活...

要想解决上面的问题,可以显式的声明饱和退休

using ::testing::Return;
...
for (int i = n; i > 0; i--) {
EXPECT_CALL(turtle, GetX())
.WillOnce(Return(10*i))
.RetiresOnSaturation();
}

在以上这种写法下,每个.WillOnce()一旦被满足就会退休,后面发生了什么它不会去管了,也就不会报错了

当然这也可以结合前面的预期发生顺序来写,以下的写法意味着第一次调用返回10,第二次返回20.....

using ::testing::InSequence;
using ::testing::Return;
...
{
InSequence s; for (int i = 1; i <= n; i++) {
EXPECT_CALL(turtle, GetX())
.WillOnce(Return(10*i))
.RetiresOnSaturation();
}
}

后续可填坑

gMock进阶指南

【C++】GoogleTest进阶之gMock的更多相关文章

  1. 腾讯开源项目phxpaxos的编译步骤

    #paxos的一般编译流程在项目文档<中文详细编译手册>里面已经有介绍,这里重点介绍一下编译samples目录下的代码: #我的环境是ubuntu; #设置paxos根目录 phx_dir ...

  2. Google C++单元测试框架GoogleTest(总)

    之前一个月都在学习googletest框架,对googletest的文档都翻译了一遍,也都发在了之前的博客里,另外其实还有一部分的文档我没有发,就是GMock的CookBook部分:https://g ...

  3. 单元测试---googletest

    单元测试概述 测试并不只是测试工程师的责任,对于开发工程师,为了保证发布给测试环节的代码具有足够好的质量( Quality ),为所编写的功能代码编写适量的单元测试是十分必要的. 单元测试( Unit ...

  4. 如何用googletest写单元测试

    http://www.uml.org.cn/c++/201203293.asp googletest是一个用来写C++单元测试的框架,它是跨平台的,可应用在windows.linux.Mac等OS平台 ...

  5. 直接在CMake项目中编译GoogleTest和GoogleMock作为项目的一部分

    直接在CMake项目中编译GoogleTest和GoogleMock作为项目的一部分 本文是关于如何将GoogleTest和GoogleMock在没有预先编译安装在机器的情况下,直接在项目中作为项目的 ...

  6. GoogleTest入门

    Googletest入门 来源:https://github.com/google/googletest/blob/master/googletest/docs/primer.md P.S. gmoc ...

  7. C++雾中风景番外篇2:Gtest 与 Gmock,聊聊C++的单元测试

    正式工作之后,公司对于单元测试要求比较严格.(笔者之前比较懒,一般很少写完整的单测~~).作为一个合格的开发工程师,需要为所编写代码编写适量的单元测试是十分必要的,在实际进行的开发工作之中,TDD(T ...

  8. GoogleTest初探(1)

    此篇主要了解一下GoogleTest中的断言. 总的来说,GoogleTest中的断言分为两大类:EXPECT_*和ASSERT_*,这两者在测试成功或失败后均会给出测试报告,区别是前者在测试失败后会 ...

  9. gmock使用、原理及源码分析

    1      初识gmock 1.1      什么是Mock 便捷的模拟对象的方法. 1.2      Google Mock概述 google mock是用来配合google test对C++项目 ...

随机推荐

  1. 【系统设计】S3 对象存储

    在本文中,我们设计了一个类似于 Amazon Simple Storage Service (S3) 的对象存储服务.S3 是 Amazon Web Services (AWS) 提供的一项服务, 它 ...

  2. php YII2空数组插入报错问题处理 Array to string conversion

    问题描述 前端传空数组 [],php接收后处理不当插入数据库时报错Array to string conversion 参数示例 { "id": 0, //ID整型 "t ...

  3. Java精进-手写持久层框架

    前言 本文适合有一定java基础的同学,通过自定义持久层框架,可以更加清楚常用的mybatis等开源框架的原理. JDBC操作回顾及问题分析 学习java的同学一定避免不了接触过jdbc,让我们来回顾 ...

  4. Web 前端实战:Gitee 贡献图

    前言 这次要做的 Web 前端实战是一个 Gitee 个人主页下的贡献图(在线 Demo),偶尔做一两个,熟悉熟悉 JS 以及 jQ.整体来说这个案例并不难,主要是控制第一个节点以及最后一个节点处于星 ...

  5. virtio 驱动的数据结构理解

    ps:本文基于4.19.204内核 Q:vqueue的结构成员解释: A:结构如下,解析附后: struct virtqueue { struct list_head list;//caq:一个vir ...

  6. systemd之导致内核 crash

    本文主要讲解linux kernel panic系列其中一种情况: Attempted to kill init! exitcode=0x0000000b 背景:linux kernel 的panic ...

  7. Python小游戏——外星人入侵(保姆级教程)第一章 03设置飞船图片 04创建Ship类

    系列文章目录 第一章:武装飞船 03:设置飞船图片 04:创建Ship类--管理飞船行为的类 一.设置飞船图片 1.注意事项 A.将图片设置为位图bmp格式最简单,因为pygame默认加载位图 B.飞 ...

  8. Markdown使用指南

    1. Markdown是什么? Markdown是一种轻量级标记语言,它以纯文本形式(易读.易写.易更改)编写文档,并最终以HTML格式发布. Markdown也可以理解为将以MARKDOWN语法编写 ...

  9. 「题解报告」SP16185 Mining your own business

    题解 SP16185 Mining your own business 原题传送门 题意 给你一个无向图,求至少安装多少个太平井,才能使不管那个点封闭,其他点都可以与有太平井的点联通. 题解 其他题解 ...

  10. django 通过MQTT连接阿里云

    Django MQTT 连接阿里云 目录 Django MQTT 连接阿里云 目录 一.安装库 1.安装Python对接mqtt协议库,paho-mqtt 二. 设备认证,一机一密型接入 三.问题 1 ...