gtest快速上手

spacewander發表於2019-05-10

因為最近在寫的一些C++程式碼,需要給它寫單元測試,所以就得去找一個C++的測試框架。正好之前實驗室的同學有推薦過gtest,所以就不糾結了,直接去gtest的專案主頁看。

gtest好處都有啥?這個可真說不出來,畢竟C++方面的測試框架,我就只用過gtest。對比於其他語言的測試框架,比如javascript的mocha、jasmine,ruby的minitest,gtest差強人意。不過前面舉的例子,都是動態語言的測試框架(我沒用過JUnit)。在測試的方面,動態語言對於靜態語言具有巨大的優勢,因為動態語言可以輕易地hack到待測試方法的內部。何況C++連反射都沒有……

不過,總體來說,gtest已經能夠滿足我對測試的要求了。好了,外話就此打住,開始進入正文。

安裝

安裝可以見專案的README。這裡摘抄兼意譯一下:

  1. 在下下來的gtest原始碼資料夾外建立一個構建用的資料夾,這裡就用lib好了。

舉個例子, 如果你把gtest下載到~/code檔案下了,那麼就mkdir ../lib

  1. 切到該資料夾下,對gtest原始碼所在的目錄使用cmake。接著會產生一份Makefile/VC專案檔案/Xcode專案檔案(會出現哪一個取決於你用的系統)。剩下來的,就只是編譯而已。

依上面的例子為例,就是

cd ../lib
cmake ../gtest-xxxx
make # 或者build一個VC/Xcode專案

最後會生成一些東西,其中有一個libgtest.a的靜態庫(具體內容取決於你的系統,這是Linux上的情況),這就是我們想要的。

  1. gtest在使用時,依賴這個靜態庫和gtest的標頭檔案。在編譯測試程式碼時,使用

$(CC) -isystem $(GTESTHEADERS) -pthread $(CCFLAGS) $(SRC) $(GTESTLIB)

其中GTESTHEADERS是gtest標頭檔案所在的資料夾,位於下載下來的程式碼的include路徑;GTESTLIB是編譯出的靜態庫。如果不需要跨平臺,可以考慮把gtest標頭檔案和靜態庫一起隨專案釋出,這樣別人就無需再安裝gtest了。

是不是挺麻煩的?習慣了就好……C++需要的,不是那麼多奇技淫巧,而是一個好用的包管理器。

快速上手

下面就對比著其他測試框架,來一次快速上手:

首先,一個測試框架至少由兩個部分組成,測試結構斷言方法(對不起這兩個詞都是我在剛剛半分鐘內創造的)。

測試結構:測試用例的組織方式(測試用例、測試函式)、執行方式、鉤子函式。

斷言方法:可用的斷言,比如expect().toBe()assert_respond_to等等。

那我們就從這兩方面介紹。

測試結構

如果你用過動態語言的測試框架,你會注意到測試函式是這樣組織的:

class XXXTest ...
  def test_xxx
  def test_yyy

或者

describe ...
  it xxx
  it yyy

gtest的組織方式也不例外。不過它並不是將測試函式巢狀到測驗用例之下。看一下示例就明白:

// test.cpp
#include "../lib/gtest.h" // 注意要把gtest.h給include進來
TEST(Gtest, testItWorks)
{ 
    EXPECT_EQ(2, 1 + 1);
}

這裡,Gtest相當於XXXTest類,而testItWorks則是一個要執行的測試函式。

那麼怎麼讓它執行呢?我們還需要另外一個檔案,實現一個執行測試的main函式。姑且叫它main.cpp好了:

 #include "../lib/gtest.h" // 注意要把gtest.h給include進來

int main(int argc, char** argv) 
{ 
    testing::InitGoogleTest(&argc, argv); 

    // Runs all tests using Google Test. 
    return RUN_ALL_TESTS(); 
 }

接下來就是編譯了(路徑什麼的得按照你的實際情況來):
g++ -isystem ../lib -pthread test.cpp main.cpp ../lib/libgtest.a

執行生成的a.out檔案,你看,測試結果出來了:

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Gtest
[ RUN      ] Gtest.testItWorks
[       OK ] Gtest.testItWorks (0 ms)
[----------] 1 test from Gtest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (1 ms total)
[  PASSED  ] 1 test.

如果你只是想執行部分測試,執行時新增--gtest_filter選項。如`--gtest_filter=Gtest.*`就是隻執行Gtest下的測試函式。

接下來我們講講鉤子函式。一般來說,在每個測試函式之前,往往會做一些通用的操作,比如準備fixture等等。有時在測試函式結束後,還需要做掃尾的任務。這時候就需要使用鉤子函式了。

先講一下如何設定fixture。Fixture就是在每個測試函式執行之前,事先準備好的被測試物件。
在gtest中,需要定義一個類來新增fixture。


#include <vector> #include "../lib/gtest.h" class Gtest : public ::testing::Test { protected: std::vector<int> v; }; TEST_F(Gtest, testItWorks) { EXPECT_EQ(2, 1 + 1); } TEST_F(Gtest, testVector) { EXPECT_NE(1, v.size()); }

請注意有兩點變化,一個是我們定義了Gtest類,另一個是我們把TEST改成TEST_F。通過定義與測試用例同名的類,我們可以把fixture放在它的protected域或者public域,這樣每個測試函式都能用到。如果想每個測試函式共用同一個fixture,把它設定為static的類變數。另外,如果定義了使用fixture的類,就要使用TEST_F而不是TEST

接下看看兩個鉤子函式,SetUp和Teardown。

修改Gtest定義為:

#include <iostream>

class Gtest : public ::testing::Test 
{
protected:
    virtual void SetUp()
    {
        std::cout << "Hi 
";
    }
    virtual void TearDown()
    {
        std::cout << "Bye 
";
    }
    std::vector<int> v;
};

對應的輸出是這樣:

[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from Gtest
[ RUN      ] Gtest.testItWorks
Hi 
Bye 
[       OK ] Gtest.testItWorks (0 ms)
[ RUN      ] Gtest.testVector
Hi 
Bye 
[       OK ] Gtest.testVector (0 ms)
[----------] 2 tests from Gtest (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran. (1 ms total)
[  PASSED  ] 2 tests.

斷言方法

斷言方法的豐富程度,決定了寫測試程式碼時,需不需要做很多hack。
這裡直接把各種方法貼出來:

基本斷言

出錯會停止測試的斷言 出錯不會停止測試的斷言 意思
ASSERT_EQ(expected, actual) EXPECT_EQ(expected, actual) val1 == val2
ASSERT_NE(val1, val2) EXPECT_NE(val1, val2) val1 != val2
ASSERT_LT(val1, val2) EXPECT_LT(val1, val2) val1 < val2
ASSERT_LE(val1, val2) EXPECT_LE(val1, val2) val1 <= val2
ASSERT_GT(val1, val2) EXPECT_GT(val1, val2) val1 > val2
ASSERT_GE(val1, val2) EXPECT_GE(val1, val2) val1 >= val2
ASSERT_TRUE(condition) EXPECT_TRUE(condition) condition為true
ASSERT_FALSE(condition) EXPECT_FALSE(condition) condition為false

適用於C Style字串的斷言

出錯會停止測試的斷言 出錯不會停止測試的斷言 意思
ASSERT_STREQ(expected_str, actual_str) EXPECT_STREQ(expected_str, actual_str) 兩個字串相等
ASSERT_STRNE(str1, str2) EXPECT_STRNE(str1, str2) 兩個字串不等
ASSERT_STRCASEEQ(expected_str,actual_str) EXPECT_STRCASEEQ(expected_str,actual_str) 兩個字串相等,忽略大小寫
ASSERT_STRCASENE(str1, str2) EXPECT_STRCASENE(str1, str2) 兩個字串不等,忽略大小寫

適用於浮點數比較的斷言

出錯會停止測試的斷言 出錯不會停止測試的斷言 意思
ASSERT_FLOAT_EQ(expected, actual) EXPECT_FLOAT_EQ(expected, actual) float型別近似相等
ASSERT_DOUBLE_EQ(expected, actual) EXPECT_DOUBLE_EQ(expected, actual) double型別近似相等

近似相等的斷言

出錯會停止測試的斷言 出錯不會停止測試的斷言 意思
ASSERT_NEAR(val1, val2, abs_error) EXPECT_NEAR(val1, val2, abs_error) val1和val2的差的絕對值小於abs_error

是否丟擲異常的斷言

出錯會停止測試的斷言 出錯不會停止測試的斷言 意思
ASSERT_THROW(statement, exception_type) EXPECT_THROW(statement, exception_type) 程式碼塊會丟擲指定異常
ASSERT_ANY_THROW(statement) EXPECT_ANY_THROW(statement) 程式碼塊會丟擲異常
ASSERT_NO_THROW(statement) EXPECT_NO_THROW(statement) 程式碼塊不會丟擲異常

借用官方示例:

ASSERT_THROW(Foo(5),bar_exception);

EXPECT_NO_THROW({
 int n=5;
 Bar(&n);
});

是不是感覺缺少了什麼?

缺了比較C Style陣列的斷言呀!

有一次我想找個如何比較兩個int型別陣列的斷言,卻怎麼也找不到。網上的解決辦法,都是需要引入另外一個Google Mock框架。我不想再增加別的依賴,所以最後hack一下,把int型別陣列轉換成vector<int>

我介紹的大概就這麼多,最後借用gtest的README上的一句話:

Happy testing!

相關文章