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
- 说明 doctest::detail::Subcase 对象一定重载了
看到这里,应该能猜测到, 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 分支组成。能多次重入,每次只执行一部分代码
从这个角度看,它们的核心编程技巧一样的