本文翻译自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是怎么使用的。让我们花一点时间来考虑一下下边的几件事情:
- 所有我们需要做的事情就是定义一个宏,并且包含一个头文件。设置都不必写一个
main
函数,命令行参数会负责处理。因为特别明显的原因,你只能使用那个宏定义#define CATCH_CONFIG_MAIN
在一个cpp文件里。一旦你有多个文件里面有测试用例,你仅仅需要加入#include "catch.hpp"
就可以了。一般情况下,建议在一个cpp文件里只放#define CATCH_CONFIG_MAIN
和#include "catch.hpp"
。你也可以提供你自己实现的mian
函数(参考提供你自己的main函数)。 - 我们通过使用
TEST_CASE
宏来引入一个测试用例。这个宏有一个或者两个参数——一个是没有格式要求的测试用例的名字,另一个是可选的,一个或多个标签(更多细节请参考)。测试用例的名字必须是唯一的。你可以通过使用通配符来指定运行一系列的测试用例,这些通配符用来寻找名字匹配或者标签匹配的测试用例。你可以查阅命令行文档来获取更多关于运行测试用例的信息。 - 测试用例的名字和标签仅仅是字符串。我们还没有在任何一个声明一个函数或者方法——或者显示的注册一个测试用例。在这种情况下,我们会为你定义一个有生成的名字的函数,而且自动的使用静态的注册类来进行注册。通过将函数名称抽象化,我们能够摆脱没有标识符名称的限制。
- 我们有我们自己的测试断言宏——
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 );
}
}
对于每个SECTION
,TEST_CASE
是从头开始运行,所以我们知道vector
的大小是5
,容量至少是5
。我们在最外层使用REQUIRE
断言宏来强制vector
的大小为5
,容量至少为5
。SECTION
宏中包含了一个if
语句,这个if
语句是用来给Catch
来判断是否需要执行这个section
。每次运行一个TEST_CASE
,只跑一个叶子section
。其他的叶子section
都会被跳过。下次再运行下一个section
,直到所有的section
都被执行过。
到目前为止都还是好的。对于setup/teardown
方法,这是一个改善,因为,现在我们内联(inline
)了setup
代码,并且,使用了栈。
然而,当我们需要执行一系列的需要check的操作时,就可以见识到section
的威力。继续vector
的测试用例,我们可能需要验证reverse
操作不会减少vector
的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
可以任意地嵌套(可能受你的栈的限制)。每一个叶子section
(例如,不包含嵌套section的section)都会被执行一次,并且跟其他的叶子section
的执行路径是不同的(所以,叶子section
之间不会有相互影响)。父section
的失败会导致嵌套的section
不在执行,但这就是我们希望的。
BDD-风格
如果你能正确的命名你的测试用例和SECTION
,你可以实现一个BDD风格的规范结构。这是一种很有用的工作方式,已经被作为一级支持加入到了Catch中。可以使用宏SCENARIO
,GIVEN
,WHEN
,和THEN
来指定场景,这些场景会分别映射成TEST_CASE
和SECTION
。如果需要更详细的讲解,请参考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_CASE
和TEMPLATE_PRODUCT_TEST_CASE
。它们同TEST_CASE的用法类似,但是可以提供更多的参数。
更多的内容请参考test cases and sections。