Catch2 自动测试框架学习
-
[测试固件 Test fixtures](#测试固件 Test fixtures "#%E6%B5%8B%E8%AF%95%E5%9B%BA%E4%BB%B6-test-fixtures")
- [定义测试固件 Defining test fixtures](#定义测试固件 Defining test fixtures "#%E5%AE%9A%E4%B9%89%E6%B5%8B%E8%AF%95%E5%9B%BA%E4%BB%B6-defining-test-fixtures")
- [基于特征的参数化测试固件 Signature-based parametrised test fixtures](#基于特征的参数化测试固件 Signature-based parametrised test fixtures "#%E5%9F%BA%E4%BA%8E%E7%89%B9%E5%BE%81%E7%9A%84%E5%8F%82%E6%95%B0%E5%8C%96%E6%B5%8B%E8%AF%95%E5%9B%BA%E4%BB%B6-signature-based-parametrised-test-fixtures")
- [在模板类型列表中指定类型的模板固件 Template fixtures with types specified in template type lists](#在模板类型列表中指定类型的模板固件 Template fixtures with types specified in template type lists "#%E5%9C%A8%E6%A8%A1%E6%9D%BF%E7%B1%BB%E5%9E%8B%E5%88%97%E8%A1%A8%E4%B8%AD%E6%8C%87%E5%AE%9A%E7%B1%BB%E5%9E%8B%E7%9A%84%E6%A8%A1%E6%9D%BF%E5%9B%BA%E4%BB%B6-template-fixtures-with-types-specified-in-template-type-lists")
-
[报告器 Reporters](#报告器 Reporters "#%E6%8A%A5%E5%91%8A%E5%99%A8-reporters")
- [使用不同的报告器 Using different reporters](#使用不同的报告器 Using different reporters "#%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%90%8C%E7%9A%84%E6%8A%A5%E5%91%8A%E5%99%A8-using-different-reporters")
-
[事件监听器 Event Listeners](#事件监听器 Event Listeners "#%E4%BA%8B%E4%BB%B6%E7%9B%91%E5%90%AC%E5%99%A8-event-listeners")
- [实现时间监听器 Implementing a Listener](#实现时间监听器 Implementing a Listener "#%E5%AE%9E%E7%8E%B0%E6%97%B6%E9%97%B4%E7%9B%91%E5%90%AC%E5%99%A8-implementing-a-listener")
- [钩子事件 Events that can be hooked](#钩子事件 Events that can be hooked "#%E9%92%A9%E5%AD%90%E4%BA%8B%E4%BB%B6-events-that-can-be-hooked")
-
[数据生成器 Data Generators](#数据生成器 Data Generators "#%E6%95%B0%E6%8D%AE%E7%94%9F%E6%88%90%E5%99%A8-data-generators")
- [GENERATE 和 SECTION 组合使用 Combining GENERATE and SECTION](#GENERATE 和 SECTION 组合使用 Combining GENERATE and SECTION "#generate-%E5%92%8C-section-%E7%BB%84%E5%90%88%E4%BD%BF%E7%94%A8-combining-generate-and-section")
- [默认提供的生成器 Provided generators](#默认提供的生成器 Provided generators "#%E9%BB%98%E8%AE%A4%E6%8F%90%E4%BE%9B%E7%9A%84%E7%94%9F%E6%88%90%E5%99%A8-provided-generators")
- [生成器接口 Generator interface](#生成器接口 Generator interface "#%E7%94%9F%E6%88%90%E5%99%A8%E6%8E%A5%E5%8F%A3-generator-interface")
-
[其他宏 Other macros](#其他宏 Other macros "#%E5%85%B6%E4%BB%96%E5%AE%8F-other-macros")
- [断言相关的宏 Assertion related macros](#断言相关的宏 Assertion related macros "#%E6%96%AD%E8%A8%80%E7%9B%B8%E5%85%B3%E7%9A%84%E5%AE%8F-assertion-related-macros")
- [测试用例相关的宏 Test case related macros](#测试用例相关的宏 Test case related macros "#%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B%E7%9B%B8%E5%85%B3%E7%9A%84%E5%AE%8F-test-case-related-macros")
-
[编写基准测试 Authoring benchmarks](#编写基准测试 Authoring benchmarks "#%E7%BC%96%E5%86%99%E5%9F%BA%E5%87%86%E6%B5%8B%E8%AF%95-authoring-benchmarks")
- [执行程序 Execution procedure](#执行程序 Execution procedure "#%E6%89%A7%E8%A1%8C%E7%A8%8B%E5%BA%8F-execution-procedure")
- [基准规范 Benchmark specification](#基准规范 Benchmark specification "#%E5%9F%BA%E5%87%86%E8%A7%84%E8%8C%83-benchmark-specification")
- [高级基准测试 Advanced benchmarking](#高级基准测试 Advanced benchmarking "#%E9%AB%98%E7%BA%A7%E5%9F%BA%E5%87%86%E6%B5%8B%E8%AF%95-advanced-benchmarking")
- [基准的构造函数和析构函数 Constructors and destructors](#基准的构造函数和析构函数 Constructors and destructors "#%E5%9F%BA%E5%87%86%E7%9A%84%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%92%8C%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0-constructors-and-destructors")
- [优化器 The optimizer](#优化器 The optimizer "#%E4%BC%98%E5%8C%96%E5%99%A8-the-optimizer")
本篇基于 Catch2 版本:2.13.0 / 2020-7-19 DOC翻译整理
测试固件 Test fixtures
定义测试固件 Defining test fixtures
虽然 Catch 允许将测试分组为测试用例中的各个部分,但有时使用更传统的测试 fixture 对它们分组仍然很方便。Catch 也完全支持这一点。您将测试固件定义为一个简单的结构:
cpp
class UniqueTestsFixture {
private:
static int uniqueID;
protected:
DBConnection conn;
public:
UniqueTestsFixture() : conn(DBConnection::createConnection("myDB")) {
}
protected:
int getID() {
return ++uniqueID;
}
};
int UniqueTestsFixture::uniqueID = 0;
TEST_CASE_METHOD(UniqueTestsFixture, "Create Employee/No Name", "[create]") {
REQUIRE_THROWS(conn.executeSQL("INSERT INTO employee (id, name) VALUES (?, ?)", getID(), ""));
}
TEST_CASE_METHOD(UniqueTestsFixture, "Create Employee/Normal", "[create]") {
REQUIRE(conn.executeSQL("INSERT INTO employee (id, name) VALUES (?, ?)", getID(), "Joe Bloggs"));
}
这里的两个测试用例将创建唯一命名的 UniqueTestsFixture 派生类,从而可以访问受 getID()保护的方法和 conn 成员变量。 这样可以确保两个测试用例都能够使用相同的方法(DRY原理)创建DBConnection,并且确保创建的任何 ID 都是唯一的,从而使执行测试的顺序无关紧要。
Catch2 还提供了 TEMPLATE_TEST_CASE_METHOD 和 TEMPLATE_PRODUCT_TEST_CASE_METHOD,它们可以与测试组一起使用,以对多种不同类型进行测试。 与 TEST_CASE_METHOD 不同, TEMPLATE_TEST_CASE_METHOD 和 TEMPLATE_PRODUCT_TEST_CASE_METHOD 要求标签规范为非空,因为它后面是其他宏参数。
还要注意,由于 C++ 预处理程序的限制,如果要指定具有多个模板参数的类型,则需要将其括在括号中,例如 std::map<int, std::string>
需要以 (std::map<int, std::string>)
的形式传递。 在 TEMPLATE_PRODUCT_TEST_CASE_METHOD 的情况下,如果类型列表的成员应由多个类型组成,则需要将其括在另一对括号中,例如 (std::map, std::pair)
和 ((int, float), (char, double))
。
cpp
template< typename T >
struct Template_Fixture {
Template_Fixture(): m_a(1) {}
T m_a;
};
TEMPLATE_TEST_CASE_METHOD(Template_Fixture,"A TEMPLATE_TEST_CASE_METHOD based test run that succeeds", "[class][template]", int, float, double) {
REQUIRE( Template_Fixture<TestType>::m_a == 1 );
}
template<typename T>
struct Template_Template_Fixture {
Template_Template_Fixture() {}
T m_a;
};
template<typename T>
struct Foo_class {
size_t size() {
return 0;
}
};
TEMPLATE_PRODUCT_TEST_CASE_METHOD(Template_Template_Fixture, "A TEMPLATE_PRODUCT_TEST_CASE_METHOD based test succeeds", "[class][template]", (Foo_class, std::vector), int) {
REQUIRE( Template_Template_Fixture<TestType>::m_a.size() == 0 );
}
基于特征的参数化测试固件 Signature-based parametrised test fixtures
Catch2 还提供 TEMPLATE_TEST_CASE_METHOD_SIG 和 TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG 以支持使用非类型模板参数的灯具。 这些测试用例的工作方式类似于 TEMPLATE_TEST_CASE_METHOD 和 TEMPLATE_PRODUCT_TEST_CASE_METHOD,并带有用于签名的附加位置参数。
cpp
template <int V>
struct Nttp_Fixture{
int value = V;
};
TEMPLATE_TEST_CASE_METHOD_SIG(Nttp_Fixture, "A TEMPLATE_TEST_CASE_METHOD_SIG based test run that succeeds", "[class][template][nttp]",((int V), V), 1, 3, 6) {
REQUIRE(Nttp_Fixture<V>::value > 0);
}
template<typename T>
struct Template_Fixture_2 {
Template_Fixture_2() {}
T m_a;
};
template< typename T, size_t V>
struct Template_Foo_2 {
size_t size() { return V; }
};
TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(Template_Fixture_2, "A TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG based test run that succeeds", "[class][template][product][nttp]", ((typename T, size_t S), T, S),(std::array, Template_Foo_2), ((int,2), (float,6)))
{
REQUIRE(Template_Fixture_2<TestType>{}.m_a.size() >= 2);
}
在模板类型列表中指定类型的模板固件 Template fixtures with types specified in template type lists
Catch2 还提供 TEMPLATE_LIST_TEST_CASE_METHOD,以支持模板类型列表中指定类型的模板固定装置。 此测试用例与 TEMPLATE_TEST_CASE_METHOD 相同,仅区别在于类型的来源。这使您可以在多个测试案例中重用模板类型列表。
cpp
using MyTypes = std::tuple<int, char, double>;
TEMPLATE_LIST_TEST_CASE_METHOD(Template_Fixture, "Template test case method with test types specified inside std::tuple", "[class][template][list]", MyTypes)
{
REQUIRE( Template_Fixture<TestType>::m_a == 1 );
}
报告器 Reporters
Catch2 具有模块化的报告系统,并捆绑了一些有用的内置报告器。您还可以编写自己的报告器。
使用不同的报告器 Using different reporters
可以从命令行轻松控制要使用的报告程序。要指定报告者,使用 -r 或--reporter,后跟报告者的名称,例如:
cpp
-r xml
-
如果您未指定报告程序,则默认情况下使用控制台报告程序。 内置有四个报告器:
- console:控制台以文本行形式写入,格式化为典型的终端宽度,如果检测到有能力的终端,则使用颜色。
- compact:类似于控制台的紧凑型设备,但针对最小输出进行了优化,每个输入一行。
- junit:编写与 Ant 的 junit report 目标相对应的 xml。 对于了解 Junit 的构建系统很有用。 由于 junit 格式的结构方式,运行必须在写入任何内容之前完成。
- xml:编写专门为 Catch 设计的 xml 格式。 与 junit 不同,这是一种流格式,因此结果将逐步传递。
在 Catch 信息库中(包括include \ reporters),还有一些针对特定构建系统的其他报告程序,如果想使用它们,可以将其 #include 到项目中。 在一个源文件中执行此操作-与拥有 CATCH_CONFIG_MAIN 或 CATCH_CONFIG_RUNNER 的源文件方式相同。
事件监听器 Event Listeners
侦听器是可以在 Catch 中注册的类,该类将在测试运行期间通过事件(例如测试用例的开始或结束)传递给事件。 侦听器实际上是 Reporter 的类型,有一些细微差别:
- 在代码中注册后,它们会自动使用-您无需在命令行上指定它们。
- 除了(在之前)任何报告程序之外,还可以调用它们,并且您可以注册多个侦听器。
- 它们从
Catch::TestEventListenerBase
派生而来,它具有所有事件的默认存根,因此您不必强制实施不感兴趣的事件。 - 使用 CATCH_REGISTER_LISTENER 注册。
实现时间监听器 Implementing a Listener
只需从 Catch::TestEventListenerBase 派生一个类并在主源文件(即定义 CATCH_CONFIG_MAIN 或 CATCH_CONFIG_RUNNER 的文件)中或在定义 CATCH_CONFIG_EXTERNAL_INTERFACES 的文件中实现您感兴趣的方法。
cpp
//210-Evt-EventListeners.cpp
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
struct MyListener : Catch::TestEventListenerBase {
using TestEventListenerBase::TestEventListenerBase; // inherit constructor
void testCaseStarting( Catch::TestCaseInfo const& testInfo ) override {
// Perform some setup before a test case is run
}
void testCaseEnded( Catch::TestCaseStats const& testCaseStats ) override {
// Tear-down after a test case is run
}
};
CATCH_REGISTER_LISTENER( MyListener )
钩子事件 Events that can be hooked
以下是可以在侦听器中覆盖的方法:
cpp
// The whole test run, starting and ending
virtual void testRunStarting( TestRunInfo const& testRunInfo );
virtual void testRunEnded( TestRunStats const& testRunStats );
// Test cases starting and ending
virtual void testCaseStarting( TestCaseInfo const& testInfo );
virtual void testCaseEnded( TestCaseStats const& testCaseStats );
// Sections starting and ending
virtual void sectionStarting( SectionInfo const& sectionInfo );
virtual void sectionEnded( SectionStats const& sectionStats );
// Assertions before/ after
virtual void assertionStarting( AssertionInfo const& assertionInfo );
virtual bool assertionEnded( AssertionStats const& assertionStats );
// A test is being skipped (because it is "hidden")
virtual void skipTest( TestCaseInfo const& testInfo );
有关事件的更多信息(例如测试用例的名称)包含在作为参数传递的结构中-只需查看源代码即可查看哪些字段可用。
数据生成器 Data Generators
数据生成器(也称为数据驱动/参数化测试用例)可以跨不同的输入值重用同一组断言。 在 Catch2 中,它们遵循 TEST_CASE和SECTION 宏的顺序和嵌套,并且它们的嵌套节在生成器中每个值运行一次。
cpp
TEST_CASE("Generators") {
auto i = GENERATE(1, 3, 5);
REQUIRE(is_odd(i));
}
TEST_CASE 将被输入3次,i 的值依次为 1、3 和 5。generate 还可以在同一范围内多次使用,在这种情况下,结果将是生成器中所有元素的笛卡尔积。这意味着在下面的代码片段中,测试用例将运行 6(2*3) 次。
cpp
TEST_CASE("Generators") {
auto i = GENERATE(1, 2);
auto j = GENERATE(3, 4, 5);
}
Catch2 中的生成器分为两部分,GENERATE 宏以及已经提供的生成器,以及允许用户实现自己的生成器的 IGenerator <T>
接口。
GENERATE 和 SECTION 组合使用 Combining GENERATE and SECTION
GENERATE 可以看作是隐式的 SECTION,从使用 GENERATE 的位置到作用域的末尾。这可以用于各种效果。下面显示了最简单的用法,其中第一个部分运行 4(2 * 2
次,而第二个部分运行 6 次(12 * 3)
。
cpp
TEST_CASE("Generators") {
auto i = GENERATE(1, 2);
SECTION("one") {
auto j = GENERATE(-3, -2);
REQUIRE(j < i);
}
SECTION("two") {
auto k = GENERATE(4, 5, 6);
REQUIRE(j != k);
}
}
The specific order of the SECTIONs will be "one", "one", "two", "two", "two", "one"
GENERATE 引入虚拟 SECTION 的实现方式也可以用于使生成器仅重播某些 SECTION,而不必显式添加 SECTION。 例如,下面的代码报告3个断言,因为"第一"部分运行一次,而"第二"部分运行两次。
cpp
TEST_CASE("GENERATE between SECTIONs") {
SECTION("first") { REQUIRE(true); }
auto _ = GENERATE(1, 2);
SECTION("second") { REQUIRE(true); }
}
这会导致令人惊讶的复杂测试流程。例如,下面的测试将报告 14 个断言:
cpp
TEST_CASE("Complex mix of sections and generates") {
auto i = GENERATE(1, 2);
SECTION("A") {
SUCCEED("A");
}
auto j = GENERATE(3, 4);
SECTION("B") {
SUCCEED("B");
}
auto k = GENERATE(5, 6);
SUCCEED();
}
默认提供的生成器 Provided generators
Catch2 提供的生成器功能包括以下几个部分:
-
GENERATE 宏,用于将生成器表达式与测试用例集成在一起。
-
两个基本生成器:
SingleValueGenerator<T>
:仅包含单个元素。FixedValuesGenerator<T>
:包含多个元素。
-
5个通用生成器,可修改其他生成器:
FilterGenerator<T, Predicate>
:从 Predicate 返回 "false" 的生成器中滤除元素。TakeGenerator<T>
:从生成器中获取前 n 个元素。RepeatGenerator<T>
:重复生成器的输出 n 次。MapGenerator<T, U, Func>
:返回将 Func 应用于来自其他生成器的元素的结果。ChunkGenerator<T>
:从生成器返回 n 个元素的块(通过 vector 容器)。
-
4个专用生成器:
RandomIntegerGenerator<Integral>
-- 从范围生成随机积分。RandomFloatGenerator<Float>
-- 从范围生成随机浮点数。RangeGenerator<T>
-- 生成一个算术范围内的所有值。IteratorGenerator<T>
-- 从迭代器范围复制并返回值。
生成器还具有关联的辅助函数,这些函数可以推断其类型,从而使它们的用法更好:
value(T&&)
forSingleValueGenerator<T>
values(std::initializer_list<T>)
forFixedValuesGenerator<T>
table<Ts...>(std::initializer_list<std::tuple<Ts...>>)
forFixedValuesGenerator<std::tuple<Ts...>>
filter(predicate, GeneratorWrapper<T>&&)
forFilterGenerator<T, Predicate>
take(count, GeneratorWrapper<T>&&)
forTakeGenerator<T>
repeat(repeats, GeneratorWrapper<T>&&)
forRepeatGenerator<T>
map(func, GeneratorWrapper<T>&&)
forMapGenerator<T, U, Func>
(mapU
toT
, deduced fromFunc
)map<T>(func, GeneratorWrapper<U>&&)
forMapGenerator<T, U, Func>
(mapU
toT
)chunk(chunk-size, GeneratorWrapper<T>&&)
forChunkGenerator<T>
random(IntegerOrFloat a, IntegerOrFloat b)
forRandomIntegerGenerator
orRandomFloatGenerator
range(Arithemtic start, Arithmetic end)
forRangeGenerator<Arithmetic>
with a step size of1
range(Arithmetic start, Arithmetic end, Arithmetic step)
forRangeGenerator<Arithmetic>
with a custom step sizefrom_range(InputIterator from, InputIterator to)
forIteratorGenerator<T>
from_range(Container const&)
forIteratorGenerator<T>
cpp
TEST_CASE("Generating random ints", "[example][generator]") {
SECTION("Deducing functions") {
auto i = GENERATE(take(100, filter([](int i) { return i % 2 == 1; }, random(-100, 100))));
REQUIRE(i > -100);
REQUIRE(i < 100);
REQUIRE(i % 2 == 1);
}
}
除了在 Catch2 中注册生成器外,GENERATE 宏还有一个目的,那就是提供一种生成定制生成器的简单方法,如本页第一个示例所示,在此我们将其用作 auto i = GENERATE(1, 2, 3)
;。这种用法将逐句地将每一个都转换为单个 SingleValueGenerator<int>
,然后将它们全部放置在连接其他生成器的特殊生成器中。它也可以与其他生成器一起用作参数,例如 auto i = GENERATE(0, 2, take(100, random(300, 3000)));
。 这很有用,例如 如果您知道特定的输入有问题,并想分别进行测试。
出于安全原因,不能在 GENERATE 宏中使用变量。 这样做是因为生成器表达式将超出声明周期,因此使用引用会引入问题。 如果需要在生成器表达式中使用变量,请确保仔细考虑生命周期的影响并使用 GENERATE_COPY 或 GENERATE_REF。
还可以通过使用 as<type>
作为宏的第一个参数来覆盖推断的类型。如果您希望它们以 std::string
的类型提供,这在处理字符串文字时可能会很有用。
cpp
TEST_CASE("type conversion", "[generators]") {
auto str = GENERATE(as<std::string>{}, "a", "bb", "ccc");
REQUIRE(str.size() > 0);
}
生成器接口 Generator interface
您还可以通过继承 IGenerator<T>
接口来实现自己的生成器:
cpp
template<typename T>
struct IGenerator : GeneratorUntypedBase {
// via GeneratorUntypedBase:
// Attempts to move the generator to the next element.
// Returns true if successful (and thus has another element that can be read)
virtual bool next() = 0;
// Precondition:
// The generator is either freshly constructed or the last call to next() returned true
virtual T const& get() const = 0;
};
其他宏 Other macros
此页面可作为未在其他地方记录的宏的参考。目前,这些宏分为 2 个大致类别,即"与断言相关的宏"和"与测试用例相关的宏"。
断言相关的宏 Assertion related macros
CHECKED_IF
&CHECKED_ELSE
:CHECKED_IF( expr ) 是一个 if 替换,它也将 Catch2 的字符串化机制应用于 expr 并记录结果。与 if 一样,仅当表达式的计算结果为 true 时,才输入 CHECKED_IF 之后的块。 CHECKED_ELSE( expr ) 的工作方式类似,但是只有在 expr 评估为 false 时才进入该块。
cpp
int a = ...;
int b = ...;
CHECKED_IF( a == b ) {
// This block is entered when a == b
} CHECKED_ELSE ( a == b ) {
// This block is entered when a != b
}
- CHECK_NOFAIL:CHECK_NOFAIL( expr ) 是 CHECK 的变体,如果 expr 评估为 false,它不会使测试用例失败。 这对于检查某些假设可能很有用,而在测试未必一定失败的情况下可能被违反。
cpp
main.cpp:6:
FAILED - but was ok:
CHECK_NOFAIL( 1 == 2 )
main.cpp:7:
PASSED:
CHECK( 2 == 2 )
- SUCCEED
SUCCEED( msg ) is mostly equivalent with INFO( msg ); REQUIRE( true );. 换句话说,"成功"适用于仅达到特定水平即表示测试已成功的情况。
cpp
TEST_CASE( "SUCCEED showcase" ) {
int I = 1;
SUCCEED( "I is " << I );
}
- STATIC_REQUIRE:STATIC_REQUIRE( expr ) 是一个宏,可以与 static_assert 相同的方式使用,但也可以向 Catch2 注册成功,因此在运行时报告为成功。通过在包含 Catch2 标头之前定义CATCH_CONFIG_RUNTIME_STATIC_REQUIRE,也可以将整个检查推迟到运行时。
cpp
TEST_CASE("STATIC_REQUIRE showcase", "[traits]") {
STATIC_REQUIRE( std::is_void<void>::value );
STATIC_REQUIRE_FALSE( std::is_void<int>::value );
}
测试用例相关的宏 Test case related macros
- METHOD_AS_TEST_CASE:METHOD_AS_TEST_CASE( member-function-pointer, description ) 可以将类的成员函数注册为 Catch2 测试用例。对于以这种方式注册的每个方法,将分别实例化该类。
cpp
class TestClass {
std::string s;
public:
TestClass()
:s( "hello" )
{}
void testCase() {
REQUIRE( s == "hello" );
}
};
METHOD_AS_TEST_CASE( TestClass::testCase, "Use class's method as a test case", "[class]" )
- REGISTER_TEST_CASE:REGISTER_TEST_CASE( function, description ) 将一个函数注册为测试用例。 该函数必须具有
void()
签名,描述可以包含名称和标签。
cpp
REGISTER_TEST_CASE( someFunction, "ManuallyRegistered", "[tags]" );
请注意,注册仍必须在启动 Catch2
的会话之前进行。 这意味着它要么需要在全局构造函数中完成,要么需要在用户自己的 main 中创建 Catch2 的会话之前。
- ANON_TEST_CASE:ANON_TEST_CASE 将 TEST_CASE 替换自动生成唯一名称。这样做的好处是您不必为测试用例考虑一个名称,"缺点是该名称不一定在不同链接之间保持稳定,因此可能很难直接指定运行。
cpp
ANON_TEST_CASE() {
SUCCEED("Hello from anonymous test case");
}
- DYNAMIC_SECTION:DYNAMIC_SECTION is a SECTION where the user can use operator<< to create the final name for that section. This can be useful with e.g. generators, or when creating a SECTION dynamically, within a loop.
cpp
TEST_CASE( "looped SECTION tests" ) {
int a = 1;
for( int b = 0; b < 10; ++b ) {
DYNAMIC_SECTION( "b is currently: " << b ) {
CHECK( b > a );
}
}
}
编写基准测试 Authoring benchmarks
请注意,默认情况下基准测试支持处于禁用状态,要启用它,您需要定义 CATCH_CONFIG_ENABLE_BENCHMARKING。有关更多详细信息,请参见编译时配置文档。
编写基准并不容易。Catch 简化了某些方面,但是您始终需要注意各个方面。在编写基准测试时,了解有关 Catch 运行代码方式的一些知识将非常有帮助。
- 用户代码:用户代码是用户提供的要测量的代码。
- 运行:一次运行是对用户代码的一次执行。
- 样本:一个样本是通过测量执行一定数量的运行所花费的时间而获得的一个数据点。如果可用时钟的分辨率不足以精确测量一次运行,则一个样本可以包含一个以上的运行。给定基准执行的所有样本均以相同的运行次数获得。
执行程序 Execution procedure
现在,我可以解释如何在Catch中执行基准测试。 有三个主要步骤,尽管不必为每个基准重复第一个步骤:
- 环境探测:在执行任何基准测试之前,先估算时钟的分辨率。此时还估计了其他一些环境伪影,例如调用时钟函数的成本,但它们几乎对结果没有任何影响。
- 估计参数:用户代码执行几次,以获得每个样本中应进行的运行量的估计。这也具有在实际测量开始之前将相关代码和数据带入缓存的潜在影响。
- 测量:通过执行每个样品在上一步中估计的运行次数,依次收集所有样品。
这已经为我们提供了一个为Catch编写基准的重要规则:基准必须是可重复的。用户代码将被执行几次,并且在估计步骤中将被执行的次数是事先未知的,因为它取决于执行代码所花费的时间。无法重复执行的用户代码将导致虚假结果或崩溃。
基准规范 Benchmark specification
基准可以在 Catch 测试用例内的任何地方指定。有一个简单的高级版本的 BENCHMARK 宏。
让我们看一下如何对朴素的斐波那契实现进行基准测试:
cpp
std::uint64_t Fibonacci(std::uint64_t number) {
return number < 2 ? 1 : Fibonacci(number - 1) + Fibonacci(number - 2);
}
最简单的基准测试此功能的方法是在我们的测试用例中添加一个BENCHMARK 宏:
cpp
TEST_CASE("Fibonacci") {
CHECK(Fibonacci(0) == 1);
// some more asserts..
CHECK(Fibonacci(5) == 8);
// some more asserts..
// now let's benchmark:
BENCHMARK("Fibonacci 20") {
return Fibonacci(20);
};
BENCHMARK("Fibonacci 25") {
return Fibonacci(25);
};
BENCHMARK("Fibonacci 30") {
return Fibonacci(30);
};
BENCHMARK("Fibonacci 35") {
return Fibonacci(35);
};
}
-
有以下几点注意:
- 当 BENCHMARK 扩展为 lambda 表达式时,有必要在右括号后添加分号(与第一个实验版本相反)。
- 返回值是避免编译器优化基准代码的便捷方法。
输出:
cpp
-------------------------------------------------------------------------------
Fibonacci
-------------------------------------------------------------------------------
C:\path\to\Catch2\Benchmark.tests.cpp(10)
...............................................................................
benchmark name samples iterations estimated
mean low mean high mean
std dev low std dev high std dev
-------------------------------------------------------------------------------
Fibonacci 20 100 416439 83.2878 ms
2 ns 2 ns 2 ns
0 ns 0 ns 0 ns
Fibonacci 25 100 400776 80.1552 ms
3 ns 3 ns 3 ns
0 ns 0 ns 0 ns
Fibonacci 30 100 396873 79.3746 ms
17 ns 17 ns 17 ns
0 ns 0 ns 0 ns
Fibonacci 35 100 145169 87.1014 ms
468 ns 464 ns 473 ns
21 ns 15 ns 34 ns
高级基准测试 Advanced benchmarking
上面显示的最简单的用例,不带参数,仅运行需要测量的用户代码。 但是,如果使用 BENCHMARK_ADVANCED 宏并在宏之后添加 Catch::Benchmark::Chronometer 参数,则可以使用某些高级功能。简单基准测试的内容每次运行被调用一次,而高级基准测试的块恰好被调用两次:一次在估计阶段,另一次在执行阶段。
cpp
BENCHMARK("simple"){ return long_computation(); };
BENCHMARK_ADVANCED("advanced")(Catch::Benchmark::Chronometer meter) {
set_up();
meter.measure([] { return long_computation(); });
};
这些高级基准不再完全由要测量的用户代码组成。 在这些情况下,要通过 Catch::Benchmark::Chronometer::measure 成员函数提供要测量的代码。 这使您可以设置基准可能需要的但不包含在测量中的任何类型的状态,例如制作随机整数向量以馈入排序算法。
对 Catch::Benchmark::Chronometer::measure 的单个调用通过调用传入的可调用对象来执行实际测量,该对象在必要时会多次传递。在测量之外需要执行的任何操作都可以在测量调用之外进行。
传递给度量的可调用对象可以选择接受一个int参数。
cpp
meter.measure([](int i) { return long_computation(i); });
如果它接受一个 int 参数,则将传入每次运行的序列号,从 0 开始。这对于例如要测量某些变异代码很有用。可以通过调用 Catch::Benchmark::Chronometer::runs 事先知道运行次数。有了这个,就可以设置一个不同的实例,以便每次运行都可以对其进行突变。
cpp
std::vector<std::string> v(meter.runs());
std::fill(v.begin(), v.end(), test_string());
meter.measure([&v](int i) { in_place_escape(v[i]); });
请注意,不可简单地将同一实例用于不同的运行,并在每次运行之间将其重置,因为那样会导致重置代码污染测量结果。
也可以仅向简单的 BENCHMARK 宏提供参数名称,以获取与提供带 int 参数的 meter.measure 的可调用对象相同的语义:
BENCHMARK("indexed", i){ return long_computation(i); };
基准的构造函数和析构函数 Constructors and destructors
所有这些工具都为您提供了很多帮助,但是有两件事仍然需要特殊处理:构造函数和析构函数。问题是,如果使用自动对象,它们会在作用域结束时被销毁,因此最终需要同时度量构建和销毁的时间。如果使用动态分配,最终会在度量中包含分配内存的时间。
为了解决这个难题,Catch提供了类模板,允许您在不进行动态分配的情况下手动构造和销毁对象,并且允许您分别度量构造和销毁。
cpp
BENCHMARK_ADVANCED("construct")(Catch::Benchmark::Chronometer meter) {
std::vector<Catch::Benchmark::storage_for<std::string>> storage(meter.runs());
meter.measure([&](int i) { storage[i].construct("thing"); });
};
BENCHMARK_ADVANCED("destroy")(Catch::Benchmark::Chronometer meter) {
std::vector<Catch::Benchmark::destructable_object<std::string>> storage(meter.runs());
for(auto&& o : storage)
o.construct("thing");
meter.measure([&](int i) { storage[i].destruct(); });
};
Catch::Benchmark::storage_for<T>
对象只是适用于 T 对象的原始存储片段。 您可以使用 Catch::Benchmark::storage_for::construct
成员函数来调用构造函数并在该存储中创建对象。 因此,如果要测量某个构造函数运行所花费的时间,则只需测量运行此函数所花费的时间即可。
当 Catch::Benchmark::storage_for<T>
对象的生命周期结束时,如果在那里构造了实际对象,它将被自动销毁,因此不会泄漏任何内容。
但是,如果你想测量析构函数,我们需要使用 Catch::Benchmark::destructable_object<T>
。这些对象类似于T对象的结构中的 Catch::Benchmark::storage_for<T>
,但它不会自动销毁任何东西。相反,你需要调用 Catch::Benchmark::destructable_object::destruct
析构成员函数,你可以用它来测量销毁时间。
优化器 The optimizer
有时优化器会优化掉您想要度量的代码。有几种使用结果的方法可以防止优化器删除它们。您可以使用 volatile 关键字,也可以将值输出到标准输出或文件中,这两种方法都会迫使程序以某种方式实际生成该值。
Catch 增加了第三个选项。作为用户代码提供的任何函数返回的值都保证会被计算而不会被优化出来。这意味着,如果您的用户代码由计算某个值组成,那么您不需要费心使用 volatile 或强制输出。从函数中返回它。这有助于保持代码的自然方式。
cpp
// may measure nothing at all by skipping the long calculation since its
// result is not used
BENCHMARK("no return"){ long_calculation(); };
// the result of long_calculation() is guaranteed to be computed somehow
BENCHMARK("with return"){ return long_calculation(); };
但是,对优化器没有任何其他形式的控制。 由您决定编写一个基准,该基准可以实际测量所需的内容,而不仅仅是测量无所事事的时间。
总结起来,有两个简单的规则:在 Catch 中仍然可以使用手写代码来控制优化的任何事情; Catch 使用户代码的返回值变成无法优化的可观察效果。