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 已經(jīng)提供了 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

跟多詳細(xì)內(nèi)容參考 readme 和代碼中提供的例子(samples ; make 目錄下),比如如何解決重復(fù)定義宏等問題。

gtest 測試程序

通過 編程參考源碼中 sample 目錄下的示例,我們可以很快上手 gtest。gtest 定義了宏供我們寫斷言語句,一個或者多個斷言組成我們的測試用例 case,多個測試用例有時候需要共享一些通用對象,可以把這些用例放在同一個 fixture 中。

斷言和 case

gtest 斷言提供兩個版本

  • ASSERT_* 版本斷言,在同一個 case 中(測試函數(shù))中,ASSERT_* 失敗就會終止當(dāng)前用例,開始其他 case ;
  • EXPECT_*版本,當(dāng)斷言失敗時,會報錯,但是會繼續(xù)執(zhí)行剩余語句。

完整的 宏定義, 或見源碼 include/gtest/gtest.h

使用哪種語句斷言取決自己用例場景,如當(dāng)前語句失敗時后續(xù)語句沒有繼續(xù)執(zhí)行意義,則可以直接使用 ASSERT 終止,否則使用 EXPECT 可以發(fā)現(xiàn)更多錯誤。

如果用例之間不需要什么公用資源,相互獨立,可以使用如下方式定義每一個 case

TEST(套件名,用例名)
{
    //套件名和用例名自定義
    //斷言語句
    //如一般的c++ 函數(shù),不 return value 
}

進(jìn)入目錄 sample 中, 以 sample1_unittest.cc 為例子

#include "sample1.h"  // 測試對象頭文件,接口
#include "gtest/gtest.h"  // gtest 頭文件

TEST(IsPrimeTest, Negative) {
    EXPECT_FALSE(IsPrime(-1)) << "這樣子失敗時打印自己的信息"; 
    EXPECT_FALSE(IsPrime(-2)); // 如果此斷言失敗,還會繼續(xù)執(zhí)行下一個
    EXPECT_FALSE(IsPrime(INT_MIN));
  }

TEST(IsPrimeTest, Negative) {
    EXPECT_FALSE(IsPrime(-1));
    ASSERT_FALSE(IsPrime(-2)); // 如果此斷言失敗,下一條不執(zhí)行,這個case 結(jié)束
    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 函數(shù),執(zhí)行所用測試用例,否者,也可以自己定義 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();
}

編譯后執(zhí)行輸出 bin 直接運行便運行所有用例,可以使用 -h 查看可選的執(zhí)行參數(shù),如--gtest_filter=IsPrimeTest.Negative 指定執(zhí)行 套件和 case ; --gtest_output=xml[:DIRECTORY_PATH/|:FILE_PATH]生成報告等。

Fixture

多個用例需要使用相同的數(shù)據(jù),每次都在用例中準(zhǔn)備顯得很重復(fù)麻煩,這時候,可以使用 Fixture 來構(gòu)建用例,使多個用例共用相同的數(shù)據(jù)對象配置。
使用 Fiture 第一部是定義一個繼承自::testing::Test 的類,在類中定義初始化函數(shù),清理函數(shù)和聲明需要使用的對象。

class QueueTest : public ::testing::Test { // 定義套件名,繼承自 Test
 protected:   // 建議,子類可用成員
  //定義setup 函數(shù),在每個用例執(zhí)行前調(diào)用
  void SetUp() override {
     q1_.Enqueue(1);
     q2_.Enqueue(2);
     q2_.Enqueue(3);
  }
  // 定義清理函數(shù),在每個用例執(zhí)行后調(diào)用
  // void TearDown() override {}
  // 定義需要用到的變量
  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};

//寫用例,套件名(上面定義的類名),用例名
TEST_F(QueueTest, IsEmptyInitially) {
  EXPECT_EQ(q0_.size(), 0); //直接使用成員變量
}

以上我們定義了一個套件 QueueTest , 當(dāng)我們執(zhí)行該套件用例時,

  1. gtest 構(gòu)建 QueueTest 實例 qt1;
  2. 調(diào)用 qt1.SetUp() 初始化
  3. 執(zhí)行一個用例
  4. 調(diào)用 qt1.TearDown() 清理
  5. 析構(gòu) qt1 對象
  6. 回到1,執(zhí)行下一個用例

從步驟可知,不同用例之間,數(shù)據(jù)實際都是獨占的,不會相互影響。

使用 fixture 編寫用例后,同單獨測試用例 TEST 一樣,需要編寫 main ,然后編譯連接,執(zhí)行測試。

使用 gmock

gmock 現(xiàn)在已經(jīng)和入 gtest 的代碼庫, 1.8 和之后的版本直接在 gtest github 主頁中獲取,低版本仍然在原 github主頁。

gmock 需要依賴 gtest 使用,在測試中,當(dāng)我們測試的對象需要依賴其他模塊、接口,但是往往受條件限制無法使用真實依賴的對象,通過 mock 對象來模擬我們需要依賴,以協(xié)助測試本模塊,mock 對象具有和真實對象一樣的接口,但是我們可以在運行時指定他的行為,如何被使用,使用多少次、參數(shù),使用時返回什么等。

編譯

編譯說明
gmock 編譯需要依賴 gtest, 準(zhǔn)備好 gtest 和 gmock (同一個版本)后,手動編譯的方法如下:
設(shè)置好 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編譯庫,進(jìn)入 gmock 目錄(此處 gtest 已經(jīng)準(zhǔn)備并且與 gmock 同級目錄)

$ cd ./googlemock/; mkdir build
$ cd ./build; cmake ..
$ make

生成 libgmock.a 庫在 build 目錄下, 同時生成 libgtest.a gtest/ 下, 與上面手動編譯把 gtest 和 gmock 打在一個 libgmock.a 不同,使用這種編譯程序需要同時指定 鏈接 libgmock.alibgtest.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 編程指導(dǎo)codebook

gmock mock 對象,可以定義函數(shù)期望行為,如被調(diào)用時返回的值,期望被調(diào)用的次數(shù),參數(shù)等,如果不滿足就會報錯。
定義 gmock 對象的基本步驟:

  1. 創(chuàng)建 mock 對象繼承自原對象,并用框架提供的宏 MOCK_METHODn(); (or MOCK_CONST_METHODn(); 描述需要模擬的接口
  2. 寫用例,在用例中使用宏定義期望接口的行為,如果定義的行為執(zhí)行用例時不滿足,就會報錯

借用主頁提供的例子改寫,簡單學(xué)習(xí)下如何使用 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[參數(shù)個數(shù)](接口名,接口定義格式);
      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 對象替代原依賴對象,執(zhí)行測試了。

  using ::testing::AtLeast;
  TEST(PainterTest, PenDownCall) {
      MockTurtle turtle;
      EXPECT_CALL(turtle, PenDown())
      ┊   .Times(AtLeast(2));
      // 期望這個函數(shù)在本次測試需要至少被調(diào)用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));
      // 期望這個函數(shù)在本次測試需要被調(diào)用4次
      // 否則報錯
      // 第一次調(diào)用返回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));
      // 期望調(diào)用參數(shù),第一個任意,第一個必須為 100
      turtle.GoTo(1, 100);
  
      EXPECT_CALL(turtle, GoTo(_, 101));
      turtle.GoTo(2, 101);
  }

gmock 使用宏設(shè)置期望是粘性的,意思是當(dāng)我們調(diào)用達(dá)到期望后,這些設(shè)置的期望仍然保持活性。
舉個例子,mock 一個接口 a(int),我們設(shè)置第一個期望: a 調(diào)用傳入?yún)?shù)任意,調(diào)用次數(shù)任意;然后設(shè)置第二個期望: a 調(diào)用傳入?yún)?shù)必須為1, 調(diào)用次數(shù)為2;當(dāng)我們調(diào)用 a(1) 兩次后,達(dá)到了第二個期望上邊界(此時第二個期望并不會失效),這時候,第三次調(diào)用 a(1) 就會報錯,因為匹配到第二個期望說調(diào)用超過2次。(總是匹配最后一個期望
如果想設(shè)置多個期望,并按順序執(zhí)行,可以如下實現(xiàn)

 //sticky
  TEST(PainterTest, GetY) {
      //設(shè)置調(diào)用按照期望設(shè)置順序,定義一個 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 函數(shù)完成調(diào)用,不過注意到,調(diào)用的 init 函數(shù)不同,之后便可以按前面提到的編譯命令執(zhí)行編譯,運行測試了。

int main(int argc, char** argv) {
      //初始化 gtest 和 gmock
      ::testing::InitGoogleMock(&argc, argv);
      return RUN_ALL_TESTS();
  }       

參考

我的博客即將搬運同步至騰訊云+社區(qū),邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=38q7yly61twk8

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,554評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,036評論 25 709
  • 用了大約20分鐘,完成了課后21天加強(qiáng)訓(xùn)練的第一張思維導(dǎo)圖,規(guī)劃了一周的主要行程,也不知道這樣用的對不對,忐忑一下!
    李大鵬_365閱讀 285評論 0 0
  • 日常腦子里有很多念頭,都是一閃而過,一些好的點,就隨手記在了手機(jī)上。以下是我最近是一段時間,有關(guān)于廣告行業(yè)的小思考...
    賈桃閱讀 857評論 0 2
  • 最完美的婚姻一定是勢均力敵的。 ??? “我養(yǎng)你”,才是世界上最毒的情話 這兩天心情一直不算好,于是刷遍了網(wǎng)絡(luò)綜藝...
    歐陽茜茜閱讀 593評論 0 1

友情鏈接更多精彩內(nèi)容