美文网首页
测试框架 catch2 教程

测试框架 catch2 教程

作者: fck_13 | 来源:发表于2020-04-07 23:55 被阅读0次

    本文翻译自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

    相关文章

      网友评论

          本文标题:测试框架 catch2 教程

          本文链接:https://www.haomeiwen.com/subject/siqvuhtx.html