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。