1. 概述
单元测试是构建稳定、高质量的程序、服务或系统的必不可少的一环。通过单元测试,我们可以在开发过程中及时发现和修复代码中的问题,提高代码的质量和可维护性。同时,单元测试也可以帮助我们更好地理解代码的功能和实现细节,从而更好地进行代码重构和优化。
然而,很多C++单元测试框架都是“重量级”的,使用起来比较复杂,而且很多情况下我们并不需要那么多复杂的功能。因此,开发一个轻量级的C++单元测试框架,可以减少代码中不必要的依赖,提高代码的可维护性和可测试性,同时也可以加快编译和运行速度。
轻量级的C++单元测试框架,可以帮助我们更加方便地编写和管理单元测试,提高代码的质量和可维护性。
2. 实现原理
在正式开始介绍实现原理之前,需要特别强调的是,在这个单元测试框架中,所有的代码都定义在UnitTest命名空间中。这样做的好处是可以避免与其他代码的命名冲突,同时也可以更好地组织和管理代码。
2.1 测试用例基类
我们抽象出一个测试用例基类,它的定义如下所示。
1class TestCase { 2 public: 3 virtual void Run() = 0; 4 virtual void TestCaseRun() { Run(); } 5 bool Result() { return result_; } 6 void SetResult(bool result) { result_ = result; } 7 std::string CaseName() { return case_name_; } 8 TestCase(std::string case_name) : case_name_(case_name) {} 9 10 private: 11 bool result_{true}; 12 std::string case_name_; 13};
在上面的代码中我们定义了一个C++中的测试用例基类TestCase,它定义了一些虚函数和成员变量,用于派生出具体的测试用例类。
首先,它定义了一个纯虚函数Run(),用于执行测试用例的具体逻辑。这个函数需要在具体的测试用例类中实现。
其次,它定义了一个虚函数TestCaseRun(),它调用了Run()函数,并将执行结果保存在result_成员变量中。这个函数可以在具体的测试用例类中重写,以实现特定的测试逻辑。
接着,它定义了一个Result()函数,用于获取测试结果。这个函数返回一个bool类型的值,表示测试是否通过。
然后,它定义了一个SetResult()函数,用于设置测试结果。这个函数接受一个bool类型的参数,表示测试是否通过。
最后,它定义了一个CaseName()函数,用于获取测试用例的名称。这个函数返回一个std::string类型的值,表示测试用例的名称。
在这个类的构造函数中,它接受一个std::string类型的参数case_name,用于设置测试用例的名称。这个参数会被保存在case_name_成员变量中。
2.2 单元测试核心类
我们实现了单元测试核心类,它的定义如下所示。
1class UnitTestCore { 2 public: 3 static UnitTestCore *GetInstance() { 4 static UnitTestCore instance; 5 return &instance; 6 } 7 8 int Run(int argc, char *argv[]) { 9 result_ = true; 10 failure_count_ = 0; 11 success_count_ = 0; 12 std::cout << kGreenBegin << "[==============================] Running " << test_cases_.size() << " test case." 13 << kColorEnd << std::endl; 14 constexpr int kFilterArgc = 2; 15 for (int i = 0; i < test_cases_.size(); i++) { 16 if (argc == kFilterArgc) { 17 // 第二参数时,做用例CaseName来做过滤 18 if (not std::regex_search(test_cases_[i]->CaseName(), std::regex(argv[1]))) { 19 continue; 20 } 21 } 22 std::cout << kGreenBegin << "Run TestCase:" << test_cases_[i]->CaseName() << kColorEnd << std::endl; 23 test_cases_[i]->TestCaseRun(); 24 std::cout << kGreenBegin << "End TestCase:" << test_cases_[i]->CaseName() << kColorEnd << std::endl; 25 if (test_cases_[i]->Result()) { 26 success_count_++; 27 } else { 28 failure_count_++; 29 result_ = false; 30 } 31 } 32 std::cout << kGreenBegin << "[==============================] Total TestCase:" << test_cases_.size() << kColorEnd 33 << std::endl; 34 std::cout << kGreenBegin << "Passed:" << success_count_ << kColorEnd << std::endl; 35 if (failure_count_ > 0) { 36 std::cout << kRedBegin << "Failed:" << failure_count_ << kColorEnd << std::endl; 37 } 38 return 0; 39 } 40 41 TestCase *Register(TestCase *test_case) { 42 test_cases_.push_back(test_case); 43 return test_case; 44 } 45 46 private: 47 bool result_{true}; 48 int32_t success_count_{0}; 49 int32_t failure_count_{0}; 50 std::vector<TestCase *> test_cases_; // 测试用例集合 51};
在上面的代码中我们定义了一个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 单测宏定义
我们的单元测试框架预定义了一系列的宏,用于快速构建单元测试。这些宏的内容如下。
1#define TEST_CASE_CLASS(test_case_name) \ 2 class test_case_name : public UnitTest::TestCase { \ 3 public: \ 4 test_case_name(std::string case_name) : UnitTest::TestCase(case_name) {} \ 5 virtual void Run(); \ 6 \ 7 private: \ 8 static UnitTest::TestCase *const test_case_; \ 9 }; \ 10 UnitTest::TestCase *const test_case_name::test_case_ = \ 11 UnitTest::UnitTestCore::GetInstance()->Register(new test_case_name(#test_case_name)); \ 12 void test_case_name::Run() 13 14#define TEST_CASE(test_case_name) TEST_CASE_CLASS(test_case_name) 15 16#define ASSERT_EQ(left, right) \ 17 if ((left) != (right)) { \ 18 std::cout << UnitTest::kRedBegin << "assert_eq failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \ 19 << "!=" << (right) << UnitTest::kColorEnd << std::endl; \ 20 SetResult(false); \ 21 return; \ 22 } 23 24#define ASSERT_NE(left, right) \ 25 if ((left) == (right)) { \ 26 std::cout << UnitTest::kRedBegin << "assert_ne failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \ 27 << "==" << (right) << UnitTest::kColorEnd << std::endl; \ 28 SetResult(false); \ 29 return; \ 30 } 31 32#define ASSERT_LT(left, right) \ 33 if ((left) >= (right)) { \ 34 std::cout << UnitTest::kRedBegin << "assert_lt failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \ 35 << ">=" << (right) << UnitTest::kColorEnd << std::endl; \ 36 SetResult(false); \ 37 return; \ 38 } 39 40#define ASSERT_LE(left, right) \ 41 if ((left) > (right)) { \ 42 std::cout << UnitTest::kRedBegin << "assert_le failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) << ">" \ 43 << (right) << UnitTest::kColorEnd << std::endl; \ 44 SetResult(false); \ 45 return; \ 46 } 47 48#define ASSERT_GT(left, right) \ 49 if ((left) <= (right)) { \ 50 std::cout << UnitTest::kRedBegin << "assert_gt failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \ 51 << "<=" << (right) << UnitTest::kColorEnd << std::endl; \ 52 SetResult(false); \ 53 return; \ 54 } 55 56#define ASSERT_GE(left, right) \ 57 if ((left) < (right)) { \ 58 std::cout << UnitTest::kRedBegin << "assert_ge failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) << "<" \ 59 << (right) << UnitTest::kColorEnd << std::endl; \ 60 SetResult(false); \ 61 return; \ 62 } 63 64#define ASSERT_TRUE(expr) \ 65 if (not(expr)) { \ 66 std::cout << UnitTest::kRedBegin << "assert_true failed at " << __FILE__ << ":" << __LINE__ << ". " << (expr) \ 67 << " is false" << UnitTest::kColorEnd << std::endl; \ 68 SetResult(false); \ 69 return; \ 70 } 71 72#define ASSERT_FALSE(expr) \ 73 if ((expr)) { \ 74 std::cout << UnitTest::kRedBegin << "assert_false failed at " << __FILE__ << ":" << __LINE__ << ". " << (expr) \ 75 << " if true" << right << UnitTest::kColorEnd << std::endl; \ 76 SetResult(false); \ 77 return; \ 78 } 79 80#define RUN_ALL_TESTS() \ 81 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。
