Gmock使用说明

Gmock是C++中的一个接口测试框架,一般来说和Google Test搭配使用,但Google Test也可以和其他Mock框架一起使用。 本部分是Google Mock基础常用的用法,如需要特殊用法,请查阅Google Mock官方文档。

一、安装部署

依次执行下面命令即可:

git clone https://github.com/google/googletest
git checkout release-1.8.0
cd ~/googletest && cmake .
make && sudo make install

官方文档:https://google.github.io/googletest/

二、用法说明

  1. Fake、Mock、Stub

    • Fake对象有具体的实现,但采取一些捷径,比如用内存替代真实的数据库读取
    • Stub对象没有具体的实现,只是返回提前准备好的数据
    • Mock对象和Stub类似,只是在测试中需要调用时,针对某种输入指定期望的行为,Mock和Stub的区别是,Mock除了返回数据还可以指定期望以验证行为。
  2. 简单示例

    Tutle类:

    class Turtle {
    	...
    	virtual ~Turtle() {};
    	virtual void PenUp() = 0;
    	virtual void PenDown() = 0;
    	virtual void Forward(int distance) = 0;
    	virtual void Turn(int degrees) = 0;
    	virtual void GoTo(int x, int y) = 0;
    	virtual int GetX() const = 0;
    	virtual int GetY() const = 0;
    };
    

    MockTurtle类:

    #include "gmock/gmock.h"
    
    class MockTurtle : public Turtle {
    public:
    	...
    	MOCK_METHOD(void, PenUp, (), (override));
    	MOCK_METHOD(void, PenDown, (), (override));
    	MOCK_METHOD(void, Forward, (int distance), (override));
    	MOCK_METHOD(void, Turn, (int degrees), (override));
    	MOCK_METHOD(void, GoTo, (int x, int y), (override));
    	MOCK_METHOD(int, GetX, (), (const, override));
    	MOCK_METHOD(int, GetY, (), (const, override));
    };
    

    创建Mock类的步骤:

    1. MockTutle继承Tutle

    2. 找到Tutle的一个虚函数

    3. 在public的部分,写一个MOCK_METHOD()

    4. 将虚函数的函数签名复制进MOCK_METHOD()中,加两个逗号:

      一个在返回类型和函数名之间,另一个在函数名和参数列表之间

      例如:void PenDown() 有三部分:void、PenDown、和(),这三部分就是MOCK_METHOD的前三个参数

    5. 如果要模拟const方法,添加一个包含const的第四个参数,必须到括号

    6. 建议添加override关键字。所以对于const方法,第四个参数变为(const, override),对于非const方法,第四个参数变为override。这不是强制性的。

    7. 重复步骤直至完成要模拟的所有虚拟函数

  3. 在测试中使用Mock

    在测试中使用Mock的步骤:

    1. 从testing名称空间导入gmock.h的函数名(每个文件只需要执行一次)
    2. 创建一些Mock对象
    3. 指定对它们的期望(方法将被调用多少次? 带有什么参数? 每次应该做什么? 返回什么值 等等)
    4. 使用Mock对象;可以使用googletest断言检查结果。如果mock函数的调用超出预期或参数错误,将会立即收到错误信息。
    5. 当Mock对象被销毁时,gmock自动检查对模拟的所有期望是否得到满足
    #include "path/to/mock-turtle.h"
    #include "gmock/gmock.h"
    #include "gtest/gtest.h"
    
    using ::testing::AtLeast;                         	// #1
    
    TEST(PainterTest, CanDrawSomething) {
    	MockTurtle turtle;                              // #2
    	EXPECT_CALL(turtle, PenDown())                  // #3
    		.Times(AtLeast(1));
    
    	Painter painter(&turtle);                       // #4
    
    	EXPECT_TRUE(painter.DrawCircle(0, 0, 10));      // #5
    }
    

    在这个例子中,我们期望tutle的PenDown()至少被调用一次。如果在tutle对象被销毁时,PenDown()还没有被调用或者调用两次以上,测试会失败。

  4. 指定期望

    EXCEPT_CALL(指定期望)是使用Google Mock的核心。EXCEPT_CALL的作用是两方面的:

    1. 告诉这个Mock(假)方法如何模拟原始方法:

      我们在EXPECT_CALL中告诉Google Mock,某个对象的某个方法第一次被调用时,会修改某个参数,会返回某个值,第二次调用时, 会修改某个参数,会返回某个值......

    2. 验证被调用的情况

      我们在EXPECT_CALL中告诉Google Mock,某个对象的某个方法总共会被调用N次(或大于N次,小于N次)。如果

      最终次数不符合预期,会导致测试失败。

    4.1 基本语法

    EXPECT_CALL(mock_object, method(matchers))
    	.Times(cardinality)
    	.WillOnce(action)
    	.WillRepeatedly(action);
    
    • mock_object是对象
    • method(matchers)用于匹配相应的函数调用
    • cardinality指定基数(被调用次数情况)
    • action指定被调用时的行为

    例子:

    using ::testing::Return;
    ...
    EXPECT_CALL(turtle, GetX())
    	.Times(5)
    	.WillOnce(Return(100))
    	.WillOnce(Return(150))
    	.WillRepeatedly(Return(200));
    

    这个EXPECT_CALL()指定的期望是:在turtle这个Mock对象销毁之前,turtle的getX()函数会被调用五次。第一次返回100,第二次返回150,第三次及以后都返回200。指定期望后, 5次对getX的调用会有这些行为。但如果最终调用次数不为5次,则测试失败。

    4.2 参数匹配:哪次调用

    using ::testing::_;
    using ::testing::Ge;
    // 只与Forward(100)匹配
    EXPECT_CALL(turtle, Forward(100));
    // 与GoTo(x,y)匹配, 只要x>=50
    EXPECT_CALL(turtle, GoTo(Ge(50), _));
    
    • _ 相当于“任何”
    • 100相当于Eq(100)
    • Ge(50)指参数大于或等于50
    • 如果不关心参数,只写函数名就可以。比如:EXPECT_CALL(turtle, GoTo)

    4.3 基数:被调用几次

    用Times(m),TIme(AtLeast(n))等来指定期待的调用次数

    Times可以被省略。比如整个EXPECT_CALL只有一个WillOnce(action)相当于也说明了调用次数只能为1

    4.4 行为:该做什么

    常用模式:如果需要指定前几次调用的特殊情况,并且之后的调用情况相同。使用一系列WillOnce()之后有WillRepeatedly()

    ​ 除了用来指定调用返回值的Return(),Google Mock中常用行为中还有:SetArgPointee(value),SetArgPointee将第N个指针参数(从0开始)指向的变量赋值为value。

    ​ 比如void getObject(Object* response){...}的EXCEPT_CALL:

    Object* a = new Object;
    EXPECT_CALL(object, request)
    	.WillOnce(SetArgPointee<1>(*a));
    

    ​ 就修改了传入的指针response,使其指向了一个我们新创建的对象 。

    ​ 如果有多个行为,应该使用DoALL(a1, a2, ..., an)。DoAll指向所有n个action并返回an的结果。

    4.5 使用多个预期

    例子:

    using ::testing::_;
    ...
    EXPECT_CALL(turtle, Forward(_))		// #1
    	.Times(3);  	
    EXPECT_CALL(turtle, Forward(10))  	// #2
    	.Times(2);
    ...mock对象函数被调用...
    	//Forward(10);						// 与#2匹配
    	//Forward(20);						// 与#1匹配
    

    ​ 正常情况下,Google Mock以倒序搜索预期:如果和多个EXCEPT_CALL都可以匹配,只有之前的,距离调用最近的一个EXPECT_CALL()会被匹配。例如:

    • 连续三次调用Forward(10)会产生错误因为它和 #2 匹配
    • 连续三次调用Forward(20)不会有错误因为它和 #1 匹配

    ​ 一旦匹配,该预期会被一直绑定,即使执行次数达到上限之后,还是生效的,这就是为什么三次调用Forward(10)超过了2号的EXPECT_CALL的上限时,不会去试图调用绑定1号EXPECT_CALL而报错的原因。

    ​ 为了明确地让某一个EXPECT_CALL “退休”, 可以加上RetiresOnSaturation(),例如:

    using ::testing::Return;
    
    EXPECT_CALL(turtle, GetX())		// #1
    	.WillOnce(Return(10))
    	.RetiresOnSaturation();
    EXPECT_CALL(turtle, GetX())		// #2
    	.WillOnce(Return(20))
    	.RetiresOnSaturation();
    
    turtle.GetX()					// 与#2匹配,返回20,然后#2“退休”
    turtle.GetX()					// 与#1匹配,返回10
    

    ​ 在这个例子中,第一次GetX()调用和#2匹配,返回20,然后这个EXPECT_CALL就 “退休”了;第二次 GetX()调用和 #1匹配,返回10

    4.6 Sequence

    可以用sequence来指定期望匹配的顺序

    using ::testing::Return;
    using ::testing::Sequence;
    Sequence s1, s2;
    ...
    EXPECT_CALL(foo, Reset())
        .InSequence(s1, s2)
        .WillOnce(Return(true));
    EXPECT_CALL(foo, GetSize())
        .InSequence(s1)
        .WillOnce(Return(1));
    EXPECT_CALL(foo, Describe(A<const char*>()))
        .InSequence(s2)
        .WillOnce(Return("dummy"));
    

    image-20210812101918896

    ​ 在上面的例子中,创建了两个Sequence s1 和 s2,属于 s1 的有Reset() 和 GetSize(),所以Reset()必须在GetSize()之前执行。属于s2的有Reset()和Describe(A<const char*>()),所以Reset()必须在Describe(A<const char >())之前执行。所以,Reset()必须在Describe(A<const char>())之前执行,而GetSize()和Describe()这两者之间没有顺序约束。

    ​ 如果需要指定很多期望的顺序,有另一种写法:

    using ::testing::InSequence;
    {
      InSequence seq;
    
      EXPECT_CALL(...)...;
      EXPECT_CALL(...)...;
      ...
      EXPECT_CALL(...)...;
    }
    

    ​ 在这种用法中,scope(大括号中)的期望必须遵守严格的顺序。

    三、情景示例

    在这部分,我们找一个示例项目来演示,如何在不同的情景中使用Google Test和 Google Mock写单元测试用例。

    1. 项目结构

      示例项目是一个C++命令行聊天室软件,包含服务器和客户端。

      .
      ├── CMakeLists.txt
      ├── README.md
      ├── client_main.cpp
      ├── server_main.cpp
      ├── include
      │   ├── chat_client.hpp
      │   ├── chat_message.hpp
      │   ├── chat_participant.hpp
      │   ├── chat_room.hpp
      │   ├── chat_server.hpp
      │   ├── chat_session.hpp
      │   ├── http_request.hpp
      │   ├── http_request_impl.hpp
      │   ├── message_dao.hpp
      │   └── message_dao_impl.hpp
      ├── src
      │   ├── chat_client.cpp
      │   ├── chat_message.cpp
      │   ├── chat_room.cpp
      │   ├── chat_server.cpp
      │   ├── chat_session.cpp
      │   ├── http_request_impl.cpp
      │   └── message_dao_impl.cpp
      └── tests
          ├── chat_message_unittest.cpp
          └── chat_room_unittest.cpp
      
    2. 普通测试

      如果被测试的函数不包含外部依赖,用Google Test基础的用法就可以完成用例编写。

      原函数:

      void chat_message::body_length(std::size_t new_length) {
          body_length_ = new_length;
          if (body_length_ > 512)
              body_length_ = 512;
      }
      

      ​ 这个函数很简单,就是给body_length_赋值,但是有最大值限制。测试用例可以这样写:

      TEST(ChatMessageTest, BodyLengthNegative) {
          chat_message c;
          c.body_length(-50);
          EXPECT_EQ(512, c.body_length());
      }
      
      TEST(ChatMessageTest, BodyLength0) {
          chat_message c;
          c.body_length(0);
          EXPECT_EQ(0, c.body_length());
      }
      
      TEST(ChatMessageTest, BodyLength100) {
          chat_message c;
          c.body_length(100);
          EXPECT_EQ(100, c.body_length());
      }
      
      TEST(ChatMessageTest, BodyLength512) {
          chat_message c;
          c.body_length(512);
          EXPECT_EQ(512, c.body_length());
      }
      
      TEST(ChatMessageTest, BodyLength513) {
          chat_message c;
          c.body_length(513);
          EXPECT_EQ(512, c.body_length());
      }
      

      ​ 我们可以看到,对于这类函数,用例编写很直接简单,步骤都是构造变量,再用合适的Google Test宏来验证变量值或者函数调用的返回值。

    3. 简单Mock

      原函数

      void chat_room::leave(chat_participant_ptr participant) {
          participants_.erase(participant);
      }
      

      ​ participants_类型是 std::set<chat_participant_ptr>。这个函数的目的很明显,将一个participant从set中移除。

      ​ 真实地创建一个聊天参与者participant对象可能条件比较严苛或者成本比较高。为了有效率地验证这个函数,我们可以新建一些Mock的chat_participant_ptr而不用严格地去创建participant对象。

      ​ chat_participant 对象:

      class chat_participant {
      public:
          virtual ~chat_participant() {}
          virtual void deliver(const chat_message &msg) = 0;
      };
      

      ​ Mock对象:

      class mock_chat_participant : public chat_participant {
      public:
          MOCK_METHOD(void, deliver, (const chat_message &msg), (override));
      };
      

      ​ 测试用例:

      TEST(ChatRoomTest, leave) {
          auto p1 = std::make_shared<mock_chat_participant>();	//新建第一个Mock指针
          auto p2 = std::make_shared<mock_chat_participant>();	//新建第二个Mock指针
          auto p3 = std::make_shared<mock_chat_participant>();	//新建第三个Mock指针
          auto p4 = std::make_shared<mock_chat_participant>();	//新建第四个Mock指针
          chat_room cr;											//新建待测试对象chat_room
          cr.join(p1);
          cr.join(p2);
          cr.join(p3);
          cr.join(p4);
          EXPECT_EQ(cr.participants().size(), 4);
          cr.leave(p4);
          EXPECT_EQ(cr.participants().size(), 3);
          cr.leave(p4);
          EXPECT_EQ(cr.participants().size(), 3);
          cr.leave(p2);
          EXPECT_EQ(cr.participants().size(), 2);
      }
      
    4. Web请求

      ​ chat_room中有一个log(),依赖网络请求。原函数:

      std::string chat_room::log() {
          std::string* response;
          this->requester->execute("request",response);		// web访问,结果存在response指针中
          return *response;
      }
      

      ​ 在单元测试中,我们只关心被测试部分的逻辑。为了测试这个函数,我们不应该创建真实的 requester,应该使用mock。

      ​ http_request对象:

      class http_request {
      public:
          virtual ~http_request(){}
          virtual bool execute(std::string request, std::string* response)=0;
      };
      

      Mock对象:

      class mock_http_request : public http_request {
      public:
          MOCK_METHOD(bool, execute, (std::string request, std::string * response), (override));
      };
      

      测试用例:

      TEST(ChatRoomTest, log) {
          testing::NiceMock<mock_message_dao> mock_dao;	//在下一部分会提到mock_message_dao
          mock_http_request mock_requester;				//Mock对象
          std::string response = "response";				//期待调用函数的第二个参数将指向这个string对象
          EXPECT_CALL(mock_requester, execute)
          	.WillRepeatedly(							//每次调用都会(WillRepeatedly)执行
          		testing::DoAll(							//每次执行包含多个行为
          			testing::SetArgPointee<1>(response),//将传入参数指针变量response指向response
          			testing::Return(true)));			//返回值为true
          chat_room cr 
          	= chat_room(&mock_dao, &mock_requester);	//将mock对象通过chat_room的constructor注入
          EXPECT_EQ(cr.log(),"response");					//调用和Google Test断言
      }
      
    5. 数据库访问

      chat_room 对象会将聊天者发送的消息存储在redis中。当新用户加入时,chat_room对象从数据库获取所有历史消息发送给该新用户。

      join函数:

      void chat_room::join(chat_participant_ptr participant) {
          participants_.insert(participant);
          std::vector<std::string> recent_msg_strs = 
          	this->dao->get_messages(); 			//从数据库中获取历史消息
          for (std::string recent_msg_str: recent_msg_strs) {
      										    //将每一个消息发送给该聊天参与者	
              auto msg = chat_message();
              msg.set_body_string(recent_msg_str);
              participant->deliver(msg);
          }
      }
      

      message_dao对象:

      class message_dao {
      public:
          virtual ~message_dao(){}
          virtual bool add_message(std::string m)=0;
          virtual std::vector<std::string> get_messages()=0;
      };
      

      Mock对象:

      class mock_message_dao : public message_dao {
      public:
          MOCK_METHOD(bool, add_message, (std::string m), (override));
          MOCK_METHOD(std::vector<std::string>, get_messages, (), (override));
      };
      

      测试用例:

      EST(ChatRoomTest, join) {
          mock_message_dao mock_dao;				//创建mock对象(需要注入chat_room)
          http_request_impl requester;			//创建web访问对象(也需要注入chat_room)
          auto mock_p1 = std::make_shared<mock_chat_participant>();
          										//创建participant的mock指针
          EXPECT_CALL(mock_dao, get_messages)
          	.WillOnce(testing::Return(std::vector<std::string>{"test_msg_body_1", "test_msg_body_2", "test_msg_body_3"}));
          										//指定get_messages调用的返回值
          EXPECT_CALL(*mock_p1, deliver).Times(3);
          										//指定deliver调用的次数
          chat_room cr = chat_room(&mock_dao, &requester);
          										//创建chat_room对象,注入dao和requester
          cr.join(mock_p1);						//调用
      }
      

    四、FAQ

    1、单元测试文件应该放在项目的什么位置?

    ​ 一般来说,我们是会在根目录创建一个tests文件夹,里面放单元测试部分的源码,从而不会和被测试代码混在一起

    ​ 如果需要和其他测试(如接口测试、压力测试)等区分开,可以:

    ​ 1、把tests改成unittests、utests等

    ​ 2、在tests创建不同的子文件夹存放不同类型的测试代码

    2、Google Mock只能Mock虚函数,如果我想Mock非虚函数怎么办?

    ​ 由于Google Mock(及其他大部分Mock框架)通过继承来动态重载机制的限制,一般来说Google Mock只能Mock 虚函数。如果要Mock非虚函数,官方文档提供这几种思路:

    ​ 1、Mock类和原类没有继承关系,测试对象使用函数模板。在测试中,测试对象接收Mock类。

    ​ 2、创建一个接口(抽象类),原类继承自这个接口(抽象类)。在测试中Mock这个接口(抽象类)。

    ​ 这两种方法,都需要对代码进行一定的修改或重构。如果不想修改被测试代码。可以考虑使用hook技术替换被 Mock的部分从而Mock一般函数。

    使用TMock对非虚函数Mock的例子:

    mock函数:

    # include "tmock.h"
    
    class MockClass
    {
    public:
        //注册mock类
        TMOCK_CLASS(MockClass);
        //声明mock类函数,TMOCK_METHOD{n}第一个参数与attach_func_lib第一个参数相同,其余参考与MOCK_METHOD{n}一致。
        TMOCK_METHOD1("original", original, uint32_t(const char * str_file_md5) )
    };
    

    ​ 单测中应用tmock的方法和Google Mock基本一致。但在结束的时候需要使用TMOCK_CLEAR清除exception,detach hook的函数,防止干扰其他单元测试。

    3、Google Test官方文档中说测试套件名称、测试夹具名称、测试名称中不应该出现下划线_,为什么?

    ​ TEST(TestSuiteName, TestName),生成名为TestSuiteName_TestName_Test的类。

    ​ 下划线_是特殊的,因为C++保留以下内容供编译器和标准库使用。所以开头和结尾有下划线很容易让生成的类的标识符不合法。

    ​ 另一方面,下划线可能让不同测试生成相同的类。比如TEST(Time, Files_Like_An_Arrow) {.....}都生成名为Time_Files_Like_An_Arrow_Test的类。

    4、测试输出里有很多Uniteresting mock function call 警告怎么办?

    ​ 创建的Mock对象的某些调用如果没有相应匹配的EXCEPT_CALL,Google Mock会生成这个警告。

    ​ 为了去除这个警告,可以使用NiceMock。比如如果原本使用MockFoo nice_foo;新建mock对象的话,可以改成NiceMock nice_foo; NiceMock是MockFoo的子类。

    五、实践小结

    ​ 框架的使用,无非是一些语法糖的差异和使用的难易程度。不管使用什么语言,什么框架,最关键的是利用单元测试的思路,写出解耦的、可测试的、易于维护的代码,保证代码的质量。

    ​ 单元测试是一种手段,能够一定程度的改善生产力。凡事有度,过犹不及。如果一味盲目的追求测试覆盖率,忽视了测试代码本身的质量。那么各种无效的单元测试反而带来了沉重的维护负担。因此单测的代码,本身也是代码,也是和项目本身的代码一样,需要重构、维护的。

posted @ 2021-10-13 09:04  砚台是黑的  阅读(3207)  评论(0编辑  收藏  举报