极简框架,助力单元测试落地实施

354 阅读8分钟

哈喽,大家好,我是春哥、欢迎来到本期分享。

1.问题

在测试金字塔理论中,单元测试被视为软件开发质量保证的基石,它对于构建高质量、稳定的系统至关重要。单元测试能显著提升代码质量,它有助于在软件开发过程中尽早发现问题,降低开发成本并提高整体效率。特别是在代码重构和优化的场景下,完备的单元测试会让你对重构过程更具信心。

FaAfrqmf-W7PIu5b10bb1GabntoXDDmAjnH5Io2XNzg.png

然而,许多单元测试框架往往是庞大且全面的重量级框架,使用起来相对复杂。实际工作场景中,我们并不需要如此多的功能。因此,构建一个「轻量级」「易于上手」的单元测试框架变得至关重要和迫切。

春哥针对这一需求,使用C++实现了轻量级的单元测试框架UnitTest,旨在抛砖引玉,希望能为大家带来实际帮助。

2.架构设计

这个UnitTest单元测试框架是如何设计的呢?让我们先来看一下框架中各个类之间的关系图。

QM2Jp1TPbX-XIA-ucgCnMGkIfllbzZ1s0YaF2Ev8PwQ.png

2.1 TestCase

TestCase为测试用例基类,可以在它的基础上派生出具体的测试用例类。

首先,它定义了一个纯虚函数Run(),用于执行测试用例的具体逻辑。这个函数需要在具体的测试用例类中实现。

其次,它定义了一个虚函数TestCaseRun(),它调用了Run()函数,并将执行结果保存在result_成员变量中。必要时可以在具体的测试用例类中重写,以实现特定的测试逻辑。

接着,它定义了一个Result()函数,用于获取测试结果。这个函数返回一个bool类型的值,表示测试是否通过。

然后,它定义了一个SetResult()函数,用于设置测试结果。这个函数接受一个bool类型的参数,表示测试是否通过。

最后,它定义了一个CaseName()函数,用于获取测试用例的名称。这个函数返回一个std::string类型的值,表示测试用例的名称。

在这个类的构造函数中,它接受一个std::string类型的参数case_name,用于设置测试用例的名称。这个参数会被保存在case_name_成员变量中。

2.2 TestCaseA、TestCaseB

TestCaseA和TestCaseB是具体的测试用例类,它们都继承自TestCase类,是单元测试用例的具体实现,也是框架中最小的单元测试单位。

2.3 UnitTestCore

UnitTestCore为单元测试框架的核心类,它提供了注册测试用例、运行测试用例的功能,是单元测试框架的「引擎」

首先,它定义了一个静态函数GetInstance(),用于获取单例对象。这个函数使用了静态局部变量,保证了线程安全。

接着,它定义了一个Run()函数,用于运行所有注册的测试用例。这个函数接受两个参数,分别是命令行参数的数量和参数数组。在函数内部,它会遍历所有注册的测试用例,并支持通过正则表达式匹配测试用例名称来过滤测试用例,然后依次执行它们的TestCaseRun()函数。在执行完每个测试用例后,它会根据测试结果更新success_count_和failure_count_成员变量,并输出测试结果。如果有测试用例执行失败,它会将result_成员变量设置为false。

然后,它定义了一个Register()函数,用于注册测试用例。这个函数接受一个TestCase类型的指针参数,表示要注册的测试用例。在函数内部,它会将测试用例指针保存在test_cases_成员变量中,并返回测试用例指针。

最后,它定义了一些私有成员变量,包括result_、success_count_、failure_count_和test_cases_。这些成员变量用于保存测试结果和测试用例集合。

UnitTestCore类提供了注册测试用例、运行测试用例的基本功能,可以帮助我们更加方便的管理和执行单元测试用例。

3.源码

talk is cheap, show me the code

看完类的关系图后,让我们来看一下具体的代码实现和如何使用该框架的demo示例。

3.1 核心代码

我们单元测试框架的核心代码文件unittestcore.hpp,内容如下所示。

#pragma once

#include <iostream>
#include <regex>
#include <string>
#include <vector>

namespace UnitTest {

static const char *kGreenBegin = "\033[32m";
static const char *kRedBegin = "\033[31m";
static const char *kColorEnd = "\033[0m";

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_;
};

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 (size_t 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_;  // 测试用例集合
};

#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" << UnitTest::kColorEnd << std::endl;                                          \
    SetResult(false);                                                                                              \
    return;                                                                                                        \
  }

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

通过上面的代码可以看出,我们的单元测试框架代码非常简单。除了定义了TestCase和UnitTestCore这两个类之外,还有一些辅助宏的定义。

3.2 辅助宏

我们的单元测试框架提供了一系列的辅助宏,可以帮助开发者快速搭建和执行测试用例。下面,我们将详细介绍这些宏的作用和用法。通过使用这些宏,开发者可以更加方便地编写测试用例和断言语句,从而提高测试代码的可读性和可维护性。

3.2.1 TEST_CASE_CLASS

这个宏用于定义测试用例类。它接受一个参数test_case_name,表示测试用例类的名称。这个宏它定义了一个继承自UnitTest::TestCase的测试用例类,并实现了Run()函数。同时,它还定义了一个静态成员变量test_case_,用于注册测试用例。在宏定义的最后,它使用UnitTest::UnitTestCore::GetInstance()->Register()函数将测试用例注册到测试框架中。

3.2.2 TEST_CASE

这个宏用于定义测试用例。这个宏接受一个参数test_case_name,表示测试用例的名称。在宏定义中,它使用TEST_CASE_CLASS宏定义测试用例类,并将测试用例类的名称作为参数传递给TEST_CASE_CLASS宏。

3.2.3 ASSERT_XXX

ASSERT_XXX是一系列的宏,用于在每个单独的测试用例中校验执行结果是否符合预期。如果执行结果不符合预期,宏会中断当前用例的执行,并标记测试用例执行失败。

3.2.4 RUN_ALL_TESTS

这个宏用于运行所有注册的测试用例。这个宏定义了一个main()函数,并调用UnitTest::UnitTestCore::GetInstance()->Run()函数来运行所有的测试用例。

3.3 demo示例

接下来,我们将展示单元测试框架的demo示例。在单元测试文件中,只需使用include预编译指令来引入unittestcore.hpp文件,然后使用辅助宏。如此简单,轻松上手,是不是so easy呀!!!

#include <iostream>

#include "unittestcore.hpp"

using namespace std;

int func(int value) { return value; }

TEST_CASE(assert_eq) {
  ASSERT_EQ(func(1), 1);
  ASSERT_EQ(1, 1);
  ASSERT_EQ(1 + 2, 1 + 1 + 1);
}

TEST_CASE(assert_true) {
  ASSERT_TRUE(true);
  ASSERT_TRUE(10 > 2);
  ASSERT_TRUE(func(1) == 1);
}

TEST_CASE(assert_false) {
  ASSERT_FALSE(false);
  ASSERT_FALSE(2 > 10);
  ASSERT_FALSE(func(1) != 1);
}

TEST_CASE(assert_ne) {
  ASSERT_NE(10, 11);
  ASSERT_NE(10 + 10, 11 + 11);
  ASSERT_NE(func(1), func(1) + func(9));
}

TEST_CASE(assert_gt) {
  ASSERT_GT(1, 0);
  ASSERT_GT(func(1), 0);
  ASSERT_GT(func(3), func(1) + func(1));
}

TEST_CASE(assert_ge) {
  ASSERT_GE(1, 1);
  ASSERT_GE(func(1), 1);
  ASSERT_GE(func(3), func(1) + func(1) + func(1));
}

TEST_CASE(assert_lt) {
  ASSERT_LT(0, 1);
  ASSERT_LT(func(1), 2);
  ASSERT_LT(func(3), func(1) + func(1) + func(1) + func(1));
}

TEST_CASE(assert_le) {
  ASSERT_LE(0, 1);
  ASSERT_LE(1, 1);
  ASSERT_LE(func(2), 2);
  ASSERT_LE(func(3), func(1) + func(1) + func(1));
}

RUN_ALL_TESTS();

在 CentOS 云服务器上,编译和运行 demo 示例的效果如下所示。

[root@VM-114-245-centos UnitTest]# make
g++ -g -O2 -Wall -Werror -pipe -m64 -std=c++11  -c demo_test.cpp -o demo_test.o
g++ -g -O2 -Wall -Werror -pipe -m64 -std=c++11 ./demo_test.o -o UnitTest 
Type UnitTest to execute the program.
[root@VM-114-245-centos UnitTest]# ./UnitTest 
[==============================] Running 8 test case.
Run TestCase:assert_eq
End TestCase:assert_eq
Run TestCase:assert_true
End TestCase:assert_true
Run TestCase:assert_false
End TestCase:assert_false
Run TestCase:assert_ne
End TestCase:assert_ne
Run TestCase:assert_gt
End TestCase:assert_gt
Run TestCase:assert_ge
End TestCase:assert_ge
Run TestCase:assert_lt
End TestCase:assert_lt
Run TestCase:assert_le
End TestCase:assert_le
[==============================] Total TestCase:8
Passed:8
[root@VM-114-245-centos UnitTest]# 

3.4 完整项目

春哥已将此框架开源并托管在GitHub上,项目页面包含了详细的使用说明和编译脚本。传送门:github.com/wanmuc/Unit… 。欢迎大家积极参与,进行fork、star以及提出issue。如果因为网络问题导致无法下载,请随时私信我,我会直接将项目完整源码发送给您。

4.落地实施

我们都知道人们天生有懒惰的一面,春哥也不例外,也不太想编写单元测试代码。那么如何在团队中推广并实施单元测试呢?

首先,需要选择一个合适的单元测试框架,降低个人或团队的使用成本,减少「抵触心理」。春哥已经在前文提供了一个轻量级的实现,供大家参考。

其次,团队成员需要达成共识,认识到单元测试带来的好处,并在项目排期中给予相应的「工时支持」。这样一来,大家的抵触心理会进一步降低。

最后,我们要强调的是,编写单元测试代码和业务代码同样「重要且严肃」,需要认真对待。同时,保持单元测试代码的可读性和可维护性也是至关重要的。

5.总结

单元测试、接口测试、集成测试和端到端测试共同构建了测试金字塔,其中单元测试是其基石。在未来的分享中,春哥将持续运用这个单元测试框架,以提高春哥开源项目的代码质量。

今天的内容就到这里,如果你喜欢我的分享,记得点赞、评论、转发加关注。我会持续为大家带来更多有价值的内容,你的支持是我最大的动力。感谢你的关注和支持!