C++霧中風(fēng)景番外篇2:Gtest 與 Gmock湃密,聊聊C++的單元測試

正式工作之后诅挑,公司對于單元測試要求比較嚴(yán)格四敞。(筆者之前比較懶,一般很少寫完整的單測~~)拔妥。作為一個合格的開發(fā)工程師忿危,需要為所編寫代碼編寫適量的單元測試是十分必要的,在實際進(jìn)行的開發(fā)工作之中没龙,TDD(Test drivern development) 是一種經(jīng)過實踐可行的開發(fā)方式铺厨。編寫單元測試可以幫助我們在開發(fā)階段就發(fā)現(xiàn)錯誤,并且保證新的修改沒有破壞已有的程序邏輯硬纤。 在 C++之中解滓,常用的測試框架有 Gtest,Boost test筝家,CPPUint 等洼裤。正是由于 Gmock 的加持,讓 Gtest 在多種測試框架之中脫穎而出溪王。今天筆者在這里要和大家聊聊的就是目前我司主力在使用的Gtest腮鞍,以及配套的 Gmock,通過兩者的配合使用莹菱,相信能夠搞定絕大多數(shù)的測試場景了缕减。

1.Gtest 的安裝

筆者目前使用的系統(tǒng)是Deepin 15.6,是基于 Debian jessie的一款國內(nèi)發(fā)行版芒珠。安裝 Gtest 和 GMock 十分簡單:

sudo apt-get install libgtest-dev libgmock-dev 

其他發(fā)行版如:Ubuntu,Centos 應(yīng)該也可以通過自帶的包管理軟件就可以完成安裝了搅裙。但是如果包管理軟件之中沒有帶上對應(yīng)的開發(fā)包皱卓,也可以選擇編譯安裝:

  • 下載源碼
 git clone https://github.com/google/googletest
  • 用 CMake 生成 Makefile之后直接 make 編譯
cd build && cmake .. && make -j 2
  • 最后進(jìn)行安裝
sudo make install

之后只要在/usr/include路徑下找到gtest.h,gmock.h就說明我們安裝成功了。之后只需要在 CMake 之中鏈接對應(yīng)的靜態(tài)庫部逮,就可以利用 Gtest 進(jìn)行單元測試了娜汁。

2.Gtest 的使用

Gtest 十分容易上手,通過其中的定義的宏就可以輕松實現(xiàn)要進(jìn)行單元測試兄朋。并且其中每個單元測試都會計算出對應(yīng)執(zhí)行時間掐禁,可以通過執(zhí)行時間來分析代碼的執(zhí)行效率。

測試函數(shù)TEST

先舉個簡單的栗子颅和,假如現(xiàn)在我們需要測試一下函數(shù)來判斷質(zhì)數(shù)傅事,代碼如下:

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;
}

現(xiàn)在我們用 Gtest 對這個函數(shù)進(jìn)行測試,TEST的宏定義代表了會被RUN_ALL_TESTS執(zhí)行的測試函數(shù)峡扩。在 Gtest 之中提供了兩類斷言ASSERT_*系列和EXPECT_*系列蹭越。兩者的區(qū)別就在于,ASSERT 失敗之后就不會運(yùn)行后續(xù)的測試了教届,但是 EXPECT 雖然失敗响鹃,但是不影響后續(xù)測試的進(jìn)行驾霜。看起來EXPECT會更加靈活一些买置,尤其是需要釋放一些資源或執(zhí)行一些其他邏輯時粪糙,更適合用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 初始化測試框架忿项,必須在運(yùn)行測試之前調(diào)用 RUN_ALL_TESTS 會運(yùn)行所有由TEST 宏定義的測試蓉冈。測試結(jié)果如下圖所示:我們定義的is_true和 is_false同屬同一個測試 case:test_prime,并且成功通過了測試倦卖。

測試結(jié)果

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

Gtest 提供了多種的判斷宏洒擦,包括字符串的判斷,數(shù)值判斷等等怕膛,具體的細(xì)節(jié)可以參照 Gtest 的官方文檔熟嫩,筆者這里不再贅述。

測試函數(shù)TEST_F

很多時候褐捻,我們進(jìn)行一些測試的時候需要進(jìn)行資源初始化工作掸茅,進(jìn)行資源復(fù)用,最后回收資源柠逞。這樣的場景就適合使用 TEST_F的宏來進(jìn)行測試昧狮。TEST_F適用于多種測試場景需要相同數(shù)據(jù)配置的情況,利用了 C++繼承類來實現(xiàn)對父類方法的測試板壮。舉個例子逗鸣,筆者實現(xiàn)了一個跳表,下面是對跳表進(jìn)行測試的代碼:

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進(jìn)行的測試類需要繼承testing::Test類撒璧。同時要實現(xiàn)對應(yīng)的 SetUpTearDown方法,SetUp方式執(zhí)行資源的初始化操作笨使,而TearDown則負(fù)責(zé)資源的釋放卿樱。需要注意的是,上述代碼我們測試了跳表的search,remove,insert方法硫椰,而每個測試是獨(dú)立的進(jìn)行的繁调。也就是每個測試執(zhí)行時都會運(yùn)行對應(yīng)的SetUp和 TearDown方法。

命令行參數(shù)

編譯生成二進(jìn)制的測試執(zhí)行文件之后靶草,直接運(yùn)行就可以執(zhí)行單元測試了蹄胰。但是 Gtest 提供了一些命令行參數(shù)來幫助我們更好的使用,下面介紹一下筆者常用的幾個命令行參數(shù):

  • --gtest_list_tests
    列出所有需要執(zhí)行的測試奕翔,但是并不執(zhí)行烤送。
  • --gtest_filter
    對所執(zhí)行的測試進(jìn)行過濾,支持通配符
    ? 單個字符
    * 任意字符
    - 排除
    ./test --gtest_filter=SkTest.*-SkTest.insert 表示運(yùn)行所有名為SkTest的案例糠悯,排除了SkTest.insert這個案例帮坚。
  • --gtest_repeat=[count]
    設(shè)置測試重復(fù)運(yùn)行的次數(shù)妻往,其中-1表示無限執(zhí)行。

3.Gmock 的使用

上述 Gtest 的使用應(yīng)該能夠滿足絕大多數(shù)小型項目的測試場景了试和。但是對于一些涉及數(shù)據(jù)庫交互讯泣,網(wǎng)絡(luò)通信的大型項目的測試場景,我們很難仿真一個真實的環(huán)境進(jìn)行單元測試阅悍。所以這時就需要引入** Mock Objects **(模擬對象)來模擬程序的一部分來構(gòu)造測試場景好渠。Mock Object模擬了實際對象的接口,通過一些簡單的代碼模擬實際對象部分的邏輯节视,實現(xiàn)起來簡單很多拳锚。通過 Mock object 的方式可以更好的提升項目的模塊化程度,隔離不同的程序邏輯或環(huán)境寻行。

至于如何使用 Mock Object 呢霍掺?這里要引出本章的主角 Gmock 了,接下來筆者將編寫一個簡要的 Mock對象并進(jìn)行單元測試拌蜘,來展示一下 GMock 的用法杆烁。這里我們用 Gmock 模擬一個 kv 存儲引擎,并運(yùn)行一些簡單的測試邏輯简卧。下面的代碼是我們要模擬的 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举娩,后面的數(shù)字代表了模擬函數(shù)的參數(shù)個數(shù)析校,如MOCK_METHOD0MOCK_METHOD1铜涉。它接受兩個參數(shù):

  • 參數(shù)1智玻,方法名稱。
  • 參數(shù)2骄噪,函數(shù)的指針的定義
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&));
};

通過這個宏定義,我們已經(jīng)初步模擬出對應(yīng)的方法了蠢箩。接下來我們需要告訴 Mock Object 被調(diào)用時的正確行為链蕊。

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 的對應(yīng)行為谬泌,其中 WillOnce代表調(diào)用一次返回的結(jié)果滔韵。通過鏈?zhǔn)秸{(diào)用的方式,我們就可以構(gòu)造出所有我們想要的模擬結(jié)果掌实。當(dāng)然如果每次調(diào)用的結(jié)果都一樣陪蜻,這里也可以使用WillRepeatedly直接返回對應(yīng)的結(jié)果。由上述代碼可以看到贱鼻,這里我們用寥寥數(shù)十行代碼就模擬出了一個 KV 存儲引擎宴卖,可見 Gmock 的強(qiáng)大滋将。

這里要注意,在通過 Gmock 來編寫 Mock Object 時症昏,能夠模擬的方法是對于原抽象類之中的virtual 方法随闽。這個是因為 C++只有通過virtual的方式才能實現(xiàn)子類覆寫的多態(tài),這一點(diǎn)在編寫代碼進(jìn)行抽象和編寫 Mock Object 的時候需要多加注意肝谭。

4.小結(jié)

通過Gtest 與 Gmock 的使用掘宪,能夠覆蓋絕大多數(shù)進(jìn)行 C++ 單元測試的場景,同時也減少了我們編寫單元測試的工作攘烛。筆者希望通過本篇文章來拋磚引玉魏滚,希望大家多寫單測。在筆者實際的工作經(jīng)驗之中坟漱,單測給項目帶來的影響是極其正面的鼠次,一定要堅持寫單測,堅持寫單測靖秩,堅持寫單測~~~P刖臁!沟突!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末花颗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子惠拭,更是在濱河造成了極大的恐慌扩劝,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件职辅,死亡現(xiàn)場離奇詭異棒呛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)域携,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門簇秒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秀鞭,你說我怎么就攤上這事趋观。” “怎么了锋边?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵皱坛,是天一觀的道長。 經(jīng)常有香客問我豆巨,道長剩辟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮贩猎,結(jié)果婚禮上熊户,老公的妹妹穿的比我還像新娘。我一直安慰自己融欧,他們只是感情好敏弃,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著噪馏,像睡著了一般麦到。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上欠肾,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天瓶颠,我揣著相機(jī)與錄音,去河邊找鬼刺桃。 笑死粹淋,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瑟慈。 我是一名探鬼主播桃移,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼葛碧!你這毒婦竟也來了借杰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤进泼,失蹤者是張志新(化名)和其女友劉穎亲怠,沒想到半個月后甘晤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體襟诸,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡公给,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了洋措。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片济蝉。...
    茶點(diǎn)故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖菠发,靈堂內(nèi)的尸體忽然破棺而出王滤,到底是詐尸還是另有隱情,我是刑警寧澤雷酪,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布淑仆,位于F島的核電站涝婉,受9級特大地震影響哥力,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一吩跋、第九天 我趴在偏房一處隱蔽的房頂上張望寞射。 院中可真熱鬧,春花似錦锌钮、人聲如沸桥温。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽侵浸。三九已至,卻和暖如春氛谜,著一層夾襖步出監(jiān)牢的瞬間掏觉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工值漫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留澳腹,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓杨何,卻偏偏與公主長得像酱塔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子危虱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Gtest Github使用 gtest(gmock) 方便我們編寫組織 c++ 單元測試羊娃。 編譯 lib 到 g...
    orientlu閱讀 4,301評論 0 1
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)槽地,斷路器迁沫,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • Android單元測試介紹 處于高速迭代開發(fā)中的Android項目往往需要除黑盒測試外更加可靠的質(zhì)量保障,這正是單...
    東經(jīng)315度閱讀 3,096評論 6 37
  • 像一只會跳舞的兔子 總有陪伴 沉睡的貓 芬芳的花 或者 漂亮的裙子 慵懶的午后 冰鎮(zhèn)飲料配上咕嚕咕嚕的花香 吶 舒...
    李威Lvee閱讀 273評論 6 7
  • 文/影月笙歌 沒有人栽種 它卻自己扎根 在墻頭捌蚊,在檐下 在田野集畅,在路邊 在水溝旁,在小山上 沒有人護(hù)養(yǎng) 它就自己生...
    知遙記閱讀 147評論 0 1