白盒单元测试框架

举报
Jet Ding 发表于 2020/09/29 10:34:03 2020/09/29
【摘要】 当你开始开发新的库/类/程序时,你需要做的第一件事是什么?没错。你需要从单元测试模块开始。现在我们来看看C++ 白盒测试比较流行的单元测试框架。

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 <boost/test/included/unit_test.hpp>

n  编写一个测试案例:

BOOST_AUTO_TEST_CASE( your_test_case ) {

    std::vector<int> a{12};

    std::vector<int> b{12};

    BOOST_TEST( a == b );

}

 

n  编译运行

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

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

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

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

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

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

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

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

 

2.4    用法

 

2.4.1    简单测试

 

看一下简单的测试用例。

 

#include <my_class.hpp>

#define BOOST_TEST_MODULE MyTest

#include <boost/test/unit_test.hpp>

 

BOOST_AUTO_TEST_CASE( my_test )

{

    my_class test_object"qwerty" );

 

    BOOST_CHECKtest_object.is_valid() );

}

 

#define BOOST_TEST_MODULE MyTest

#include <boost/test/unit_test.hpp>

 

int addint iint j ) { return i+j; }

 

BOOST_AUTO_TEST_CASE( my_test )

{

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

    BOOST_CHECKadd2,2 ) == 4 );        // #1 出错后继续

 

    BOOST_REQUIREadd2,2 ) == 4 );      // #2 出错后抛出

 

    ifadd2,2 ) != 4 )

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

 

    ifadd2,2 ) != 4 )

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

 

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

 

    BOOST_CHECK_MESSAGEadd2,2 ) == 4,  // #6 出错后继续

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

 

    BOOST_CHECK_EQUALadd2,2 ), 4 );   // #7 出错后继续

}

 

2.4.2    Fixture

 

看一下测试用例Fixture

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

 

#define BOOST_TEST_MODULE example

#include <boost/test/included/unit_test.hpp>

 

struct F {

  F() : i0 ) { 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_TESTtrue );

}

 

2.4.3    Suite

 

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

 

#define BOOST_TEST_MODULE example

#include <boost/test/included/unit_test.hpp>

 

BOOST_AUTO_TEST_SUITE( test_suite1 )

 

BOOST_AUTO_TEST_CASE( test_case1 )

{

    BOOST_WARNsizeof(int) < 4 );

}

 

BOOST_AUTO_TEST_CASE( test_case2 )

{

    BOOST_REQUIRE_EQUAL12 );

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

}

 

BOOST_AUTO_TEST_CASE( test_case4 )

{

    BOOST_CHECKfalse );

}

 

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: 

  ComplexNumberTeststd::string name ) : CppUnit::TestCase( name ) {}

  

  void runTest() {

    CPPUNIT_ASSERTComplex (101) == Complex (101) );

    CPPUNIT_ASSERT( !(Complex (11) == Complex (22)) );

  }

};

 

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

    m_1_1 = new Complex11 );

    m_11_2 = new Complex112 );  

  }

 

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

    m_1_1 = new Complex11 );

    m_11_2 = new Complex112 );  

  }

 

  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<ComplexNumberTesttest"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.addTestnew CppUnit::TestCaller<ComplexNumberTest>(

                       "testEquality"

                       &ComplexNumberTest::testEquality ) );

suite.addTestnew CppUnit::TestCaller<ComplexNumberTest>(

                       "testAddition"

                       &ComplexNumberTest::testAddition ) );

suite.run( &result );

 

 

3.3.5    TestRunner

 

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

 

#include <cppunit/ui/text/TestRunner.h>

#include "ExampleTestCase.h"

#include "ComplexNumberTest.h"

 

int mainint argcchar **argv)

{

  CppUnit::TextUi::TestRunner runner;

  runner.addTestExampleTestCase::suite() );

  runner.addTestComplexNumberTest::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<int> q0_;

  Queue<int> q1_;

  Queue<int> q2_;

 

   TEST_F(QueueTestIsEmptyInitially)

    {

        EXPECT_EQ(q0_.size(), 0);

    }

 

    TEST_F(QueueTestDequeueWorks)

    {

        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的、只引入头文件的、用于单元测试、TDDBDD的测试框架,使用C++11C++14C++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++运算符被用于比较,但完整的表达式被分解,并且lhsrhs值被记录下来。

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

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

n  失败可以融入WindowsMac的调试器。

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 Factorialunsigned int number ) {

    return number <= 1 ? number : Factorial(number-1)*number;

}

 

TEST_CASE"Factorials are computed""[factorial]" ) {

    REQUIREFactorial(1) == 1 );

    REQUIREFactorial(2) == 2 );

    REQUIREFactorial(3) == 6 );

    REQUIREFactorial(10) == 3628800 );

}

 

5.4.2    测试案例和区域

 

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

 

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

 

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

 

    std::vector<intv5 );

 

    REQUIREv.size() == 5 );

    REQUIREv.capacity() >= 5 );

 

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

        v.resize10 );

 

        REQUIREv.size() == 10 );

        REQUIREv.capacity() >= 10 );

    }

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

        v.resize0 );

 

        REQUIREv.size() == 0 );

        REQUIREv.capacity() >= 5 );

    }

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

        v.reserve10 );

 

        REQUIREv.size() == 5 );

        REQUIREv.capacity() >= 10 );

    }

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

        v.reserve0 );

 

        REQUIREv.size() == 5 );

        REQUIREv.capacity() >= 5 );

    }

}

 

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

5.4.3    BDD-风格

 

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

 

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

 

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

        std::vector<intv5 );

 

        REQUIREv.size() == 5 );

        REQUIREv.capacity() >= 5 );

 

        WHEN"尺寸增加" ) {

            v.resize10 );

 

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

                REQUIREv.size() == 10 );

                REQUIREv.capacity() >= 10 );

            }

        }

        WHEN"尺寸减少了" ) {

            v.resize0 );

 

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

                REQUIREv.size() == 0 );

                REQUIREv.capacity() >= 5 );

            }

        }

        WHEN"增加容量" ) {

            v.reserve10 );

 

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

                REQUIREv.size() == 5 );

                REQUIREv.capacity() >= 10 );

            }

        }

        WHEN"减少容量" ) {

            v.reserve0 );

 

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

                REQUIREv.size() == 5 );

                REQUIREv.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/

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。