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
二、用法說明
-
Fake、Mock、Stub
- Fake對象有具體的實現,但采取一些捷徑,比如用內存替代真實的數據庫讀取
- Stub對象沒有具體的實現,只是返回提前准備好的數據
- Mock對象和Stub類似,只是在測試中需要調用時,針對某種輸入指定期望的行為,Mock和Stub的區別是,Mock除了返回數據還可以指定期望以驗證行為。
-
簡單示例
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類的步驟:
-
MockTutle繼承Tutle
-
找到Tutle的一個虛函數
-
在public的部分,寫一個MOCK_METHOD()
-
將虛函數的函數簽名復制進MOCK_METHOD()中,加兩個逗號:
一個在返回類型和函數名之間,另一個在函數名和參數列表之間
例如:void PenDown() 有三部分:void、PenDown、和(),這三部分就是MOCK_METHOD的前三個參數
-
如果要模擬const方法,添加一個包含const的第四個參數,必須到括號
-
建議添加override關鍵字。所以對於const方法,第四個參數變為(const, override),對於非const方法,第四個參數變為override。這不是強制性的。
-
重復步驟直至完成要模擬的所有虛擬函數
-
-
在測試中使用Mock
在測試中使用Mock的步驟:
- 從testing名稱空間導入gmock.h的函數名(每個文件只需要執行一次)
- 創建一些Mock對象
- 指定對它們的期望(方法將被調用多少次? 帶有什么參數? 每次應該做什么? 返回什么值 等等)
- 使用Mock對象;可以使用googletest斷言檢查結果。如果mock函數的調用超出預期或參數錯誤,將會立即收到錯誤信息。
- 當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()還沒有被調用或者調用兩次以上,測試會失敗。
-
指定期望
EXCEPT_CALL(指定期望)是使用Google Mock的核心。EXCEPT_CALL的作用是兩方面的:
-
告訴這個Mock(假)方法如何模擬原始方法:
我們在EXPECT_CALL中告訴Google Mock,某個對象的某個方法第一次被調用時,會修改某個參數,會返回某個值,第二次調用時, 會修改某個參數,會返回某個值......
-
驗證被調用的情況
我們在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"));
在上面的例子中,創建了兩個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寫單元測試用例。
-
項目結構
示例項目是一個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
-
普通測試
如果被測試的函數不包含外部依賴,用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宏來驗證變量值或者函數調用的返回值。
-
簡單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); }
-
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斷言 }
-
數據庫訪問
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的子類。 五、實踐小結
框架的使用,無非是一些語法糖的差異和使用的難易程度。不管使用什么語言,什么框架,最關鍵的是利用單元測試的思路,寫出解耦的、可測試的、易於維護的代碼,保證代碼的質量。
單元測試是一種手段,能夠一定程度的改善生產力。凡事有度,過猶不及。如果一味盲目的追求測試覆蓋率,忽視了測試代碼本身的質量。那么各種無效的單元測試反而帶來了沉重的維護負擔。因此單測的代碼,本身也是代碼,也是和項目本身的代碼一樣,需要重構、維護的。
-