Google 單元測試框架

weixin_34007291發表於2018-09-04

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 , 當我們執行該套件用例時,

  1. gtest 構建 QueueTest 例項 qt1;
  2. 呼叫 qt1.SetUp() 初始化
  3. 執行一個用例
  4. 呼叫 qt1.TearDown() 清理
  5. 析構 qt1 物件
  6. 回到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.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 程式設計指導codebook

gmock mock 物件,可以定義函式期望行為,如被呼叫時返回的值,期望被呼叫的次數,引數等,如果不滿足就會報錯。
定義 gmock 物件的基本步驟:

  1. 建立 mock 物件繼承自原物件,並用框架提供的巨集 MOCK_METHODn(); (or MOCK_CONST_METHODn(); 描述需要模擬的介面
  2. 寫用例,在用例中使用巨集定義期望介面的行為,如果定義的行為執行用例時不滿足,就會報錯

借用主頁提供的例子改寫,簡單學習下如何使用 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

相關文章