Google 單元測試框架
Gtest Github
使用 gtest(gmock) 方便我們編寫組織 c++ 單元測試。
編譯 lib
到 github 拉取程式碼或者下載某個版本的 zip 包到本地目錄,參考 gtest 中的 README.md 如何編譯庫和編譯自己的程式碼,下面簡單介紹下編譯方法
手動編譯
$ g++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \
-pthread -c ${GTEST_DIR}/src/gtest-all.cc
$ ar -rv libgtest.a gtest-all.o
cmake 編譯
gtest 已經提供了 cmakelist,可以直接使用cmake 生成 makefile, 編譯庫和 sample
$ mkdir mybuild # Create a directory to hold the build output.
$ cd mybuild
$ cmake ${GTEST_DIR} # Generate native build scripts.
$ make
然後就可以在編譯自己的測試程式時連結 gtest 了。
$ g++ -isystem ${GTEST_DIR}/include -pthread path/to/your_test.cc libgtest.a -o your_test
跟多詳細內容參考 readme 和程式碼中提供的例子(samples ; make 目錄下),比如如何解決重複定義巨集等問題。
gtest 測試程式
通過 程式設計參考 和 原始碼中 sample 目錄下的示例
,我們可以很快上手 gtest。gtest 定義了巨集供我們寫斷言語句,一個或者多個斷言組成我們的測試用例 case,多個測試用例有時候需要共享一些通用物件,可以把這些用例放在同一個 fixture 中。
斷言和 case
gtest 斷言提供兩個版本
-
ASSERT_*
版本斷言,在同一個 case 中(測試函式)中,ASSERT_* 失敗就會終止當前用例,開始其他 case ; -
EXPECT_*
版本,當斷言失敗時,會報錯,但是會繼續執行剩餘語句。
完整的 巨集定義, 或見原始碼 include/gtest/gtest.h
使用哪種語句斷言取決自己用例場景,如當前語句失敗時後續語句沒有繼續執行意義,則可以直接使用 ASSERT 終止,否則使用 EXPECT 可以發現更多錯誤。
如果用例之間不需要什麼公用資源,相互獨立,可以使用如下方式定義每一個 case
TEST(套件名,用例名)
{
//套件名和用例名自定義
//斷言語句
//如一般的c++ 函式,不 return value
}
進入目錄 sample 中, 以 sample1_unittest.cc 為例子
#include "sample1.h" // 測試物件標頭檔案,介面
#include "gtest/gtest.h" // gtest 標頭檔案
TEST(IsPrimeTest, Negative) {
EXPECT_FALSE(IsPrime(-1)) << "這樣子失敗時列印自己的資訊";
EXPECT_FALSE(IsPrime(-2)); // 如果此斷言失敗,還會繼續執行下一個
EXPECT_FALSE(IsPrime(INT_MIN));
}
TEST(IsPrimeTest, Negative) {
EXPECT_FALSE(IsPrime(-1));
ASSERT_FALSE(IsPrime(-2)); // 如果此斷言失敗,下一條不執行,這個case 結束
EXPECT_FALSE(IsPrime(INT_MIN));
}
編譯修改的測試程式碼,其中 libgtest.a
是 gtest 的庫。
g++ -isystem ../include/ ./sample1.cc ./sample1_unittest.cc -pthread ../libgtest.a ../libgtest_main.a
連結 libgtest_main.a
是為了使用 src/gtest_main.cc
中定義 main 函式,執行所用測試用例,否者,也可以自己定義 main。
#include <stdio.h>
#include "gtest/gtest.h"
int main(int argc, char **argv) {
printf("Running main() from gtest_main.cc\n");
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
編譯後執行輸出 bin 直接執行便執行所有用例,可以使用 -h
檢視可選的執行引數,如--gtest_filter=IsPrimeTest.Negative
指定執行 套件和 case ; --gtest_output=xml[:DIRECTORY_PATH/|:FILE_PATH]
生成報告等。
Fixture
多個用例需要使用相同的資料,每次都在用例中準備顯得很重複麻煩,這時候,可以使用 Fixture 來構建用例,使多個用例共用相同的資料物件配置。
使用 Fiture 第一部是定義一個繼承自::testing::Test
的類,在類中定義初始化函式,清理函式和宣告需要使用的物件。
class QueueTest : public ::testing::Test { // 定義套件名,繼承自 Test
protected: // 建議,子類可用成員
//定義setup 函式,在每個用例執行前呼叫
void SetUp() override {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
// 定義清理函式,在每個用例執行後呼叫
// void TearDown() override {}
// 定義需要用到的變數
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
//寫用例,套件名(上面定義的類名),用例名
TEST_F(QueueTest, IsEmptyInitially) {
EXPECT_EQ(q0_.size(), 0); //直接使用成員變數
}
以上我們定義了一個套件 QueueTest , 當我們執行該套件用例時,
- gtest 構建 QueueTest 例項 qt1;
- 呼叫 qt1.SetUp() 初始化
- 執行一個用例
- 呼叫 qt1.TearDown() 清理
- 析構 qt1 物件
- 回到1,執行下一個用例
從步驟可知,不同用例之間,資料實際都是獨佔的,不會相互影響。
使用 fixture 編寫用例後,同單獨測試用例 TEST 一樣,需要編寫 main ,然後編譯連線,執行測試。
使用 gmock
gmock 現在已經和入 gtest 的程式碼庫, 1.8 和之後的版本直接在 gtest github 主頁中獲取,低版本仍然在原 github主頁。
gmock 需要依賴 gtest 使用,在測試中,當我們測試的物件需要依賴其他模組、介面,但是往往受條件限制無法使用真實依賴的物件,通過 mock 物件來模擬我們需要依賴,以協助測試本模組,mock 物件具有和真實物件一樣的介面,但是我們可以在執行時指定他的行為,如何被使用,使用多少次、引數,使用時返回什麼等。
編譯
編譯說明
gmock 編譯需要依賴 gtest, 準備好 gtest 和 gmock (同一個版本)後,手動編譯的方法如下:
設定好 gtest 和 gmock 的工程路徑,或者在下面命令中直接替換源路徑。
g++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \
-isystem ${GMOCK_DIR}/include -I${GMOCK_DIR} \
-pthread -c ${GTEST_DIR}/src/gtest-all.cc
g++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \
-isystem ${GMOCK_DIR}/include -I${GMOCK_DIR} \
-pthread -c ${GMOCK_DIR}/src/gmock-all.cc
ar -rv libgmock.a gtest-all.o gmock-all.o
由命令可知,libgmock.a 包含了 libgtest.a,所有實際編譯測試程式時,只需要連結 libglmock.a 就好了。
使用 cmake編譯庫,進入 gmock 目錄(此處 gtest 已經準備並且與 gmock 同級目錄)
$ cd ./googlemock/; mkdir build
$ cd ./build; cmake ..
$ make
生成 libgmock.a 庫在 build 目錄下, 同時生成 libgtest.a gtest/
下, 與上面手動編譯把 gtest 和 gmock 打在一個 libgmock.a 不同,使用這種編譯程式需要同時指定 連結 libgmock.a 和 libgtest.a, 否則會報各種 undefine 的錯誤 。
編譯測試程式 :
g++ -isystem ${GTEST_DIR}/include \
-isystem ${GMOCK_DIR}/include \
-pthread path/to/your_test.cc libgmock.a -o your_test
測試時,我連結 cmake 編譯出來的庫時報錯,檢視庫中很多符號沒有,原因就是 cmake 輸出的 libmock.a 不包含 gtest,需要指定連結 libgtest.a
gmock 測試程式
gmock mock 物件,可以定義函式期望行為,如被呼叫時返回的值,期望被呼叫的次數,引數等,如果不滿足就會報錯。
定義 gmock 物件的基本步驟:
- 建立 mock 物件繼承自原物件,並用框架提供的巨集
MOCK_METHODn(); (or MOCK_CONST_METHODn();
描述需要模擬的介面 - 寫用例,在用例中使用巨集定義期望介面的行為,如果定義的行為執行用例時不滿足,就會報錯
借用主頁提供的例子改寫,簡單學習下如何使用 mock
比如你測試的物件依賴的介面定義如下,
class Turtle {
public:
virtual ~Turtle() {}
virtual void PenUp() = 0;
virtual void PenDown() = 0;
virtual void Forward(int distance) = 0;
virtual void Turn(int degrees) = 0;
virtual void GoTo(int x, int y) = 0;
virtual int GetX() const = 0;
virtual int GetY() const = 0;
};
此時通過繼承這個物件,定義了 mock 物件,在物件中通過巨集描述需要 mock 的介面,這樣,就完成了物件的 mock 操作。
#include "gmock/gmock.h"
#include "gtest/gtest.h
class MockTurtle: public Turtle {
public:
// MOCK_METHOD[引數個數](介面名,介面定義格式);
MOCK_METHOD0(PenUp, void());
MOCK_METHOD0(PenDown, void());
MOCK_METHOD1(Forward, void(int distance));
MOCK_METHOD1(Turn, void(int degrees));
MOCK_METHOD2(GoTo, void(int x, int y));
MOCK_CONST_METHOD0(GetX, int());
MOCK_CONST_METHOD0(GetY, int());
};
定義了 mock 物件後,就可以在測試用例使用 mock 物件替代原依賴物件,執行測試了。
using ::testing::AtLeast;
TEST(PainterTest, PenDownCall) {
MockTurtle turtle;
EXPECT_CALL(turtle, PenDown())
┊ .Times(AtLeast(2));
// 期望這個函式在本次測試需要至少被呼叫2次
// 否則報錯
turtle.PenDown();
turtle.PenDown();
}
using ::testing::Return;
TEST(PainterTest, GetX) {
MockTurtle turtle;
EXPECT_CALL(turtle, GetX())
┊ .Times(4)
┊ .WillOnce(Return(100))
┊ .WillOnce(Return(150))
┊ .WillRepeatedly(Return(200));
// 期望這個函式在本次測試需要被呼叫4次
// 否則報錯
// 第一次呼叫返回100, 第二次150,之後都是200
EXPECT_EQ(turtle.GetX(), 100);
EXPECT_EQ(turtle.GetX(), 150);
EXPECT_EQ(turtle.GetX(), 200);
EXPECT_EQ(turtle.GetX(), 200);
}
using ::testing::_;
TEST(PainterTest, GoTo) {
MockTurtle turtle;
EXPECT_CALL(turtle, GoTo(_, 100));
// 期望呼叫引數,第一個任意,第一個必須為 100
turtle.GoTo(1, 100);
EXPECT_CALL(turtle, GoTo(_, 101));
turtle.GoTo(2, 101);
}
gmock 使用巨集設定期望是粘性的,意思是當我們呼叫達到期望後,這些設定的期望仍然保持活性。
舉個例子,mock 一個介面 a(int),我們設定第一個期望: a 呼叫傳入引數任意,呼叫次數任意;然後設定第二個期望: a 呼叫傳入引數必須為1, 呼叫次數為2;當我們呼叫 a(1) 兩次後,達到了第二個期望上邊界(此時第二個期望並不會失效),這時候,第三次呼叫 a(1) 就會報錯,因為匹配到第二個期望說呼叫超過2次。(總是匹配最後一個期望)
如果想設定多個期望,並按順序執行,可以如下實現
//sticky
TEST(PainterTest, GetY) {
//設定呼叫按照期望設定順序,定義一個 sq 物件,名隨意
using ::testing::InSequence;
InSequence dummyObj;
MockTurtle turtle;
EXPECT_CALL(turtle, GetY())
┊ .Times(2)
┊ .WillOnce(Return(100))
┊ .WillOnce(Return(150))
┊ .RetiresOnSaturation(); // 指定匹配後不再生效,退休
EXPECT_CALL(turtle, GetY())
┊ .Times(1)
┊ .WillOnce(Return(200))
┊ .RetiresOnSaturation();
EXPECT_EQ(turtle.GetY(), 100);
EXPECT_EQ(turtle.GetY(), 150);
EXPECT_EQ(turtle.GetY(), 200);
}
最後,和 gtest 中一樣,可以自己編寫 main 函式完成呼叫,不過注意到,呼叫的 init 函式不同,之後便可以按前面提到的編譯命令執行編譯,執行測試了。
int main(int argc, char** argv) {
//初始化 gtest 和 gmock
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}
參考
我的部落格即將搬運同步至騰訊雲+社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=38q7yly61twk8
相關文章
- 一文掌握開源單元測試框架Google Test框架Go
- 單元測試框架 mockito框架Mockito
- JavaScript單元測試框架JavaScript框架
- 前端單元測試框架梳理前端框架
- 單元測試利器Mockito框架Mockito框架
- jest對react單元測試框架React框架
- Netty 框架學習 —— 單元測試Netty框架
- 單元測試模擬框架Mockito框架Mockito
- PHP單元測試框架PHPUnit的使用PHP框架
- 測試 之Java單元測試、Android單元測試JavaAndroid
- Python單元測試框架pytest常用測試報告型別Python框架測試報告型別
- 搞定Go單元測試(二)—— mock框架(gomock)GoMock框架
- 單元測試:單元測試中的mockMock
- Python中的單元測試框架:使用unittest進行有效測試Python框架
- unittest 單元測試框架教程 1-執行測試指令碼框架指令碼
- Pytest單元測試框架生成HTML測試報告及優化框架HTML測試報告優化
- GoLang快速上手單元測試(思想、框架、實踐)Golang框架
- 單元測試,只是測試嗎?
- SAP CDS view單元測試框架Test Double介紹View框架
- 搞定Go單元測試(四)—— 依賴注入框架(wire)Go依賴注入框架
- Angular單元測試框架裡API toHaveBeenCalledTimes的工作原理Angular框架API
- javascript單元測試框架mocha 和 斷言庫 assertJavaScript框架
- 單元測試-【轉】論單元測試的重要性
- golang單元測試Golang
- 單元測試真
- iOS 單元測試iOS
- python 單元測試Python
- 前端單元測試前端
- Flutter 單元測試Flutter
- 單元測試 Convey
- 單元測試工具
- 聊聊單元測試
- 十五、單元測試
- Go單元測試Go
- SpringBoot單元測試Spring Boot
- 前端測試:Part II (單元測試)前端
- 一文讓你快速上手 Mockito 單元測試框架Mockito框架
- Pytest單元測試框架——Pytest+Allure+Jenkins的應用框架Jenkins