测试框架catch使用方法

前言

都说要TDD(Test Driven Develop),catch是适用于C++的一个测试框架,我们来看看如何使用它。

实际上catch已经升级至2代,准确的说,我们应该称这个框架为catch2。项目是开源的,具体链接

catch2的一个重要的特点是不再支持c++98标准,毕竟2018年了,直接支持C11, C17,因为catch2本身的源代码就使用了很多C11的新特性。因此友情提醒:如果你的编译器没有完全支持C++11,请使用1.x版本的catch(参见项目介绍 https://github.com/catchorg/Catch2/releases/tag/v2.0.1)。

catch的改进:section

section是catch的一个特色,一般传统的xUnit测试框架往往都是有setup 和teardown两个过程,对于C++类的语言就是构造函数和析构函数,测试前的许多准备工作都对在两个过程去定义。不过有些不灵活,比较死板,catch的解决方案是使用section。
举个例子:

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

上面的测试用例实际上测试了vector的resize和reserve两个函数,虽然是一个用例,但实际上会执行4次,开始的vector初始化三行,每次都会执行,然后分别测试各个section中的内容。这样的话,开始的3行就相当于setup。但是非常直观。
我们甚至可以在section中嵌套section,比如对于上述代码的第三条,我们想再测试一下reverse 指定一个比现有capacity小的值会如何?那么我们可以直接将第三条改为

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

如果把section看做一个node,那么一个catch的测试用例会产生一个关于section的树,catch框架会执行到每一个叶子节点,而如果父节点测试失败,其子节点将不会被执行。非常符合常识。

catch的BDD style

BDD也是一种开发思想,catch有了section,就可以基于此封装宏GIVEN WHEN THEN,实现BDD风格的测试用例。BDD风格简单的说就是三段式,假设某些条件成立,当一个事件发生,然后确认程序会按照预期的行为执行。三段式内容如下:

Given some initial context (the givens),

When an event occurs,

Then ensure some outcomes.

所以第一部分的测试用例还可以写成:

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

BDD的写法和刚才的section写法是等价的,我对BDD思想还需要继续体会,的确是一个好方法就是了。

具体的生产场景的使用例子

参考了文章 https://dzone.com/articles/unit-tests-for-qt-based-applications-with-catch 以及官方文档 https://github.com/catchorg/Catch2/blob/master/docs/own-main.md#top:
我们一般得单独建立一个testmain.cpp,里面用来包含main函数,例如:

// testmain.cpp
#define CATCH_CONFIG_RUNNER
#include <QCoreApplication>

#include "catch.hpp" // include after defining CATCH_CONFIG_RUNNER
int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv);
    int res = Catch::Session().run(argc, argv);
    return res;
}

上面的是可以用于QtApplication project的testMain.cpp,注意我们的宏使用的是#define CATCH_CONFIG_RUNNER,也就是说需要我们自定义main函数,因为对于Qt Application工程,需要初始化QCoreApplication,不然编译会报错undefined reference to winmain@16。

然后建立一个testXXX.cpp,里面是具体的测试用例:

// tests-factorial.cpp
#include "catch.hpp"

#include "factorial.h"

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

一些小的细节

REQUIRE与CHECK的区别:REQUIRE的话,遇到测试用例失败直接停止,不运行后面的用例。而CHECK报错但是不停止,继续运行其他用例。
REQUIRE/CHECK中无法使用&& || 之类的,要用的话,只能分开写为多个。

写在最后

实际使用的过程中,catch编译起来很慢,那种肉眼可见的慢。而且并不支持多线程测试用例。不过TDD的工作方法是值得提倡的,也是敏捷方法落地的重要一环,自动化的测试,持续的集成交付,能让软件开发的更新周期减少不是吗?
题外话:JetBrains的CLion产品自带catch支持,因为catch作者就在JetBrains工作啊。。。

Leave a Reply

Your email address will not be published. Required fields are marked *