测试框架 catch2 教程

本文翻译自Catch2的教程文档
GitHub地址:Catch2

获取Catch2


最简单的获取方法时下载最新的头文件版本(下载地址)。这个头文件是一系列的头文件合并生成的,里面都是普通的源代码。
其他方法包括使用系统的包管理软件或者使用它的Cmake包来进行安装。
Catch2的所有内容,包括测试项目,文档和其他内容,都可以在GitHub上下载,Clone或者fork。你也可以访问http://catch-lib.net/,会把你重定向到GitHub上。

将Catch2放在哪


Catch2只需要包含头文件就可以使用。你可以将头文件放到你的项目可以访问到的地方,或者是你的头文件搜索路径中的某个位置,或者直接放到你的项目目录树中,这对其它想要使用Catch作为它的测试框架的开源是一个很好的选项。更多内容,请访问这里
下面的教程假设Catch2的头文件是可以访问到的,但是你可能需要在头文件的前边加上正确的文件夹名。
如果你是使用系统的包管理器或者CMake包安装的,你需要#include<catch2/catch.hpp>来包含头文件。

编写测试用例


让我们从一个很简单的例子(源代码)开始。你已经写了一个计算阶乘的函数,现在你需要测试它(现在让我们先把TDD放在一边)。

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

为了简便,我们把所有的源代码放到一个文件中(后边我们才会涉及怎么组织多个测试文件)。

#define CATCH_CONFIG_MAIN  // This tells Catch to provide a main() - only do this in one cpp file
#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 );
}

上述代码可以编译成完整的可执行文件,并且可以处理相应的命令行参数。如果不带命令行参数来运行生成的可执行文件,会将所有的测试用例都执行一遍(当前的代码示例只有一个测试用例),然后报告出现的任何失败情况,给出一个总结运行结果的总结,包括多少测试用例成功,多少测试用例失败,并且返回失败的数目(如果你只是想确认是否正常工作,只看返回的失败数目是很有用的)。
如果你运行上述的代码,测试用例能够通过。看起来所有的都是正确的。对吗?然而,这里仍然有一个bug。我们期望的0的阶乘是什么?0的阶乘是1,这就是那种你必须知道(记住)的事情。
我们把这对这个bug的修改加到上面的例子中,

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

重新编译执行代码,然后,我们得到了一个错误,像是这样

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

注意,上面的错误结果打印出了Factorial(0)的实际返回值0—— 即使我们使用了一个只用了原生==操作符的表达式。打印的结果让我们立即看到了问题的所在。
让我们来改一下阶乘函数,以使得所有的测试都能通过:

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

重新编译运行代码,现在所有的case都能通过了。
当然,还有许多其他的事情需要处理。例如,我们会遇到返回值的取值范围大于unisgned int的取值范围。阶乘函数很容易就会使得这种情况发生。你可能想要添加对这种情况的测试,并想方法来处理这种情况。我们这里不对这种情况进行更详细的讨论。

那我们在这讨论啥呢?


虽然这只是个简单的例子,但是这已经足够来说明Catch是怎么使用的。让我们花一点时间来考虑一下下边的几件事情:

  1. 所有我们需要做的事情就是定义一个宏,并且包含一个头文件。设置都不必写一个main函数,命令行参数会负责处理。因为特别明显的原因,你只能使用那个宏定义#define CATCH_CONFIG_MAIN在一个cpp文件里。一旦你有多个文件里面有测试用例,你仅仅需要加入#include "catch.hpp"就可以了。一般情况下,建议在一个cpp文件里只放#define CATCH_CONFIG_MAIN#include "catch.hpp"。你也可以提供你自己实现的mian函数(参考提供你自己的main函数)。
  2. 我们通过使用TEST_CASE宏来引入一个测试用例。这个宏有一个或者两个参数——一个是没有格式要求的测试用例的名字,另一个是可选的,一个或多个标签(更多细节请参考)。测试用例的名字必须是唯一的。你可以通过使用通配符来指定运行一系列的测试用例,这些通配符用来寻找名字匹配或者标签匹配的测试用例。你可以查阅命令行文档来获取更多关于运行测试用例的信息。
  3. 测试用例的名字和标签仅仅是字符串。我们还没有在任何一个声明一个函数或者方法——或者显示的注册一个测试用例。在这种情况下,我们会为你定义一个有生成的名字的函数,而且自动的使用静态的注册类来进行注册。通过将函数名称抽象化,我们能够摆脱没有标识符名称的限制。
  4. 我们有我们自己的测试断言宏——REQUIRE。不同于每一种类型的判断条件都有一种类型,我们自然的使用C/C++语法来表达判断条件。在这样的机制下,一系列简单的表达式模板捕获到左、右两边的操作数,然后我们就可以在运行结果报告中展示这些值。稍后,我们会看到许多其他的断言宏,但是由于这项技术的使用,宏的数量已经大幅的减少了。

测试用例和测试组


大部分的测试框架都有基于类的fixture机制。即,测试用例被映射为类的方法,并且在setup()和teardown()函数中执行一些通用的设定和解除设定的操作(在一些语言中,是构造和析构函数,例如C++,支持可确定的析构)。(这里原文为deterministic destruction,我理解应该是我们可以明确的知道析构的时间点。)
虽然Catch完全支持这种方式,但还是有几个问题。在特定的情况下,你的代码需要被拆分开,因为它的粗粒度可能会引起一些问题。在这个组的测试用例中通过,你只能有一对设定和接触设定的函数。但是,有时你希望每个方法有一些稍微不同的设定,或者你希望有几个层次的设定(在这个教程的后边候会详细的解释这个概念)。James Newkirk,领导创建NUnit的人,因为这样的问题,才重新开始编写xUnit
Catch使用了不同的方式(吸取了NUnit和xUnit),能够更加自然的适合C++ 和C 家族的语言。这里的代码可以很好的说明这些。

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是从头开始运行,所以我们知道vector的大小是5,容量至少是5。我们在最外层使用REQUIRE断言宏来强制vector的大小为5,容量至少为5SECTION宏中包含了一个if语句,这个if语句是用来给Catch来判断是否需要执行这个section。每次运行一个TEST_CASE,只跑一个叶子section。其他的叶子section都会被跳过。下次再运行下一个section,直到所有的section都被执行过。
到目前为止都还是好的。对于setup/teardown方法,这是一个改善,因为,现在我们内联(inline)了setup代码,并且,使用了栈。
然而,当我们需要执行一系列的需要check的操作时,就可以见识到section的威力。继续vector的测试用例,我们可能需要验证reverse操作不会减少vectorcapacity。像下面一样,很自然的就可以做到这些。

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可以任意地嵌套(可能受你的栈的限制)。每一个叶子section(例如,不包含嵌套section的section)都会被执行一次,并且跟其他的叶子section的执行路径是不同的(所以,叶子section之间不会有相互影响)。父section的失败会导致嵌套的section不在执行,但这就是我们希望的。

BDD-风格


如果你能正确的命名你的测试用例和SECTION,你可以实现一个BDD风格的规范结构。这是一种很有用的工作方式,已经被作为一级支持加入到了Catch中。可以使用宏SCENARIOGIVENWHEN,和THEN来指定场景,这些场景会分别映射成TEST_CASESECTION。如果需要更详细的讲解,请参考Test cases 和sections
上面的vector的例子可以被改为使用上面的宏的形式(源代码)。

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。然而,当你写的实际的测试用例越来越多的时候,这并不是一个很好的方法。
我们需要下面的代码块仅仅出现在一个源文件中。你的额外的测试用例源文件的数量要满足你测试用例的需求。将源文件尽可能有意义的分割开来。每一个额外的源文件只需要#include "catch.hpp",不要重复#define
实际上,把#define块放到它自己的源文件里,是一个比较好的做法(代码示例main, tests)。
不要在头文件里写你的测试用例。

编写参数化的测试用例


除了TEST_CASE,Catch2还提供了其他的宏来编写测试用例,例如TEMPLATE_TEST_CASETEMPLATE_PRODUCT_TEST_CASE。它们同TEST_CASE的用法类似,但是可以提供更多的参数。
更多的内容请参考test cases and sections

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容