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的子類。

    五、實踐小結

    ​ 框架的使用,無非是一些語法糖的差異和使用的難易程度。不管使用什么語言,什么框架,最關鍵的是利用單元測試的思路,寫出解耦的、可測試的、易於維護的代碼,保證代碼的質量。

    ​ 單元測試是一種手段,能夠一定程度的改善生產力。凡事有度,過猶不及。如果一味盲目的追求測試覆蓋率,忽視了測試代碼本身的質量。那么各種無效的單元測試反而帶來了沉重的維護負擔。因此單測的代碼,本身也是代碼,也是和項目本身的代碼一樣,需要重構、維護的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM