C++霧中風景番外篇2:Gtest 與 Gmock,聊聊C++的單元測試
正式工作之後,公司對於單元測試要求比較嚴格。(筆者之前比較懶,一般很少寫完整的單測~~)。作為一個合格的開發工程師,需要為所編寫程式碼編寫適量的 單元測試 是十分必要的,在實際進行的開發工作之中,TDD( Test drivern development ) 是一種經過實踐可行的開發方式。 編寫單元測試可以幫助我們在開發階段就發現錯誤,並且保證新的修改沒有破壞已有的程式邏輯。 在 C++之中,常用的測試框架有 Gtest,Boost test,CPPUint 等。正是由於 Gmock 的加持,讓 Gtest 在多種測試框架之中脫穎而出。今天筆者在這裡要和大家聊聊的就是目前我司主力在使用的 Gtest ,以及配套的 Gmock ,通過兩者的配合使用,相信能夠搞定絕大多數的測試場景了。
1.Gtest 的安裝
筆者目前使用的系統是 Deepin 15.6 ,是基於 Debian jessie 的一款國內發行版。安裝 Gtest 和 GMock 十分簡單:
sudo apt-get install libgtest-dev libgmock-dev
其他發行版如: Ubuntu,Centos 應該也可以通過自帶的包管理軟體就可以完成安裝了。但是如果包管理軟體之中沒有帶上對應的開發包,也可以選擇編譯安裝:
- 下載原始碼
git clone https://github.com/google/googletest
cd build && cmake .. && make -j 2
- 最後進行安裝
sudo make install
之後只要在/usr/include路徑下找到 gtest.h,gmock.h 就說明我們安裝成功了。之後只需要在 CMake 之中連結對應的靜態庫,就可以利用 Gtest 進行單元測試了。
2.Gtest 的使用
Gtest 十分容易上手,通過其中的定義的巨集就可以輕鬆實現要進行單元測試。並且其中每個單元測試都會計算出對應執行時間,可以通過執行時間來分析程式碼的執行效率。
測試函式TEST
先舉個簡單的栗子,假如現在我們需要測試一下函式來判斷 質數 ,程式碼如下:
bool is_prime(int num) { if (num < 2) return false; for(int i = 2; i <= sqrt(num) + 1; i++) { if (num % i == 0) return false; } return true; }
現在我們用 Gtest 對這個函式進行測試, TEST 的巨集定義代表了會被 RUN_ALL_TESTS 執行的測試函式。在 Gtest 之中提供了兩類斷言 ASSERT_* 系列和 EXPECT_* 系列。兩者的區別就在於, ASSERT 失敗之後就不會執行後續的測試了,但是 EXPECT 雖然失敗,但是不影響後續測試的進行。看起來 EXPECT 會更加靈活一些,尤其是需要釋放一些資源或執行一些其他邏輯時,更適合用 EXPECT 。
TEST(test_prime, is_true) { EXPECT_TRUE(is_prime(5)); ASSERT_TRUE(is_prime(5)); EXPECT_TRUE(is_prime(3)); } TEST(test_prime, is_false) { ASSERT_FALSE(is_prime(4)); EXPECT_FALSE(is_prime(4)); } int main(int argc,char *argv[]) { testing::InitGoogleTest(&argc, argv); RUN_ALL_TESTS(); }
testing::InitGoogleTest初始化測試框架,必須在執行測試之前呼叫 RUN_ALL_TESTS 會執行所有由TEST 巨集定義的測試。測試結果如下圖所示:我們定義的 is_true和 is_false同屬同一個測試 case:test_prime ,並且成功通過了測試。

上面我們使用了這TRUE 與 FALSE 的判斷巨集:

Gtest 提供了多種的判斷巨集,包括字串的判斷,數值判斷等等,具體的細節可以參照 ofollow,noindex" target="_blank">Gtest 的官方文件 ,筆者這裡不再贅述。
測試函式TEST_F
很多時候,我們進行一些測試的時候需要進行 資源初始化工作,進行資源複用,最後回收資源 。這樣的場景就適合使用 TEST_F 的巨集來進行測試。 TEST_F 適用於多種測試場景需要相同資料配置的情況,利用了 C++繼承類來實現對父類方法的測試。舉個例子,筆者實現了一個跳錶,下面是對跳錶進行測試的程式碼:
class Test_Skiplist : public testing::Test { public: virtual void SetUp() { std::cout << "Set Up Test" << std::endl; _sl->load(); } virtual void TearDown() { std::cout << "Tear Down Test" << std::endl; _sl->dump(); } ~Test_Skiplist(){}; SkipList* _sl = new SkipList(); }; TEST_F(Test_Skiplist, insert) { std::string test_string("happen"); ASSERT_EQ(_sl->insert("1", test_string.c_str(), test_string.size()), Status::SUCCESS); test_string = "lee"; ASSERT_EQ(_sl->insert("2", test_string.c_str(), test_string.size()), Status::SUCCESS); uint64_t data64 = 50; ASSERT_EQ(_sl->insert("50", reinterpret_cast<char *>(&data64), 8), Status::SUCCESS); uint32_t data32 = 20; ASSERT_EQ(_sl->insert("20", reinterpret_cast<char *>(&data32), 4), Status::SUCCESS); } TEST_F(Test_Skiplist, search) { ASSERT_EQ(_sl->search("1")->value_size, 6); ASSERT_STREQ(std::string(_sl->search("1")->value.get()).c_str(), "happen"); ASSERT_EQ(_sl->search("3"), nullptr); } TEST_F(Test_Skiplist, remove) { ASSERT_EQ(_sl->remove("1"), Status::SUCCESS); ASSERT_EQ(_sl->remove("1"), Status::FAIL); ASSERT_EQ(_sl->search("1"), nullptr); }
由上述程式碼可以看到,通過 TEST_F 進行的測試類需要繼承 testing::Test 類。同時要實現對應的 SetUp 與 TearDown 方法, SetUp 方式執行資源的初始化操作,而 TearDown 則負責資源的釋放。需要注意的是,上述程式碼我們測試了跳錶的 search,remove,insert 方法,而每個測試是 獨立的進行 的。也就是每個測試執行時都會執行對應的 SetUp和 TearDown 方法。
命令列引數
編譯生成二進位制的測試執行檔案之後,直接執行就可以執行單元測試了。但是 Gtest 提供了一些命令列引數來幫助我們更好的使用,下面介紹一下筆者常用的幾個命令列引數:
- --gtest_list_tests
列出所有需要執行的測試,但是並不執行。 - --gtest_filter
對所執行的測試進行過濾,支援萬用字元
? 單個字元
* 任意字元
- 排除
./test --gtest_filter=SkTest.*-SkTest.insert 表示執行所有名為 SkTest 的案例,排除了SkTest.insert這個案例。 - --gtest_repeat=[count]
設定測試重複執行的次數,其中-1表示無限執行。
3.Gmock 的使用
上述 Gtest 的使用應該能夠滿足絕大多數小型專案的測試場景了。但是對於一些涉及 資料庫互動,網路通訊 的大型專案的測試場景,我們很難模擬一個真實的環境進行單元測試。所以這時就需要引入** Mock Objects **(模擬物件)來模擬程式的一部分來構造測試場景。Mock Object模擬了實際物件的介面,通過一些簡單的程式碼模擬實際物件部分的邏輯,實現起來簡單很多。通過 Mock object 的方式可以更好的提升專案的模組化程度,隔離不同的程式邏輯或環境。
至於如何使用 Mock Object 呢?這裡要引出本章的主角 Gmock 了,接下來筆者將編寫一個簡要的 Mock物件並進行單元測試,來展示一下 GMock 的用法。這裡我們用 Gmock 模擬一個 kv 儲存引擎,並執行一些簡單的測試邏輯。下面的程式碼是我們要模擬的 kv 儲存引擎的標頭檔案:
#ifndef LDB_KVDB_MOCK_H #define LDB_KVDB_MOCK_H class KVDB { public: std::string get(const std::string &key) const; Status set(const std::string &key, const std::string &value); Status remove(const std::string &key); }; #endif //LDB_KVDB_MOCK_H
然後我們需要定義個 Mock 類來繼承 KVDB,並且定義需要模擬的方法: get, set, remove 。這裡我們用到了巨集定義 MOCK_METHOD ,後面的數字代表了模擬函式的引數個數,如 MOCK_METHOD0 , MOCK_METHOD1 。它接受兩個引數:
- 引數1,方法名稱。
- 引數2,函式的指標的定義
class MockKVDB : public KVDB { public: MockKVDB() { } MOCK_METHOD1(remove, Status(const std::string&)); MOCK_METHOD2(set, Status(const std::string&, const std::string&)); MOCK_METHOD1(get, std::string (const std::string&)); };
通過這個巨集定義,我們已經初步模擬出對應的方法了。接下來我們需要告訴 Mock Object 被呼叫時的正確行為。
TEST_F(TestKVDB, setstr) { EXPECT_CALL(*kvdb, set(_,_)).WillRepeatedly(Return(Status::SUCCESS)); ASSERT_EQ(kvdb->set("1", "happen"), Status::SUCCESS); ASSERT_EQ(kvdb->set("2", "lee"), Status::SUCCESS); ASSERT_EQ(kvdb->set("happen", "1"), Status::SUCCESS); ASSERT_EQ(kvdb->set("lee", "2"), Status::SUCCESS); } TEST_F(TestKVDB, getstr) { EXPECT_CALL(*kvdb, get(_)) \ .WillOnce(Return("happen")) .WillOnce(Return("lee")) .WillOnce(Return("1")) .WillOnce(Return("2")); ASSERT_STREQ(kvdb->get("1").c_str(), "happen"); ASSERT_STREQ(kvdb->get("2").c_str(), "lee"); ASSERT_STREQ(kvdb->get("happen").c_str(), "1"); ASSERT_STREQ(kvdb->get("lee").c_str(), "2"); } TEST_F(TestKVDB, remove) { EXPECT_CALL(*kvdb, remove(_)).WillOnce(Return(Status::SUCCESS)). WillOnce(Return(Status::FAIL)); EXPECT_CALL(*kvdb, get(_)) \ .WillOnce(Return("")); ASSERT_EQ(kvdb->remove("1"), Status::SUCCESS); ASSERT_EQ(kvdb->get("1"), ""); ASSERT_EQ(kvdb->remove("1"), Status::FAIL); }
由上述程式碼可以瞭解,這裡通過了 EXPECT_CALL 來指定 Mock Object 的對應行為,其中 WillOnce 代表呼叫一次返回的結果。通過 鏈式呼叫 的方式,我們就可以構造出所有我們想要的模擬結果。當然如果每次呼叫的結果都一樣,這裡也可以使用 WillRepeatedly 直接返回對應的結果。由上述程式碼可以看到,這裡我們用寥寥數十行程式碼就模擬出了一個 KV 儲存引擎,可見 Gmock 的強大。
這裡要注意,在通過 Gmock 來編寫 Mock Object 時,能夠模擬的方法是對於原抽象類之中的 virtual 方法。這個是因為 C++只有通過 virtual 的方式才能實現子類覆寫的多型,這一點在編寫程式碼進行抽象和編寫 Mock Object 的時候需要多加註意。
4.小結
通過Gtest 與 Gmock 的使用,能夠覆蓋絕大多數進行 C++ 單元測試的場景,同時也減少了我們編寫單元測試的工作。筆者希望通過本篇文章來拋磚引玉,希望大家多寫單測。在筆者實際的工作經驗之中,單測給專案帶來的影響是極其正面的,一定要 堅持寫單測,堅持寫單測,堅持寫單測 ~~~!!!