当前位置: 首页 > 工具软件 > Catch2 > 使用案例 >

一,Catch2 自动测试框架使用学习

聂建茗
2023-12-01

Catch2 自动测试框架学习

本篇基于 Catch2 版本:2.13.0 / 2020-7-19 DOC翻译整理

断言宏 Assertion Macros

Catch 的断言宏主要分两种,REQUIRE 族当失败时停止测试与 CHECK 族失败后继续执行。

  1. REQUIRE( expression )
  2. CHECK( expression )
CHECK( str == "string value" );
CHECK( thisReturnsTrue() );
REQUIRE( i == 42 );

另外提供两个逻辑非的断言宏

  1. REQUIRE_FALSE( expression )
  2. CHECK_FALSE( expression )
REQUIRE_FALSE( thisReturnsFalse() );

注意 Catch 并不支持复杂的逻辑判断,如:

CHECK(a == 1 && b == 2);

浮点比较 Floating point comparisons

比较浮点需要指定精度,Catch 提供了一种称为 Approx 的包装器类来执行浮点值比较的方法。同时提供了一个简写方式 _a ,注意它在 Catch::literals 命名空间下。

REQUIRE( performComputation() == Approx( 2.1 ) );

using namespace Catch::literals;
REQUIRE( performComputation() == 2.1_a );

Approx 的默认情况可以适配大多数情况,Catch 也提供了以下三种自定义方法:

  • epsilon - epsilon serves to set the coefficient by which a result
    can differ from Approx's value before it is rejected.
    By default set to std::numeric_limits<float>::epsilon()*100.
  • margin - margin serves to set the the absolute value by which
    a result can differ from Approx's value before it is rejected.
    By default set to 0.0.
  • scale - scale is used to change the magnitude of Approx for relative check.
    By default set to 0.0.
Approx target = Approx(100).epsilon(0.01);
100.0 == target; // Obviously true
200.0 == target; // Obviously still false
100.5 == target; // True, because we set target to allow up to 1% difference

Approx target = Approx(100).margin(5);
100.0 == target; // Obviously true
200.0 == target; // Obviously still false
104.0 == target; // True, because we set target to allow absolute difference of at most 5

异常 Exceptions

期待无异常抛出的断言宏:

  1. EQUIRE_NOTHROW( expression )
  2. CHECK_NOTHROW( expression )

期待有异常抛出的断言宏:

  1. REQUIRE_THROWS( expression )
  2. CHECK_THROWS( expression )

期待有特定异常抛出的断言宏:

  1. REQUIRE_THROWS_AS( expression, exception type )
  2. CHECK_THROWS_AS( expression, exception type )

期待抛出一个转化为字符串后于提供字符串或字符匹配器匹配的异常 (匹配器在下边会有介绍):

  1. REQUIRE_THROWS_WITH( expression, string or string matcher )
  2. CHECK_THROWS_WITH( expression, string or string matcher )
REQUIRE_THROWS_WITH( openThePodBayDoors(), Contains( "afraid" ) && Contains( "can't do that" ) );
REQUIRE_THROWS_WITH( dismantleHal(), "My mind is going" );

期待抛出一个于匹配器匹配的异常:

  1. REQUIRE_THROWS_MATCHES( expression, exception type, matcher for given exception type )
  2. CHECK_THROWS_MATCHES( expression, exception type, matcher for given exception type )

请注意,THROW 断言系列期望传递单个表达式,而不是一个语句或一系列语句。 如果要检查更复杂的操作序列,可以使用 C++ 11 lambda函数。

REQUIRE_NOTHROW([&](){
    int i = 1;
    int j = 2;
    auto k = i + j;
    if (k == 3) {
        throw 1;
    }
}());

匹配器断言宏 Matcher expressions

匹配器会在下边介绍到官方文档,这里主要描述匹配器断言宏:

  1. REQUIRE_THAT( lhs, matcher expression )
  2. CHECK_THAT( lhs, matcher expression )

匹配器可以使用 &&、|| 和 !逻辑操作。

线程安全 Thread Safety

当前 Catch 的断言是不支持线程安全的,详情请参考官方文档

逗号分隔表达式 Expressions with commas

由于预处理于编译不同的规则,造成断言宏在逗号分隔上会有问题,例如

REQUIRE_THROWS_AS(std::pair<int, int>(1, 2), std::invalid_argument);

会编译失败,因为预处理器看到提供了3个参数,但宏只接受2个参数。

使用 typedef 解决:

using int_pair = std::pair<int, int>;
REQUIRE_THROWS_AS(int_pair(1, 2), std::invalid_argument);

括号表达式(不通用,它可能需要在Catch方面进行额外更改才能正常工作):

TEST_CASE_METHOD((Fixture<int, int>), "foo", "[bar]") {
    SUCCEED();
}

Matchers

Matchers 是一种易于组合的断言形式,便于复杂的自定义类型配合使用。

Matchers 主要与 REQUIRE_THAT 或 CHECK_THAT 断言宏配合使用,并且匹配器之间还可以通过逻辑与或非组合。

using Catch::Matchers::EndsWith; // or Catch::EndsWith
std::string str = getStringFromSomewhere();
REQUIRE_THAT( str, EndsWith( "as a service" ) );

REQUIRE_THAT( str,
    EndsWith( "as a service" ) ||
    (StartsWith( "Big data" ) && !Contains( "web scale" ) ) );  // 组合

同时 Matchers 还支持多个参数,进而更加精准的断言,例如使用内置的字符串匹配器接受第二个参数标志位,指定是否区分大小写:

REQUIRE_THAT( str, EndsWith( "as a service", Catch::CaseSensitive::No ) );

注意,组合操作不会获取 Matchers 的所有权,也就是使用者必须要确保其声明周期:

TEST_CASE("Bugs, bugs, bugs", "[Bug]"){
    std::string str = "Bugs as a service";

    auto match_expression = Catch::EndsWith( "as a service" ) ||
        (Catch::StartsWith( "Big data" ) && !Catch::Contains( "web scale" ) );
    REQUIRE_THAT(str, match_expression);
}

内置匹配器 Built in matchers

Catch2 默认提供了一些 Matchers,它们都在 Catch::Matchers::foo 命名空间下,同样也可以导入到 Catch 命名空间。

内置的匹配器分为两种,一种是匹配器类型本身以及创建模板匹配器时提供模板参数推导的帮助器函数。例如,用于检查 std::vector 的两个实例是否相同的匹配器是 EqualsMatcher<T>,但是希望用户改用 Equals 帮助器函数。

字符串匹配器 String matchers

字符串匹配器包括 StartsWith, EndsWith, Contains, Equals, Matches。前四个都是将指定字符串与结果进行特定规则匹配,而 Matches 则通过 ECMAScript 正则表达式匹配整个字符串。每个提供的 std::string 匹配器还带有一个可选的第二个参数,该参数决定是否区分大小写(默认情况下,它们区分大小写)。

容器匹配器 vector matchers

Catch2 提供 5 个内置的匹配器针对 std::vector:

  1. Contains 检查结果中是否存在指定变量。
  2. VectorContains 检查结果中是否存在指定的元素。
  3. Equals 它检查结果是否与特定向量完全相等(注意元素顺序)。
  4. UnorderedEquals 检查结果是否等于排列下的特定向量。
  5. Approx 它检查结果是否与特定向量“近似相等”(顺序很重要,但比较是通过“近似”完成的)。

浮点匹配器 Floating point matchers

Catch2 提供 3 个内置的浮点匹配器

  1. WithinAbsMatcher 接受目标一定距离内的浮点数。它使用 WithinAbs(double target, double margin) 枸橘函数构造。
  2. WithinUlpsMatcher 接受在目标的 ULP 一定范围内的浮点数。 因为对于浮点数和双精度数,ULP 比较需要以不同的方式进行,所以此匹配器的工具函数有两个重载,WithinULP(float target, int64_t ULPs)WithinULP(double target, int64_t ULPs)
  3. WithinRelMatcher 接受浮点数,该浮点数与目标数近似相等,并且具有特定的公差。 换句话说,它检查 |lhs - rhs| <= epsilon * max(|lhs|, |rhs|)。四个工具构造函数 WithinRel(double target, double margin), WithinRel(float target, float margin), WithinRel(double target), WithinRel(float target)

通用匹配器 Generic matchers

Catch 可以提供一些通用的匹配器:

REQUIRE_THAT("Hello olleH",
             Predicate<std::string>(
                 [] (std::string const& str) -> bool { return str.front() == str.back(); },
                 "First and last character should be equal") //第二个参数是谓词的可选描述,仅在报告结果时使用。
);

异常匹配器 Exception matchers

Catch2 还提供了一个异常匹配器,可用于验证异常消息是否与所需字符串完全匹配。匹配器是 ExceptionMessageMatcher,我们还提供了一个辅助函数 Message。匹配的异常必须显示从 std::exception 派生,并且消息的匹配是完全正确的,包括大小写。

REQUIRE_THROWS_MATCHES(throwsDerivedException(),  DerivedException,  Message("DerivedException::what"));

自定义匹配器 Custom matchers

  1. 首先从 Catch::MatcherBase<T> 派生出一个 Matcher 类,其中 T 是要测试的类型。构造函数接受并存储所需的任何参数(例如要与之进行比较的参数),然后重写两个方法:match() 和 describe()。
  2. 一个简单的构建器功能。 这是从测试代码实际调用的内容,并允许重载。
// The matcher class
class IntRange : public Catch::MatcherBase<int> {
    int m_begin, m_end;
public:
    IntRange( int begin, int end ) : m_begin( begin ), m_end( end ) {}

    // Performs the test for this matcher
    bool match( int const& i ) const override {
        return i >= m_begin && i <= m_end;
    }

    // Produces a string describing what this matcher does. It should
    // include any provided data (the begin/ end in this case) and
    // be written as if it were stating a fact (in the output it will be
    // preceded by the value under test).
    virtual std::string describe() const override {
        std::ostringstream ss;
        ss << "is between " << m_begin << " and " << m_end;
        return ss.str();
    }
};

// The builder function
inline IntRange IsBetween( int begin, int end ) {
    return IntRange( begin, end );
}

// ...

// Usage
TEST_CASE("Integers are within a range")
{
    CHECK_THAT( 3, IsBetween( 1, 10 ) );
    CHECK_THAT( 100, IsBetween( 1, 10 ) );
}

记录宏 Logging macros

在测试期间可以记录并输出详细的数据信息,使用 INFO 可以实现记录打印的效果,但是要注意打印的相关变量有效作用域,并且如果在 INFO 记录之前就出现错误则不会上报。

TEST_CASE("Foo") {
    INFO("Test case start");
    for (int i = 0; i < 2; ++i) {
        INFO("The number is " << i);
        CHECK(i == 0);
    }
}
//当 FOO 测试失败时,会输出当前记录的打印:
//Test case start
//The number is 1
TEST_CASE("Bar") {
    INFO("Test case start");
    for (int i = 0; i < 2; ++i) {
        INFO("The number is " << i);
        CHECK(i == i);
    }
    CHECK(false);
}
//当 Bar 测试失败时,只会输出打印:
//Test case start
//超过作用域的不会在上报

全局记录 Logging without local scope

  • UNSCOPED_INFOINFO 有两点不同:
    1. 不受声明周期的限制。
    2. 信息上报则只受下问最近的一个断言宏控制输出。
void print_some_info() {
    UNSCOPED_INFO("Info from helper");
}

TEST_CASE("Baz") {
    print_some_info();
    for (int i = 0; i < 2; ++i) {
        UNSCOPED_INFO("The number is " << i);
    }
    CHECK(false);
}
// Baz 失败后输出打印:
// Info from helper
// The number is 0
// The number is 1

TEST_CASE("Qux") {
    INFO("First info");
    UNSCOPED_INFO("First unscoped info");
    CHECK(false);

    INFO("Second info");
    UNSCOPED_INFO("Second unscoped info");
    CHECK(false);
}
//当第一个 CHECK 失败后:
//First info
//First unscoped info
//到达第二个 CHECK 失败后:
//First info
//Second info
//Second unscoped info

流控宏 Streaming macros

所有的日志记录宏都可以向 c++ 的标准输入输出一样使用 << 进行数据流控制。

INFO( "The number is " << i );
  1. INFO ( message expression ):消息会被记录在缓冲区,直到断言失败后一起上报,缓冲的信息收到生命周期限制。
  2. UNSCOPED_INFO( message expression ):与 INFO 类似,但是记录的数据不受生命周期限制,具体细节见前文。
  3. WARN( message expression ):WARN 的信息总是上报,但是不会终止测试。
  4. FAIL( message expression ):FAIL 的信息总是上报,并且测试用例失败终止。
  5. FAIL_CHECK( message expression ):与 FAIL 类似,但是不会终止用例。

快速捕获变量或表达式的值 Quickly capture value of variables or expressions

CAPTURE( expression1, expression2, … ):有时,只需要记录变量或表达式的值。使用 CAPTURE 宏,该宏可以接受变量或表达式,并在捕获时打印出该变量/表达式及其值。

int a = 1, b = 2, c = 3;
CAPTURE( a, b, c, a + b, c > b, a == 1);
/*
a := 1
b := 2
c := 3
a + b := 3
c > b := true
a == 1 := true
*/

还可以捕获在括号(例如函数调用),方括号或花括号(例如初始值设定项)中使用逗号的表达式。 要正确捕获包含模板参数列表的表达式(换句话说,它包含尖括号之间的逗号),需要将该表达式括在括号内:CAPTURE( (std::pair<int, int>{1, 2}) );

测试用例与节 Test cases and sections

Catch2 提供了用于单元测试的宏,同时还提供了一种特殊的测试方式

  • TEST_CASE( test name [, tags ] )
  • SECTION( section name )

test name 与 section name 字符串格式并且要唯一,tags 同样时字符串格式它标识一个或多个用方括号标记的标签。

标签 Tags

标签允许任意数量的附加字符串与测试用例相关联。可以通过标签-甚至是组合了多个标签的表达式来选择(用于运行或仅用于列出)测试用例。 在最基本的层次上,它们提供了一种将几个相关测试组合在一起的简单方法。

TEST_CASE( "A", "[widget]" ) { /* ... */ }
TEST_CASE( "B", "[widget]" ) { /* ... */ }
TEST_CASE( "C", "[gadget]" ) { /* ... */ }
TEST_CASE( "D", "[widget][gadget]" ) { /* ... */ }

使用 "[widget]" 标签选中 A、B、D,"[gadget]" 标签选中 C、D,"[widget][gadget]" 标签仅选中 D。标记名称不区分大小写,并且支持任何 ASCII 字符串,包括空格 [tag with spaces][I said "good day"],都是可以被过滤的。

特殊标签 Special Tags

Catch2 保留所有以非字母数字字符开头的标签名称用于定义了许多“特殊”标签,这些标签对测试运行器本身具有意义,均以符号字符开头。 以下是当前定义的特殊标签及其含义的列表。

  1. [!hide] or [.]:跳过当前测试用例。一般 hide 标签会与其他标签共同使用,类似于 [.][integration] ,这样默认是不会运行当前测试用例,但是可以通过命令行指定 integration 标签来运行。[.][integration] 还可以缩写成 [.integration]
  2. [!throws]:让 Catch 知道,即使测试成功此用例也可能引发异常。使用 -e 或 --nothrow 运行时,这将导致测试被排除。
  3. [!mayfail]:如果任何给定的断言失败(但仍会报告),则不会使测试失败。这对于标记正在进行的工作或不想立即解决但仍希望在测试中进行跟踪的已知问题很有用。
  4. [!shouldfail]:类似于 [!mayfail],但如果通过则无法通过测试。如果您希望收到有关意外或第三方修复程序的通知,这将很有用。
  5. [!nonportable]:表示行为在平台或编译器之间可能有所不同。
  6. [#<filename>]:使用 -# 或 --filenames-as-tags 运行 Catch2 会向所有包含的测试中添加以 # 开头的文件名(并去除所有扩展名)作为标签,例如 testfile.cpp 中的测试将全部标记为[#testfile]。
  7. [@<alias>]:标签别名以 @ 开头。
  8. [!benchmark]:这个测试案例实际上是一个基准。 这是一项实验性功能,目前尚无文档。 如果您想尝试一下,请查看projects / SelfTest / Benchmark.tests.cpp了解详细信息。

标签别名 Tag aliases

在标签表达式和通配测试名称(以及两者的组合)之间,可以构建非常复杂的模式来指示要运行哪些测试用例。 如果经常使用复杂模式,则能够为表达式创建别名很方便。 可以使用以下形式通过代码完成此操作:

CATCH_REGISTER_TAG_ALIAS( <alias string>, <tag expression> )

别名必须以@字符开头。 标签别名的示例是:

CATCH_REGISTER_TAG_ALIAS( "[@nhf]", "[failing]~[.]" )

当在命令行上使用 [@nhf] 时,它将匹配所有标记为 [failing] 的测试,但这些测试没有被隐藏。

BDD 风格的测试用例 BDD-style test cases

行为驱动开发模式的编写测试用例:

  • SCENARIO( scenario name [, tags ] ):与 TEST_CASE 类似,只不过重新命名了,应该与 BDD 配套使用。

  • GIVEN( something )

  • WHEN( something )

  • THEN( something ):这三个宏都是映射到 SECTION 上的。

  • AND_GIVEN( something )

  • AND_WHEN( something )

  • AND_THEN( something ):与 上边三个宏类似,只不过命名特殊带有 AND。

SCENARIO( "vectors can be sized and resized", "[vector]" ) {

    GIVEN( "A vector with some items" ) {
        std::vector<int> v( 5 );

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

        WHEN( "the size is increased" ) {
            v.resize( 10 );

            THEN( "the size and capacity change" ) {
                REQUIRE( v.size() == 10 );
                REQUIRE( v.capacity() >= 10 );
            }
        }
        WHEN( "the size is reduced" ) {
            v.resize( 0 );

            THEN( "the size changes but not capacity" ) {
                REQUIRE( v.size() == 0 );
                REQUIRE( v.capacity() >= 5 );
            }
        }
        WHEN( "more capacity is reserved" ) {
            v.reserve( 10 );

            THEN( "the capacity changes but not the size" ) {
                REQUIRE( v.size() == 5 );
                REQUIRE( v.capacity() >= 10 );
            }
        }
        WHEN( "less capacity is reserved" ) {
            v.reserve( 0 );

            THEN( "neither size nor capacity are changed" ) {
                REQUIRE( v.size() == 5 );
                REQUIRE( v.capacity() >= 5 );
            }
        }
    }
}

类型参数化的测试用例 Type parametrised test cases

除了TEST_CASE,Catch2 还支持按类型参数化的测试用例,形式为 TEMPLATE_TEST_CASE,TEMPLATE_PRODUCT_TEST_CASE 和 TEMPLATE_LIST_TEST_CASE。

  • TEMPLATE_TEST_CASE( test name , tags, type1, type2, …, typen )

测试名称和标签与 TEST_CASE 中的名称和标签完全相同,不同之处在于必须提供标签字符串(但是可以为空)。 从 type1 到 typen 是该测试用例应运行的类型的列表,并且在测试代码中,当前类型可用作 TestType 类型。

由于C ++预处理程序的限制,如果要指定具有多个模板参数的类型,则需要将其括在括号中 std::map<int, std::string> needs to be passed as (std::map<int, std::string>)

TEMPLATE_TEST_CASE( "vectors can be sized and resized", "[vector][template]", int, std::string, (std::tuple<int,float>) ) {

    std::vector<TestType> v( 5 );// 使用

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

    SECTION( "resizing bigger changes size and capacity" ) {
        v.resize( 10 );

        REQUIRE( v.size() == 10 );
        REQUIRE( v.capacity() >= 10 );
    }
    SECTION( "resizing smaller changes size but not capacity" ) {
        v.resize( 0 );

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

        SECTION( "We can use the 'swap trick' to reset the capacity" ) {
            std::vector<TestType> empty;
            empty.swap( v );

            REQUIRE( v.capacity() == 0 );
        }
    }
    SECTION( "reserving smaller does not change size or capacity" ) {
        v.reserve( 0 );

        REQUIRE( v.size() == 5 );
        REQUIRE( v.capacity() >= 5 );
    }
}
  • TEMPLATE_PRODUCT_TEST_CASE( test name , tags, (template-type1, template-type2, …, template-typen), (template-arg1, template-arg2, …, template-argm) )

template-type1 到 template-typen 是模板模板类型的列表,应该与 template-arg1 到 template-argm 中的每一个结合使用,从而产生 n * m 个测试用例。在测试用例中,使用 TestType 表示不同的类型。

要将一种以上类型指定为单个 template-type 或 template-arg,您必须将这些类型括在另外的括号中,例如 ((int, float), (char, double))指定2个模板参数,每个模板参数由2种具体类型(分别为int,float 和 char,double)组成。 如果仅指定一种类型作为模板类型或 template-args 的完整集合,则也可以省略括号的外部集合。

template< typename T>
struct Foo {
    size_t size() {
        return 0;
    }
};

TEMPLATE_PRODUCT_TEST_CASE("A Template product test case", "[template][product]", (std::vector, Foo), (int, float)) {
    TestType x;
    REQUIRE(x.size() == 0);
}
TEMPLATE_PRODUCT_TEST_CASE("Product with differing arities", "[template][product]", std::tuple, (int, (int, double), (int, double, float))) {
    TestType x;
    REQUIRE(std::tuple_size<TestType>::value >= 1);
}

虽然在单个 TEMPLATE_TEST_CASE 或 TEMPLATE_PRODUCT_TEST_CASE 中指定的类型数量有上限,但该限制非常高,在实践中不应遇到。

  • TEMPLATE_LIST_TEST_CASE( test name, tags, type list )

类型列表是应在其上实例化测试用例的类型的通用列表。 列表可以是 std::tuple,boost::mpl::list,boost::mp11::mp_list 或带有模板 <typename ...> 签名的任何内容。可以在多个测试用例中重用类型列表。

using MyTypes = std::tuple<int, char, float>;
TEMPLATE_LIST_TEST_CASE("Template test case with test types specified inside std::tuple", "[template][list]", MyTypes)
{
    REQUIRE(sizeof(TestType) > 0);
}

基于签名的参数化测试用例 Signature based parametrised test cases

除了类型参数化的测试用例之外,Catch2 还支持签名基本参数化的测试用例,形式为 TEMPLATE_TEST_CASE_SIG 和 TEMPLATE_PRODUCT_TEST_CASE_SIG。 这些测试用例具有类似的语法,例如类型参数化的测试用例,并带有一个附加的位置参数来指定签名。

  • Signature:签名有一些严格的规则,遵循这些测试用例才能正常工作:

    1. 具有多个模板参数的签名 typename T, size_t S 在测试用例声明中必须具有此格式 ((typename T, size_t S), T, S)
    2. 带有可变参数模板参数的签名,例如 typename T,size_t S,typename ... Ts 在测试用例声明中必须具有此格式((typename T, size_t S, typename...Ts), T, S, Ts...)
    3. 具有单个非类型模板参数的签名,例如 int V 在测试用例声明中必须具有此格式 ((int V), V)
    4. 具有单一类型模板参数的签名,例如 不应使用 typename T ,因为它实际上是 TEMPLATE_TEST_CASE。
  • TEMPLATE_TEST_CASE_SIG( test name , tags, signature, type1, type2, …, typen ):在 TEMPLATE_TEST_CASE_SIG 测试用例中,可以使用签名中定义的模板参数的名称。

TEMPLATE_TEST_CASE_SIG("TemplateTestSig: arrays can be created from NTTP arguments", "[vector][template][nttp]",
  ((typename T, int V), T, V), (int,5), (float,4), (std::string,15), ((std::tuple<int, float>), 6)) {

    std::array<T, V> v;
    REQUIRE(v.size() > 1);
}
  • TEMPLATE_PRODUCT_TEST_CASE_SIG( test name , tags, signature, (template-type1, template-type2, …, template-typen), (template-arg1, template-arg2, …, template-argm) )
template<typename T, size_t S>
struct Bar {
    size_t size() { return S; }
};

TEMPLATE_PRODUCT_TEST_CASE_SIG("A Template product test case with array signature", "[template][product][nttp]", ((typename T, size_t S), T, S), (std::array, Bar), ((int, 9), (float, 42))) {
    TestType x;
    REQUIRE(x.size() > 0);
}
 类似资料: