之前工作中做单元测试,一直用的是googletest,使用时只需要下载源码编译出库,在测试工程中引用即可,googletest提供了很多宏来让我们很轻松的组织测试用例。
前段时间公司让研究适合项目的单元测试框架,然后就发现了Catch2,Catch2比googletest使用更简单,源码就一个头文件,使用时只需要将Catch2.hpp引用到工程中即可,基本的测试用例组织方法与googletest相似;对于测试夹具,个人认为Catch提供的TEST_CASE+SECTION更简单。
下载头文件Catch2.hpp即可,不需要其他配置。
新建一个空工程,然后加入待测代码,这里是一个简单的计算器类calculator.h:
#ifndef CALCULATOR_H
#define CALCULATOR_H
class Calculator {
public:
int add(int a, int b) {
return a + b;
}
int minus(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int division(int a, int b) {
return a / b;
}
};
#endif // CALCULATOR_H
下面是简单的测试用例main.cpp:
#define CATCH_CONFIG_MAIN // 该宏用于生成Catch2的main函数
#include "../catch.hpp"
#include "calculator.h"
TEST_CASE("Test add function", "[calculator.add]") {
Calculator calc;
REQUIRE(calc.add(1, 2) == 3);
REQUIRE_FALSE(calc.add(1, 2) == 4);
}
可以看到测试框架部分仅:
#define CATCH_CONFIG_MAIN // 该宏用于生成Catch2的main函数
#include "../catch.hpp"
然后测试用例使用TEST_CASE宏组织:
TEST_CASE("Test add function", "[calculator.add]")
TEST_CASE宏的参数都是字符串,参数1是用例名称,参数2是标签名称(标签名称注意要使用[]括起来)。
注意:用例名称不能重复,标签名可以重复
测试用例中子逻辑使用REQUIRE宏进行判断。
REQUIRE(calc.add(1, 2) == 3);
这样,一个简单的测试用例就完成了,运行效果是这样:
halo@halo:build-test-unknown-Debug$ ./test
=================================================================
All tests passed (2 assertions in 1 test case)
所有用例通过,共一条用例,该条用例包含2条断言。
再看下用例不通过的情况:
halo@halo:build-test-unknown-Debug$ ./test
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test is a Catch v2.12.2 host application.
Run with -? for options
-------------------------------------------------------------------------------
Test add function
-------------------------------------------------------------------------------
../test/main.cpp:6
...............................................................................
../test/main.cpp:9: FAILED:
REQUIRE_FALSE( calc.add(1, 2) == 3 )
with expansion:
!(3 == 3)
================================================================
test cases: 1 | 1 failed
assertions: 2 | 1 passed | 1 failed
可以看出第二条用例失败了。
目前来看的话,基础的测试用例组织与googletest用法类似。
前面说过,SECTION可以完成测试夹具之类的工作,我们来看一个简单的例子:
TEST_CASE("Test multiply function", "[calculator.multiply]") {
Calculator calc;
std::cout << "init" << std::endl;
SECTION("hello") {
std::cout << "SECTION hello" << std::endl;
REQUIRE(calc.multiply(1, 2) == 2);
}
SECTION("world") {
std::cout << "SECTION world" << std::endl;
REQUIRE(calc.multiply(2, 2) == 4);
}
std::cout << "end" << std::endl;
}
运行结果:
halo@halo:build-test-unknown-Debug$ ./test
init
SECTION hello
end
init
SECTION world
end
====================================================
All tests passed (2 assertions in 1 test case)
我在代码里打印了一些标记,从标记的输出我们可以发现,每个SECTION都输出了init和end,说明他们是共享SECTION定义之外的代码,并且是独立运行的,执行顺序就是SECTION的定义顺序。使用时我们就要注意:对于SECTION之外的代码,仅是代码共享,运行时不相关,每一个SECTION都会运行一遍。
另外,SECTION是可以嵌套的,就是可以在SECTION里面再定义SECTION。
一般情况下,掌握基础用法和SECTION基本够用了。
Catch2还提供了一种BDD方式的用例组织方式,我们简单看下:
SCENARIO("I want to test", "Calculator") {
GIVEN("A calculator") {
Calculator calc;
WHEN("I add 1 and 2") {
int a = calc.add(1, 2);
THEN("the result must be correct") {
REQUIRE(a == 3);
}
}
}
}
我们简单白话一下:
场景:我想测试下计算器
给定一个计算器
--定义一个计算器
当计算1+2时
--使用计算器进行1+2计算
然后结果必须正确
--判断结果是否正确
大概这样子。
我们前面在写用例时,提到TEST_CASE有2个参数,在我们测试程序编好之后,我们可以通过命令行参数来控制运行哪部分用例。
运行指定名称的用例:
halo@halo:build-test-unknown-Debug$ ./test "Test add function"
Filters: Test add function
=================================================================
All tests passed (2 assertions in 1 test case)
运行指定tag的用例:
halo@halo:build-test-unknown-Debug$ ./test [calculator.add]
Filters: [calculator.add]
=================================================================
All tests passed (2 assertions in 1 test case)
在增量开发或者修改bug时并不需要跑完全部用例,那在我们提交代码时,可以带上tag,CI上根据tag去跑对应的用例,是不是看起来很有用~~
Catch2的环境配置和用例组织都相当简单,有需求的话大胆的使用吧,Catch2还有很多其他的特性,可以去github看看官方文档。