正式工做以後,公司對於單元測試要求比較嚴格。(筆者以前比較懶,通常不多寫完整的單測~~)。做爲一個合格的開發工程師,須要爲所編寫代碼編寫適量的單元測試是十分必要的,在實際進行的開發工做之中,TDD(Test drivern development) 是一種通過實踐可行的開發方式。編寫單元測試能夠幫助咱們在開發階段就發現錯誤,而且保證新的修改沒有破壞已有的程序邏輯。 在 C++之中,經常使用的測試框架有 Gtest,Boost test,CPPUint 等。正是因爲 Gmock 的加持,讓 Gtest 在多種測試框架之中脫穎而出。今天筆者在這裏要和你們聊聊的就是目前我司主力在使用的Gtest,以及配套的 Gmock,經過二者的配合使用,相信可以搞定絕大多數的測試場景了。git
筆者目前使用的系統是Deepin 15.6,是基於 Debian jessie的一款國內發行版。安裝 Gtest 和 GMock 十分簡單:github
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 進行單元測試了。網絡
Gtest 十分容易上手,經過其中的定義的宏就能夠輕鬆實現要進行單元測試。而且其中每一個單元測試都會計算出對應執行時間,能夠經過執行時間來分析代碼的執行效率。app
先舉個簡單的栗子,假如如今咱們須要測試一下函數來判斷質數,代碼以下:框架
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 提供了多種的判斷宏,包括字符串的判斷,數值判斷等等,具體的細節能夠參照 Gtest 的官方文檔,筆者這裏再也不贅述。單元測試
不少時候,咱們進行一些測試的時候須要進行資源初始化工做,進行資源複用,最後回收資源。這樣的場景就適合使用 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 的使用應該可以知足絕大多數小型項目的測試場景了。可是對於一些涉及數據庫交互,網絡通訊的大型項目的測試場景,咱們很難仿真一個真實的環境進行單元測試。因此這時就須要引入** 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。它接受兩個參數:
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 的時候須要多加註意。
經過Gtest 與 Gmock 的使用,可以覆蓋絕大多數進行 C++ 單元測試的場景,同時也減小了咱們編寫單元測試的工做。筆者但願經過本篇文章來拋磚引玉,但願你們多寫單測。在筆者實際的工做經驗之中,單測給項目帶來的影響是極其正面的,必定要堅持寫單測,堅持寫單測,堅持寫單測~~~!!!