因為最近在寫的一些C++程式碼,需要給它寫單元測試,所以就得去找一個C++的測試框架。正好之前實驗室的同學有推薦過gtest,所以就不糾結了,直接去gtest的專案主頁看。
gtest好處都有啥?這個可真說不出來,畢竟C++方面的測試框架,我就只用過gtest。對比於其他語言的測試框架,比如javascript的mocha、jasmine,ruby的minitest,gtest差強人意。不過前面舉的例子,都是動態語言的測試框架(我沒用過JUnit)。在測試的方面,動態語言對於靜態語言具有巨大的優勢,因為動態語言可以輕易地hack到待測試方法的內部。何況C++連反射都沒有……
不過,總體來說,gtest已經能夠滿足我對測試的要求了。好了,外話就此打住,開始進入正文。
安裝
安裝可以見專案的README。這裡摘抄兼意譯一下:
- 在下下來的gtest原始碼資料夾外建立一個構建用的資料夾,這裡就用
lib
好了。
舉個例子, 如果你把gtest
下載到~/code
檔案下了,那麼就mkdir ../lib
。
- 切到該資料夾下,對gtest原始碼所在的目錄使用cmake。接著會產生一份Makefile/VC專案檔案/Xcode專案檔案(會出現哪一個取決於你用的系統)。剩下來的,就只是編譯而已。
依上面的例子為例,就是
cd ../lib
cmake ../gtest-xxxx
make # 或者build一個VC/Xcode專案
最後會生成一些東西,其中有一個libgtest.a
的靜態庫(具體內容取決於你的系統,這是Linux上的情況),這就是我們想要的。
- 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!