白盒单元测试框架

网友投稿 731 2022-05-30

1      引言

当你开始开发新的库/类/程序时,你需要做的第一件事是什么?

没错。你需要从单元测试模块开始。

现在我们来看看C++ 白盒测试比较流行的单元测试框架。

2      Boost.Test

Boost.Test是一个C++03/11/14/17单元测试库,可用于多种平台和编译器。

该库是Boost的一部分。该库的最新版本可以从boost网站上获得。

该库的完整使用说明可以从http://www.boost.org/doc/libs/release/libs/test/。

2.1    官方网站

http://www.boost.org

https://github.com/boostorg/test

2.2    许可

Boost Software License - Version 1.0

2.3    主要特点及使用方法

l  容易上手:

n  下载并使用boost库

n  用下面两行创建一个测试模块:

#define BOOST_TEST_MODULE your_test_module

#include 

n  编写一个测试案例:

BOOST_AUTO_TEST_CASE( your_test_case ) {

std::vector a{1, 2};

std::vector b{1, 2};

BOOST_TEST( a == b );

}

n  编译运行

l  强大而独特的测试断言宏BOOST_TEST,可用来检测浮点数、集合、字符串......并支持适当的比较范式。

l  自注册测试用例,在测试套件中组织用例,在测试用例、套件或全局上应用Fixture。

l  为故障的高级诊断提供断言。

l  强大且可扩展的数据集测试。

l  可为测试用例和套件添加高级描述、组/标签和依赖关系的装饰。

l  强大的命令行选项和测试案例过滤器。

l  为第三方工具提供可扩展的日志记录、XML和JUNIT输出(例如:连续集成)。

l  各种方法(如共享/静态库/头)用以加快集成和/或编译/构建周期,生成更小的二进制文件。

2.4    用法

2.4.1    简单测试

看一下简单的测试用例。

#include 

#define BOOST_TEST_MODULE MyTest

#include 

BOOST_AUTO_TEST_CASE( my_test )

{

my_class test_object( "qwerty" );

BOOST_CHECK( test_object.is_valid() );

}

#define BOOST_TEST_MODULE MyTest

#include 

int add( int i, int j ) { return i+j; }

BOOST_AUTO_TEST_CASE( my_test )

{

// 七种方法来检测和报告相同的错误:

BOOST_CHECK( add( 2,2 ) == 4 );        // #1 出错后继续

BOOST_REQUIRE( add( 2,2 ) == 4 );      // #2 出错后抛出

if( add( 2,2 ) != 4 )

BOOST_ERROR( "Ouch..." );            // #3 出错后继续

if( add( 2,2 ) != 4 )

BOOST_FAIL( "Ouch..." );             // #4 出错后抛出

if( add( 2,2 ) != 4 ) throw "Ouch..."; // #5 出错后抛出

BOOST_CHECK_MESSAGE( add( 2,2 ) == 4,  // #6 出错后继续

"add(..) result: " << add( 2,2 ) );

BOOST_CHECK_EQUAL( add( 2,2 ), 4 );   // #7 出错后继续

}

2.4.2    Fixture

看一下测试用例Fixture。

Fixture设置在测试用例执行前被调用,Fixture拆除在测试用例完成执行后被调用,与测试用例的执行状态无关。

#define BOOST_TEST_MODULE example

#include 

struct F {

F() : i( 0 ) { BOOST_TEST_MESSAGE( "setup fixture" ); }

~F()         { BOOST_TEST_MESSAGE( "teardown fixture" ); }

int i;

};

BOOST_FIXTURE_TEST_CASE( test_case1, F )

{

BOOST_TEST( i == 1 );

++i;

}

BOOST_FIXTURE_TEST_CASE( test_case2, F )

{

BOOST_CHECK_EQUAL( i, 1 );

}

BOOST_AUTO_TEST_CASE( test_case3 )

{

BOOST_TEST( true );

}

2.4.3    Suite

在测试套件开始和结束声明之间定义的测试单元成为测试Suite的成员。一个测试单元总是成为最近的测试Suite的成员。在测试文件范围内声明的测试单元会成为主测试Suite的成员。测试Suite包含的深度没有限制。

#define BOOST_TEST_MODULE example

#include 

BOOST_AUTO_TEST_SUITE( test_suite1 )

BOOST_AUTO_TEST_CASE( test_case1 )

{

BOOST_WARN( sizeof(int) < 4 );

}

BOOST_AUTO_TEST_CASE( test_case2 )

{

BOOST_REQUIRE_EQUAL( 1, 2 );

BOOST_FAIL( "Should never reach this line" );

}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE( test_suite2 )

BOOST_AUTO_TEST_CASE( test_case3 )

{

BOOST_CHECK( true );

}

BOOST_AUTO_TEST_CASE( test_case4 )

{

BOOST_CHECK( false );

}

BOOST_AUTO_TEST_SUITE_END()

3      cppunit

CppUnit是著名的JUnit框架的C++移植,用于单元测试。

测试输出为XML或文本格式,用于自动测试。

3.1    官方网站

http://cppunit.sourceforge.net/

3.2    许可

GNU Library or Lesser General Public License version 2.0 (LGPLv2)

3.3    用法

3.3.1    简单的测试

派生TestCase类。重写方法runTest()。当你想检查一个值时,调用CPPUNIT_ASSERT(bool),并传入一个表达式。

如果测试成功则为真。

class ComplexNumberTest : public CppUnit::TestCase {

public:

ComplexNumberTest( std::string name ) : CppUnit::TestCase( name ) {}

void runTest() {

CPPUNIT_ASSERT( Complex (10, 1) == Complex (10, 1) );

CPPUNIT_ASSERT( !(Complex (1, 1) == Complex (2, 2)) );

}

};

3.3.2    Fixture

Fixture是一组已知的对象,作为一组测试用例的基础。在开发过程中进行测试时,Fixture非常方便。

class ComplexNumberTest : public CppUnit::TestFixture {

private:

Complex *m_10_1, *m_1_1, *m_11_2;

public:

void setUp()

{

m_10_1 = new Complex( 10, 1 );

m_1_1 = new Complex( 1, 1 );

m_11_2 = new Complex( 11, 2 );

}

void tearDown()

{

delete m_10_1;

delete m_1_1;

delete m_11_2;

}

};

3.3.3    测试案例

如何使用Fixture编写和调用单个测试?

这个过程有两个步骤:

1.         把测试用例写成Fixture类的方法

2.         创建一个运行该方法的TestCaller。

class ComplexNumberTest : public CppUnit::TestFixture  {

private:

Complex *m_10_1, *m_1_1, *m_11_2;

public:

void setUp()

{

m_10_1 = new Complex( 10, 1 );

m_1_1 = new Complex( 1, 1 );

m_11_2 = new Complex( 11, 2 );

}

void tearDown()

{

delete m_10_1;

delete m_1_1;

delete m_11_2;

}

void testEquality()

{

CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );

CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );

}

void testAddition()

{

CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );

}

};

可以像这样为每个测试用例创建并运行实例:

CppUnit::TestCaller test( "testEquality",

&ComplexNumberTest::testEquality );

CppUnit::TestResult result;

test.run( &result );

测试调用器构造函数的第二个参数是ComplexNumberTest上一个方法的地址。当测试调用器运行时,该特定方法将被运行。

一旦你有几个测试,就把它们组织成一个Suite。

3.3.4    Suite

CppUnit提供了一个TestSuite类,可以一起运行任意数量的TestCases。

要创建一个由两个或多个测试组成的套件,请执行以下操作:

CppUnit::TestSuite suite;

CppUnit::TestResult result;

suite.addTest( new CppUnit::TestCaller(

"testEquality",

&ComplexNumberTest::testEquality ) );

suite.addTest( new CppUnit::TestCaller(

"testAddition",

&ComplexNumberTest::testAddition ) );

suite.run( &result );

3.3.5    TestRunner

一旦你有了一个测试Suite,你就会想要运行它。CppUnit 提供了一些工具来定义要运行的Suite并显示其结果。你可以通过一个静态方法Suite来使你的Suite可以被TestRunner程序访问,并返回一个测试Suite。

#include 

#include "ExampleTestCase.h"

#include "ComplexNumberTest.h"

int main( int argc, char **argv)

{

CppUnit::TextUi::TestRunner runner;

runner.addTest( ExampleTestCase::suite() );

runner.addTest( ComplexNumberTest::suite() );

runner.run();

return 0;

}

TestRunner将运行这些测试。如果所有的测试都通过了,你会得到一条信息性消息。如果有任何测试失败,你会得到以下信息:

n  失败的测试用例的名称

n  包含测试的源文件的名称。

n  发生故障的行号

n  调用CPPUNIT_ASSERT()检测到故障的所有文本。

CppUnit可以区分失败和错误。失败是预期的,并通过断言进行检查。错误是未预料到的问题,比如零除法和其他由C++运行时或你的代码引发的异常。

4      GoogleTest

谷歌测试和模拟框架。

4.1    官方网站

https://github.com/google/googletest/

4.2    许可

BSD-3-Clause License

4.3    主要特点

n  一个xUnit测试框架。

n  测试发现。

n  丰富的断言集。

n  用户定义的断言。

n  死亡测试。

n  致命和非致命的失败。

n  值参数化测试。

n  类型参数化测试。

n  运行测试的各种选项。

n  XML测试报告的生成。

4.4    用法

4.4.1    简单的测试

创建一个测试:

1.         使用TEST()宏来定义和命名一个测试函数。这些都是不返回值的普通C++函数。

2.         在这个函数中,连同你想包含的任何有效的C++语句,使用各种googletest断言来检查值。

3.         测试的结果是由断言决定的,如果测试中的任何断言失败(无论是致命的还是非致命的),或者测试崩溃,整个测试就会失败。否则,就会成功。

TEST(TestSuiteName, TestName) {

... 测试体 ...

}

假设我们有一个简单的函数:

int Factorial(int n); // 返回n的阶乘。

这个功能的测试Suite:

// 测试0的阶乘。

TEST(FactorialTest, HandlesZeroInput) {

EXPECT_EQ(Factorial(0), 1);

}

// 测试正数的阶乘。

TEST(FactorialTest, HandlesPositiveInput) {

EXPECT_EQ(Factorial(1), 1);

EXPECT_EQ(Factorial(2), 2);

EXPECT_EQ(Factorial(3), 6);

EXPECT_EQ(Factorial(8), 40320);

}

4.4.2    Fixture

要创建一个Fixture:

1.         从::testing::Test衍生出一个类。用protected:定义它的主体,因为我们要从子类中访问Fixture成员。

2.         在类的内部,声明计划使用的任何对象。

3.         如果有必要,写一个默认的构造函数或SetUp()函数来为每个测试准备对象。一个常见的错误是将SetUp()写成Setup(),并加上一个小u--在C++11中使用override来确保拼写正确。

4.         如果有必要,写一个析构器或TearDown()函数来释放你在SetUp()中分配的任何资源。

5.         如果需要,为你的测试定义子程序来共享。

当使用Fixture时,使用TEST_F()代替TEST(),允许访问测试Fixture中的对象和子程序。

TEST_F(TestFixtureName, TestName) {

... 测试体 ...

}

class QueueTest : public ::testing::Test {

protected:

void SetUp() override {

q1_.Enqueue(1);

q2_.Enqueue(2);

q2_.Enqueue(3);

}

// void TearDown() override {}

Queue q0_;

Queue q1_;

Queue q2_;

TEST_F(QueueTest, IsEmptyInitially)

{

EXPECT_EQ(q0_.size(), 0);

}

TEST_F(QueueTest, DequeueWorks)

{

int *n = q0_.Dequeue();

EXPECT_EQ(n, nullptr);

n = q1_.Dequeue();

ASSERT_NE(n, nullptr);

EXPECT_EQ(*n, 1);

EXPECT_EQ(q1_.size(), 0);

delete n;

n = q2_.Dequeue();

ASSERT_NE(n, nullptr);

EXPECT_EQ(*n, 2);

EXPECT_EQ(q2_.size(), 1);

delete n;

}

};

当这些测试运行时,会发生以下情况:

1.         googletest构建了一个QueueTest对象(我们称它为t1)。

2.         t1.SetUp() 初始化 t1。

3.         第一个测试 (IsEmptyInitially) 在 t1 上运行。

4.         t1.TearDown() 在测试结束后进行清理。

5.         t1被销毁。

6.         在另一个QueueTest对象上重复上述步骤,这次运行DequeueWorks测试。

5      Catch2

一个现代的、C++-native的、只引入头文件的、用于单元测试、TDD和BDD的测试框架,使用C++11、C++14、C++17和更高版本(或Catch1.x分支的C++03)。

Catch2是一个C++的多范式测试框架,也支持Objective-C(还有C)。它主要是作为一个单一的头文件发布的,尽管某些扩展可能需要额外的头文件。

5.1    官方网站

https://github.com/catchorg/Catch2

5.2    许可

白盒单元测试框架

Boost Software License 1.0

5.3    主要特点

n  快速且非常容易上手。只需下载catch.hpp,#include它就可以了。

n  没有外部依赖性。只要你能编译C++11,并且有一个C++标准库就可以使用。

n  可把测试用例写成:自注册的,函数或者方法。

n  将测试用例划分为若干部分,每个部分都是孤立运行的(不需要Fixture)。

n  使用BDD风格的Given-When-Then部分以及传统的单元测试用例。

n  只有一个核心断言宏用于比较。标准的C/C++运算符被用于比较,但完整的表达式被分解,并且lhs和rhs值被记录下来。

n  测试使用自由形式的字符串来命名,不再使用严格标识符来命名。

n  测试可以被标记,以方便运行特设的测试组。

n  失败可以融入Windows和Mac的调试器。

n  输出是通过模块化的报告对象。包括基本的文本和XML报告器。可以很容易地添加自定义报告器。

n  支持JUnit xml输出,以便与第三方工具集成,如CI服务器。

n  提供了一个默认的main()函数,但你可以提供你自己的函数来实现完全控制(例如,集成到你自己的测试运行器GUI中)。

n  提供了一个命令行解析器,你可以提供自己的main()函数。

n  Catch可以测试自己。

n  替代断言宏报告失败,但不中止测试用例。

n  浮点容差比较是通过使用一个表达式的Approx()语法建立起来的。

n  内部和友好的宏被隔离,因此可以管理名称冲突。

n  匹配器

5.4    用法

获取Catch2最简单的方法就是下载最新的单头版本。单一头文件是由一组单独的头文件合并生成的,但它仍然只是头文件中的普通源代码。

5.4.1    简单的测试

#define CATCH_CONFIG_MAIN  // 这告诉Catch提供一个main()--只在一个cpp文件中这样做

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

}

5.4.2    测试案例和区域

大多数测试框架都有一个基于类的Fixture机制。也就是测试用例映射到类上的方法,常见的设置和拆解可以在setup()和 teardown()方法中进行(或者在C++等支持的构造函数/析构函数)。

Catch采用了一种不同的方法(与NUnit和xUnit都不同),它更自然地适合C++和C语言家族。

TEST_CASE( "vector的尺寸和容量可以调整", "[vector]" ) {

std::vector v( 5 );

REQUIRE( v.size() == 5 );

REQUIRE( v.capacity() >= 5 );

SECTION( "调整尺寸和容量" ) {

v.resize( 10 );

REQUIRE( v.size() == 10 );

REQUIRE( v.capacity() >= 10 );

}

SECTION( "缩小尺寸但不改变容量" ) {

v.resize( 0 );

REQUIRE( v.size() == 0 );

REQUIRE( v.capacity() >= 5 );

}

SECTION( "增加容量但不改变尺寸" ) {

v.reserve( 10 );

REQUIRE( v.size() == 5 );

REQUIRE( v.capacity() >= 10 );

}

SECTION( "缩小尺寸或者容量" ) {

v.reserve( 0 );

REQUIRE( v.size() == 5 );

REQUIRE( v.capacity() >= 5 );

}

}

对于每个SECTION,从一开始就执行TEST_CASE, 所以当我们进入每个SECTION时,我们知道大小是5,容量至少是5。我们用顶层的REQUIREs强制执行了这些要求。

5.4.3    BDD-风格

如果你适当地命名你的测试用例和部分,你可以实现BDD式的规范结构。方案可以使用SCENARIO、GIVEN、WHEN和THEN宏来指定,这些宏分别映射到TEST_CASEs和SECTIONs。

SCENARIO( "vector的尺寸和容量可以调整", "[vector]" ) {

GIVEN( "含有一些项目的Vector" ) {

std::vector v( 5 );

REQUIRE( v.size() == 5 );

REQUIRE( v.capacity() >= 5 );

WHEN( "尺寸增加" ) {

v.resize( 10 );

THEN( "尺寸和容量发生变化" ) {

REQUIRE( v.size() == 10 );

REQUIRE( v.capacity() >= 10 );

}

}

WHEN( "尺寸减少了" ) {

v.resize( 0 );

THEN( "尺寸变化但是容量不变" ) {

REQUIRE( v.size() == 0 );

REQUIRE( v.capacity() >= 5 );

}

}

WHEN( "增加容量" ) {

v.reserve( 10 );

THEN( "增加容量但不增加尺寸" ) {

REQUIRE( v.size() == 5 );

REQUIRE( v.capacity() >= 10 );

}

}

WHEN( "减少容量" ) {

v.reserve( 0 );

THEN( "尺寸和容量都没有改变" ) {

REQUIRE( v.size() == 5 );

REQUIRE( v.capacity() >= 5 );

}

}

}

}

6      参考

https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#C++

https://www.boost.org/libs/test/

http://sourceforge.net/projects/cppunit/

https://github.com/google/googletest/

https://github.com/catchorg/Catch2

https://github.com/onqtam/doctest

https://github.com/martinmoene/lest

https://github.com/etr/liblittletest

https://github.com/mollismerx/elfspy

https://github.com/gammasoft71/xtd.tunit

http://banditcpp.github.io/bandit/

单元测试

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:第七章: C语言结构体(下)
下一篇:【第12篇】Sparse R-CNN: End-to-End Object Detection with Learnable P
相关文章