Catch2 入门教程

2019-08-08
7 min read

译者注:当前文档并不是官方文档的直译。在翻译的过程中我删除部分原文中的内容并添加了一些自己的理解,可能有偏差,请见谅

[TOC]

获得 Catch

获得Catch最简单的方式是下载最新的 single header version。这个头文件由若干其他独立的头文件合并而成。

你也可以使用其他方法获得Catch,例如使用CMake来构建编译版Catch,这可以提高项目的编译速度。

完整的Catch包含测试、说明文档等内容,你可以从GitHub下载完整的Catch。Catch官方链接为:http://catch-lib.net ,此链接将重定向到GitHub。

如何使用 Catch?

Catch是header-only的,故你只需要将Catch的头文件放到编译器可以发现的路径既可。

下面的教程默认你的编译器可以发现并使用 Catch。

如果你使用Catch的预编译形式,即已经编译并生成了Catch链接库(.lib 或者 .a 文件),你的Catch头文件包含形式应该形如:#include <catch2/catch.hpp>

编写测试用例

让我们从一个简单的示例开始(examples/010-TestCase.cpp)。假设你已经写了一个用于计算阶乘的函数,现在准备测试它。(TDD的基本原则是先写测试代码,为了方便学习,这里先忽略这个原则)

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

为了尽量简单,我们把所有的代码都放到一个源文件中。

#define CATCH_CONFIG_MAIN  // 当前宏强制Catch在当前编译单元中创建 main(),这个宏只能出现在一个CPP文件中,因为一个项目只能有一个有效的main函数
#include "catch.hpp"

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

TEST_CASE( "Factorials are computed", "[factorial]" ) {
    REQUIRE( Factorial(1) == 1 );
    REQUIRE( Factorial(2) == 2 );
    REQUIRE( Factorial(3) == 6 );
    REQUIRE( Factorial(10) == 3628800 );
}

编译结束后将生成一个可以接受运行时参数的可执行文件,具体可用参数请参考command-line.md。如果以不带参数的方式执行可执行文件,所有测试用例都将被执行。详细的测试报告将输出到终端,测试报告包含失败的测试用例、失败的测试用例个数、成功的测试用例个数等信息。

执行上面代码生成的可执行文件,所有测试用例都将通过。真的没有错误码?不是的,上面的阶乘函数是有错误的,我写的第一版教程中就有这个Bug,感谢CTMacUser帮我指出了这个错误。

这个错误是什么呢?0的阶乘是多少?——0的阶乘是1而不是0,这就是上面阶乘函数的错误之处。参考: 0的阶乘是1

让我们把上面的规则写入到测试用例中:

TEST_CASE( "Factorials are computed", "[factorial]" ) {
    REQUIRE( Factorial(0) == 1 );
    REQUIRE( Factorial(1) == 1 );
    REQUIRE( Factorial(2) == 2 );
    REQUIRE( Factorial(3) == 6 );
    REQUIRE( Factorial(10) == 3628800 );
}

现在测试失败了,Catch输出:

Example.cpp:9: FAILED:
  REQUIRE( Factorial(0) == 1 )
with expansion:
  0 == 1

Catch的测试报告会输出期望值和Factorial(0)计算出的错误值0,这样我们就可以很方便的找到错误。

让我们修正阶乘函数:

unsigned int Factorial( unsigned int number ) {
  return number > 1 ? Factorial(number-1)*number : 1;
}

现在所有的测试用例都通过了。

当然了上面的阶乘函数依旧有不少问题,例如当number很大时计算的结果将溢出,不过我们暂不管这些。

我们做了什么?

虽然上面的测试比较简单,但已经足够展示如何使用Catch了。在进一步学习前,我们先解释一下上面那段代码。

  1. 我们定义了一个宏,并包含了Catch的头文件,然后编译这个源文件并生成了一个接受运行时参数的可执行文件。为了可执行,定义了宏#define CATCH_CONFIG_MAIN,强制Catch引入预定义main函数,你也可以编写自己的main函数(参考:own-main.md)。

  2. 我们在宏TEST_CASE中编写测试用例。这个宏可以包含一个或者两个参数,其中一个参数是没有固定格式的测试名,另一个参数则包含一个或多个标签(下文介绍)。测试名必须唯一。参考command-line.md以获得更多有关执行可执行文件的信息。

  3. 测试名和标签都是字符串。

  4. 我们仅使用宏REQUIRE来编写测试断言。Catch没有使用分立的测试函数表示不同的断言(例如REQUIRE_TRUE、REQUIRE_FALSE、REQUIRE_EQUAL、REQUIRE_LESS等),而是直接使用C++表达式的真值结果(其他可用宏可参考这里)。此外Catch使用模板表达式捕获测试表达式的左侧和右侧(例如 exp_a == exp_b,Catch将捕获exp_a和exp_b的计算结果),从而在测试报告中显示两侧的计算结果。

测试用例和测试区段(Test case and section)

大部分测试框架都有某种基于类的机制。例如,在很多框架(例如JUnit)的setup()阶段可以创建一个在其他用例中使用的测试对象(可以是需要测试的对象,也可以是Mock对象),在teardown()阶段销毁这些对象,从而避免在每一个测试用例中创建与销毁测试对象(或mock对象)。

对于Catch而言,使用上面传统的测试方式有一定的缺陷,例如对于同一批测试用例你只能创建同一个测试对象,这样的话测试粒度就比较大。(译者注:其他缺陷可以参考原文)

Catch 使用全新的方式解决了上面的问题,如下:

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

    std::vector<int> 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( "reserving bigger changes capacity but not size" ) {
        v.reserve( 10 );

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

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

对于每一个SECTIONTEST_CASE都将重新从当前TEST_CASE的起始部分开始执行并忽略其他SECTION。 (译者注:这段原文简单解释了原因,Catch使用了if语句并把section看做子节点,每次执行TEST_CASE时Catch先执行起始部分的非SECTION代码,然后选择一个子节点并执行)。

到目前为止,Catch使用上述方式已经实现了大部分测试框架基于类(setup&teardown)的测试机制。

SECTION可以嵌套任意深度,每一个SECTION子节点都只会被执行一次,大量嵌套的SECTION会形成一棵“树”,父节点执行失败将不再执行对应的子节点:

    SECTION( "reserving bigger changes capacity but not size" ) {
        v.reserve( 10 );

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

        SECTION( "reserving smaller again does not change capacity" ) {
            v.reserve( 7 );

            REQUIRE( v.capacity() >= 10 );
        }
    }

BDD-Style

Catch可以使用BDD-Style形式的测试,具体请参考:test-cases-and-sections.md,下面是一个简单的例子:

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

运行上面的测试用例将输出以下内容:

Scenario: vectors can be sized and resized
     Given: A vector with some items
      When: more capacity is reserved
      Then: the capacity changes but not the size

小结

为了保证教程的简洁性我们把所有代码放在了一个文件中,在实际项目中这并不是好的方式。

比较好的方式是将下面这段代码写在一个独立的源文件中,其他测试文件仅包含Catch头文件和测试代码。不要在其他测试文件中重复包含下面的#define语句。

#define CATCH_CONFIG_MAIN
#include "catch.hpp"

不要在头文件中写测试代码!

类型参数化测试

Catch支持类型参数化测试,宏TEMPLATE_TEST_CASETEMPLATE_PRODUCT_TEST_CASE的行为和TEST_CASE类似,但测试用例会在不同类型下执行。下面代码中TestType的取值依次为intfloatstd::stringBar,所有测试用例都将在这些类型下执行一遍。

struct Bar {}; 

TEMPLATE_TEST_CASE("Templated test","",int,float, std::string, Bar)
{
    std::vector<TestType> v( 5 );
    REQUIRE( v.size() == 5 );
    REQUIRE( v.capacity() >= 5 );
}

更多信息请参考:test-cases-and-sections.md中type-parametrised-test-cases小节

后续学习与使用

当前文档简要介绍了Catch,也指出了Catch和其他测试框架的一些区别。了解这些知识后你已经可以编写一些实际的测试用例了。

当然还有很多东西需要学习,但你只需要在用到那些新特性的时候再学。你可以在 Readme.md 中找到Catch的所有特性。