高效易用的C++单元测试框架:轻松构建高质量代码

万木春
• 阅读 414

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。

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
java单元测试
Java单元测试1.概述java单元测试是最小的功能单元测试代码,单元测试就是针对单个java方法的测试。java程序的最小功能单元是方法。main方法进行测试的缺点:只能有一个main()方法,不能把测试代码分离出来无法打印出测试结果和期望结果.例如:expected:
Wesley13 Wesley13
3年前
java单元测试工具
前言随着DevOp的不断流行,自动化测试慢慢成为Java开发者的关注点。因此,本文将分享10款优秀的单元测试框架和库,它们可以帮助Java开发人员在其Java项目上编写单元测试和集成测试。1\.JUnit我绝对JUnit不需要太多的介绍了。即使您是Java初学者,也可能听说过它。它可以帮助您为Java代码编写单元测试。几
雷厉风行 雷厉风行
1年前
Mac PHP开发神器:JetBrains PhpStorm 2023轻松搞定复杂开发,永久版下载
JetBrainsPhpStorm是一款面向PHP开发者的强大集成开发环境。该软件提供了丰富的功能和工具、支持多种语言,可以帮助PHP开发者更加高效地编写PHP代码。通过PhpStorm,开发者可以轻松构建和维护高质量的PHP代码,从而提供更出色的在线解决方案。
雷厉风行 雷厉风行
1年前
简单易用的Mac Go开发工具:JetBrains GoLand 2023,永久版下载
JetBrainsGoLand是一款面向Go语言开发者的集成开发环境,可以帮助开发者快速创建、维护和调试高质量、高效的Go语言项目。该软件提供了许多强大的工具和功能,包括代码自动补全、代码重构、静态分析、Debugging和运行测试等,从而提高了Go语言开发者的编码速度和工作效率。
云原生引擎单元测试实践
快速迭代的开发工作中如何提高代码质量一直是团队痛点,特别是没有测试支持的开发团队。合理的使用单元测试,并关注单元测试通过率、代码覆盖率可以有效提高代码质量。今天就来讲讲云原生引擎单元测试实践。
Stella981 Stella981
3年前
Mock工具之Mockito实战
在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。为了解决这类问题我们引入了Mock的概念,简单的说就是模拟这些需要构建的类或者资源,提供给需要测试的对象使用。业内的Mock工具有很多,也已经很成熟了,这里我们将直接使用最流行的Moc
Stella981 Stella981
3年前
Parasoft cpptestcli 指令行参数配置
ParasoftCtest  是款功能非常强大的自动化测试工具,能有效提高软件开发效率和代码质量。功能特点如下:    静态代码分析和编码策略实施;  自动代码审查的图形界面和进度跟踪;  自动化的单元测试和回归测试;  代码覆盖率分析,集成通过应用层覆盖跟踪从单元级测试;  结合静态分析、单元测试、代码评审和其他
子桓 子桓
1年前
好用的Java开发推荐!
好用的Java开发IntelliJIDEA2023中文,IntelliJIDEA提供了丰富的工具和功能,可以帮助开发人员提高开发效率和代码质量。它具有智能代码编辑器、代码检查、快速修复、多模块构建、重构、版本控制等功能。此外,它还支持自动化构建、测试和部署
Java单元测试及常用语句 | 京东物流技术团队
编写Java单元测试用例,即把一段复杂的代码拆解成一系列简单的单元测试用例,并且无需启动服务,在短时间内测试代码中的处理逻辑。写好Java单元测试用例,其实就是把“复杂问题简单化,建单问题深入化“。在编写的过程中,我们也可以对自己的代码进行一个二次检查。
京东云开发者 京东云开发者
6个月前
一种极简单的SpringBoot单元测试方法| 京东零售技术团队
前言本文主要提供了一种单元测试方法,力求0基础人员可以从本文中受到启发,可以搭建一套好用的单元测试环境,并能切实的提高交付代码的质量。极简体现在除了POM依赖和单元测试类之外,其他什么都不需要引入,只需要一个本地能启动的springboot项目。目录1.P
万木春
万木春
Lv1
道阻且长,行则将至;行而不辍,未来可期。微信公众号【Linux后端研发工程实践】。
文章
1
粉丝
1
获赞
1
热门文章

暂无数据