doctest 中的编程技巧

doctest

Github : https://github.com/doctest/doctest

官方介绍: The fastest feature-rich C++11/14/17/20/23 single-header testing framework

用法

摘自 https://github.com/doctest/doctest/blob/master/doc/markdown/tutorial.md

基本用法有 2 种:

基础测试用例

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"

int factorial(int number) { return number <= 1 ? number : factorial(number - 1) * number; }

TEST_CASE("testing the factorial function") {
    CHECK(factorial(1) == 1);
    CHECK(factorial(2) == 2);
    CHECK(factorial(3) == 6);
    CHECK(factorial(10) == 3628800);
}

测试用例内,有多个子测试用例

TEST_CASE("vectors can be sized and resized") {
    std::vector<int> v(5);

    REQUIRE(v.size() == 5);
    REQUIRE(v.capacity() >= 5);

    SUBCASE("adding to the vector increases its size") {
        v.push_back(1);

        CHECK(v.size() == 6);
        CHECK(v.capacity() >= 6);
    }
    SUBCASE("reserving increases just the capacity") {
        v.reserve(6);

        CHECK(v.size() == 5);
        CHECK(v.capacity() >= 6);
    }
}

可以看到, doctest 设计了一种编程模式:

  • 可以按规范定义 TEST_CASE ,来写测试逻辑代码
  • 每个 TEST_CASE 对应一个测试逻辑
  • TEST_CASE 内可以有多个子测试用例
    • 每个子测试用例,仅被执行一次
    • SUBCASE 外的代码,每个子测试用例被执行,都会执行一次,且初始状态一致

在实际的项目工程需求上,有不是功能也是写代码片段:

  • GM 命令
  • 压力测试用例
  • HTTP/RPC 等网络消息
  • 其他需要按某种规范扩展逻辑代码段的业务

因此,使用这种编程模式,可以仅定义如:

  • GM(…){ … }
  • STRESS_CASE(…){ … }
  • HTTP(…){ … }
  • RPC(…){ … }

等等,就可以扩展逻辑功能

TEST_CASE 宏实现

#define TEST_CASE(name) DOCTEST_TEST_CASE(name)
#define DOCTEST_TEST_CASE(decorators) DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators)
#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators)                    \
  static void f();                                                             \
  DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators)                      \
  static void f()
#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators)                \
  global_prefix DOCTEST_GLOBAL_NO_WARNINGS(                                    \
      DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_),                                    \
      doctest::detail::regTest(                                                \
          doctest::detail::TestCase(                                           \
              f, __FILE__, __LINE__,                                           \
              doctest_detail_test_suite_ns::getCurrentTestSuite()) *           \
          decorators))

可以看到 TEST_CASE 宏依次往里面展开后的逻辑:

  • 创建 doctest::detail::TestCase 类对象
  • 通过 doctest::detail::regTest 函数注册到全局变量

然后 main 函数内会 run 遍历全局变量,执行测试用例:

#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(
    4007) // 'function' : must be 'attribute' - see issue #182
int main(int argc, char **argv) { return doctest::Context(argc, argv).run(); }
DOCTEST_MSVC_SUPPRESS_WARNING_POP
#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN

SUBCASE 宏实现

#define SUBCASE(name) DOCTEST_SUBCASE(name)
#define DOCTEST_SUBCASE(name)                                                  \
  if (const doctest::detail::Subcase &                                         \
          DOCTEST_ANONYMOUS(DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED =            \
          doctest::detail::Subcase(name, __FILE__, __LINE__))

从上面代码可以看到 SUBCASE 实际上定义了一个 if 语句:

  • if 语句内创建了一个 doctest::detail::Subcase 对象
    • 说明 doctest::detail::Subcase 对象一定重载了 operator bool() const

看到这里,应该能猜测到, TEST_CASE - SUBCASE 的实现机制:

  • 通过多次调用 TEST_CASE 的测试用例
  • 通过 if 判断该 Subcase 是否已经执行过

这样就达成了:

  • SUBCASE 外的代码每次重置被执行(初始化)
  • 每个 SUBCASE 只被执行一次

看下代码实现, Subcase 类定义如下:

struct DOCTEST_INTERFACE Subcase {
  SubcaseSignature m_signature;
  bool m_entered = false;

  Subcase(const String &name, const char *file, int line);
  ~Subcase();

  operator bool() const;
};

Subcase 类实现如下:

Subcase::Subcase(const String &name, const char *file, int line)
    : m_signature({name, file, line}) {
  auto *s = g_cs;

  // if a Subcase on the same level has already been entered
  if (s->subcasesStack.size() < size_t(s->subcasesCurrentMaxLevel)) {
    s->should_reenter = true;
    return;
  }

  // push the current signature to the stack so we can check if the
  // current stack + the current new subcase have been traversed
  s->subcasesStack.push_back(m_signature);
  if (s->subcasesPassed.count(s->subcasesStack) != 0) {
    // pop - revert to previous stack since we've already passed this
    s->subcasesStack.pop_back();
    return;
  }

  s->subcasesCurrentMaxLevel = s->subcasesStack.size();
  m_entered = true;

  DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature);
}

Subcase::~Subcase() {
  if (m_entered) {
    // only mark the subcase stack as passed if no subcases have been skipped
    if (g_cs->should_reenter == false)
      g_cs->subcasesPassed.insert(g_cs->subcasesStack);
    g_cs->subcasesStack.pop_back();

#if defined(__cpp_lib_uncaught_exceptions) &&                                  \
    __cpp_lib_uncaught_exceptions >= 201411L &&                                \
    (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) ||                              \
     __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200)
    if (std::uncaught_exceptions() > 0
#else
    if (std::uncaught_exception()
#endif
        && g_cs->shouldLogCurrentException) {
      DOCTEST_ITERATE_THROUGH_REPORTERS(
          test_case_exception,
          {"exception thrown in subcase - will translate later "
           "when the whole test case has been exited (cannot "
           "translate while there is an active exception)",
           false});
      g_cs->shouldLogCurrentException = false;
    }
    DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY);
  }
}

Subcase::operator bool() const { return m_entered; }

Subcase 实现,比猜测的更复杂些,因为它还可以嵌套:

  • 用 subcasesStack 管理 Subcase 的入栈出栈
  • 用 subcasesPassed 管理 Subcase 是否已经被执行过了

TEST_CASE - SUBCASE 的编程模式

无独有偶, C++ 无栈协程也是一个 if else 分支组成。能多次重入,每次只执行一部分代码

从这个角度看,它们的核心编程技巧一样的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fananchong2

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值