高效易用的C++單元測試框架:輕鬆構建高質量程式碼

後端研發工程實踐發表於2023-04-25

1. 概述

單元測試是構建穩定、高質量的程式、服務或系統的必不可少的一環。透過單元測試,我們可以在開發過程中及時發現和修復程式碼中的問題,提高程式碼的質量和可維護性。同時,單元測試也可以幫助我們更好地理解程式碼的功能和實現細節,從而更好地進行程式碼重構和最佳化。

然而,很多C++單元測試框架都是“重量級”的,使用起來比較複雜,而且很多情況下我們並不需要那麼多複雜的功能。因此,開發一個輕量級的C++單元測試框架,可以減少程式碼中不必要的依賴,提高程式碼的可維護性和可測試性,同時也可以加快編譯和執行速度。

輕量級的C++單元測試框架,可以幫助我們更加方便地編寫和管理單元測試,提高程式碼的質量和可維護性。

2. 實現原理

在正式開始介紹實現原理之前,需要特別強調的是,在這個單元測試框架中,所有的程式碼都定義在UnitTest名稱空間中。這樣做的好處是可以避免與其他程式碼的命名衝突,同時也可以更好地組織和管理程式碼。

2.1 測試用例基類

我們抽象出一個測試用例基類,它的定義如下所示。

class TestCase {
 public:
  virtual void Run() = 0;
  virtual void TestCaseRun() { Run(); }
  bool Result() { return result_; }
  void SetResult(bool result) { result_ = result; }
  std::string CaseName() { return case_name_; }
  TestCase(std::string case_name) : case_name_(case_name) {}

 private:
  bool result_{true};
  std::string case_name_;
};

在上面的程式碼中我們定義了一個C++中的測試用例基類TestCase,它定義了一些虛擬函式和成員變數,用於派生出具體的測試用例類。

首先,它定義了一個純虛擬函式Run(),用於執行測試用例的具體邏輯。這個函式需要在具體的測試用例類中實現。

其次,它定義了一個虛擬函式TestCaseRun(),它呼叫了Run()函式,並將執行結果儲存在result_成員變數中。這個函式可以在具體的測試用例類中重寫,以實現特定的測試邏輯。

接著,它定義了一個Result()函式,用於獲取測試結果。這個函式返回一個bool型別的值,表示測試是否透過。

然後,它定義了一個SetResult()函式,用於設定測試結果。這個函式接受一個bool型別的引數,表示測試是否透過。

最後,它定義了一個CaseName()函式,用於獲取測試用例的名稱。這個函式返回一個std::string型別的值,表示測試用例的名稱。

在這個類的建構函式中,它接受一個std::string型別的引數case_name,用於設定測試用例的名稱。這個引數會被儲存在case_name_成員變數中。

2.2 單元測試核心類

我們實現了單元測試核心類,它的定義如下所示。

class UnitTestCore {
 public:
  static UnitTestCore *GetInstance() {
    static UnitTestCore instance;
    return &instance;
  }

  int Run(int argc, char *argv[]) {
    result_ = true;
    failure_count_ = 0;
    success_count_ = 0;
    std::cout << kGreenBegin << "[==============================] Running " << test_cases_.size() << " test case."
              << kColorEnd << std::endl;
    constexpr int kFilterArgc = 2;
    for (int i = 0; i < test_cases_.size(); i++) {
      if (argc == kFilterArgc) {
        // 第二引數時,做用例CaseName來做過濾
        if (not std::regex_search(test_cases_[i]->CaseName(), std::regex(argv[1]))) {
          continue;
        }
      }
      std::cout << kGreenBegin << "Run TestCase:" << test_cases_[i]->CaseName() << kColorEnd << std::endl;
      test_cases_[i]->TestCaseRun();
      std::cout << kGreenBegin << "End TestCase:" << test_cases_[i]->CaseName() << kColorEnd << std::endl;
      if (test_cases_[i]->Result()) {
        success_count_++;
      } else {
        failure_count_++;
        result_ = false;
      }
    }
    std::cout << kGreenBegin << "[==============================] Total TestCase:" << test_cases_.size() << kColorEnd
              << std::endl;
    std::cout << kGreenBegin << "Passed:" << success_count_ << kColorEnd << std::endl;
    if (failure_count_ > 0) {
      std::cout << kRedBegin << "Failed:" << failure_count_ << kColorEnd << std::endl;
    }
    return 0;
  }

  TestCase *Register(TestCase *test_case) {
    test_cases_.push_back(test_case);
    return test_case;
  }

 private:
  bool result_{true};
  int32_t success_count_{0};
  int32_t failure_count_{0};
  std::vector<TestCase *> test_cases_;  // 測試用例集合
};

在上面的程式碼中我們定義了一個C++中的單元測試框架核心類UnitTestCore,它提供了註冊測試用例、執行測試用例等功能。

首先,它定義了一個靜態函式GetInstance(),用於獲取單例物件。這個函式使用了靜態區域性變數,保證了執行緒安全。

接著,它定義了一個Run()函式,用於執行所有註冊的測試用例。這個函式接受兩個引數,分別是命令列引數的數量和引數陣列。在函式內部,它會遍歷所有註冊的測試用例,並依次執行它們的TestCaseRun()函式。在執行完每個測試用例後,它會根據測試結果更新success_count_failure_count_成員變數,並輸出測試結果。如果有測試用例執行失敗,它會將result_成員變數設定為false。

然後,它定義了一個Register()函式,用於註冊測試用例。這個函式接受一個TestCase型別的指標引數,表示要註冊的測試用例。在函式內部,它會將測試用例指標儲存在test_cases_成員變數中,並返回測試用例指標。

最後,它定義了一些私有成員變數,包括result_success_count_failure_count_test_cases_。這些成員變數用於儲存測試結果和測試用例集合。

UnitTestCore類提供了註冊測試用例、執行測試用例等基本功能,可以幫助我們更加方便地編寫和管理單元測試。

2.3 單測宏定義

我們的單元測試框架預定義了一系列的宏,用於快速構建單元測試。這些宏的內容如下。

#define TEST_CASE_CLASS(test_case_name)                                                     \
  class test_case_name : public UnitTest::TestCase {                                        \
   public:                                                                                  \
    test_case_name(std::string case_name) : UnitTest::TestCase(case_name) {}                \
    virtual void Run();                                                                     \
                                                                                            \
   private:                                                                                 \
    static UnitTest::TestCase *const test_case_;                                            \
  };                                                                                        \
  UnitTest::TestCase *const test_case_name::test_case_ =                                    \
      UnitTest::UnitTestCore::GetInstance()->Register(new test_case_name(#test_case_name)); \
  void test_case_name::Run()

#define TEST_CASE(test_case_name) TEST_CASE_CLASS(test_case_name)

#define ASSERT_EQ(left, right)                                                                                  \
  if ((left) != (right)) {                                                                                      \
    std::cout << UnitTest::kRedBegin << "assert_eq failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \
              << "!=" << (right) << UnitTest::kColorEnd << std::endl;                                           \
    SetResult(false);                                                                                           \
    return;                                                                                                     \
  }

#define ASSERT_NE(left, right)                                                                                  \
  if ((left) == (right)) {                                                                                      \
    std::cout << UnitTest::kRedBegin << "assert_ne failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \
              << "==" << (right) << UnitTest::kColorEnd << std::endl;                                           \
    SetResult(false);                                                                                           \
    return;                                                                                                     \
  }

#define ASSERT_LT(left, right)                                                                                  \
  if ((left) >= (right)) {                                                                                      \
    std::cout << UnitTest::kRedBegin << "assert_lt failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \
              << ">=" << (right) << UnitTest::kColorEnd << std::endl;                                           \
    SetResult(false);                                                                                           \
    return;                                                                                                     \
  }

#define ASSERT_LE(left, right)                                                                                         \
  if ((left) > (right)) {                                                                                              \
    std::cout << UnitTest::kRedBegin << "assert_le failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) << ">" \
              << (right) << UnitTest::kColorEnd << std::endl;                                                          \
    SetResult(false);                                                                                                  \
    return;                                                                                                            \
  }

#define ASSERT_GT(left, right)                                                                                  \
  if ((left) <= (right)) {                                                                                      \
    std::cout << UnitTest::kRedBegin << "assert_gt failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \
              << "<=" << (right) << UnitTest::kColorEnd << std::endl;                                           \
    SetResult(false);                                                                                           \
    return;                                                                                                     \
  }

#define ASSERT_GE(left, right)                                                                                         \
  if ((left) < (right)) {                                                                                              \
    std::cout << UnitTest::kRedBegin << "assert_ge failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) << "<" \
              << (right) << UnitTest::kColorEnd << std::endl;                                                          \
    SetResult(false);                                                                                                  \
    return;                                                                                                            \
  }

#define ASSERT_TRUE(expr)                                                                                         \
  if (not(expr)) {                                                                                                \
    std::cout << UnitTest::kRedBegin << "assert_true failed at " << __FILE__ << ":" << __LINE__ << ". " << (expr) \
              << " is false" << UnitTest::kColorEnd << std::endl;                                                 \
    SetResult(false);                                                                                             \
    return;                                                                                                       \
  }

#define ASSERT_FALSE(expr)                                                                                         \
  if ((expr)) {                                                                                                    \
    std::cout << UnitTest::kRedBegin << "assert_false failed at " << __FILE__ << ":" << __LINE__ << ". " << (expr) \
              << " if true" << right << UnitTest::kColorEnd << std::endl;                                          \
    SetResult(false);                                                                                              \
    return;                                                                                                        \
  }

#define RUN_ALL_TESTS() \
  int main(int argc, char *argv[]) { return UnitTest::UnitTestCore::GetInstance()->Run(argc, argv); }

2.3.1 TEST_CASE_CLASS

這個宏用於定義測試用例類。它接受一個引數test_case_name,表示測試用例類的名稱。這個宏它定義了一個繼承自UnitTest::TestCase的測試用例類,並實現了Run()函式。同時,它還定義了一個靜態成員變數test_case_,用於註冊測試用例。在宏定義的最後,它使用UnitTest::UnitTestCore::GetInstance()->Register()函式將測試用例註冊到測試框架中。

2.3.2 TEST_CASE

這個宏用於定義測試用例。這個宏接受一個引數test_case_name,表示測試用例的名稱。在宏定義中,它使用TEST_CASE_CLASS宏定義測試用例類,並將測試用例類的名稱作為引數傳遞給TEST_CASE_CLASS宏。

2.3.3 ASSERT_XXX

ASSERT_XXX是一系列的宏,用於在每個單獨的測試用例中校驗執行結果是否符合預期。如果執行結果不符合預期,宏會中斷當前用例的執行,並標記測試用例執行失敗。

2.3.4 RUN_ALL_TESTS

這個宏用於執行所有註冊的測試用例。這個宏定義了一個main()函式,並呼叫UnitTest::UnitTestCore::GetInstance()->Run()函式來執行所有的測試用例。

3. demo示例

這個簡單的單元測試框架程式碼,我們儲存在github上,地址為:https://github.com/wanmuc/UnitTest,歡迎大家fork和star。在倉庫中有完整的示例程式碼檔案demo_test.cpp。

相關文章