什么是单元测试?
复杂的C/C++代码很可能会包含错误,并且在编写代码后尝试对其进行测试类似于在大海捞针。 一种更审慎的方法是通过添加专门针对特定区域的小型(单元)测试来测试编写的各个代码段,例如,一些计算密集型C函数或某些C++类声称对某个数据结构进行建模,例如队列。 然后,以此理念构建的回归套件将具有单元测试的集合以及运行测试并报告结果的测试驱动程序。
为特定功能或类生成测试
对于诸如文本编辑器之类的复杂代码,外部测试人员无法生成针对特定例程的测试-测试人员对内部代码组织不会有太多想法。 在白盒测试中, Boost的使用非常方便:作为开发人员,您可以编写对类和函数进行语义检查的测试代码。 这个过程至关重要,因为您的代码的未来维护者一定会在某个时候篡改原始逻辑,并且一旦发生问题,单元测试就会失败。 通过使用白盒测试,通常无需使用调试器即可更轻松地了解问题所在。
考虑清单1中的简单字符串类。 该类不够强健,您可以使用Boost对其进行测试。
清单1.一个没有启发性的字符串类
#ifndef _MYSTRING
#define _MYSTRING
class mystring {
char* buffer;
int length;
public:
void setbuffer(char* s) { buffer = s; length = strlen(s); }
char& operator[ ] (const int index) { return buffer[index]; }
int size( ) { return length; }
};
#endif
一些典型的与字符串相关的检查将验证空字符串的长度是否为0,访问索引外导致错误消息或异常等等。 清单2显示了一些值得为任何字符串实现创建的测试。 要运行清单2中的源代码,您只需使用g ++(或任何其他符合标准的C++编译器)对其进行编译。 请注意,不需要单独的主要功能,代码也不使用任何链接库:Boost安装中的unit_test.hpp标头包含所有必需的定义。
清单2.字符串类的单元测试
#define BOOST_TEST_MODULE stringtest
#include <boost/test/included/unit_test.hpp>
#include "./str.h"
BOOST_AUTO_TEST_SUITE (stringtest) // name of the test suite is stringtest
BOOST_AUTO_TEST_CASE (test1)
{
mystring s;
BOOST_CHECK(s.size() == 0);
}
BOOST_AUTO_TEST_CASE (test2)
{
mystring s;
s.setbuffer("hello world");
BOOST_REQUIRE_EQUAL ('h', s[0]); // basic test
}
BOOST_AUTO_TEST_SUITE_END( )
BOOST_AUTO_TEST_SUITE和BOOST_AUTO_TEST_SUITE_END宏分别指示测试套件的开始和结束。 各个测试位于这些宏之间,从某种意义上说,它们的语义就像C++命名空间。 每个单独的单元测试都是使用BOOST_AUTO_TEST_CASE宏定义的。 清单3显示了清单2中的代码输出。
清单3.清单2中的代码输出
[arpan@tintin] ./a.out
Running 2 test cases...
test.cpp(10): error in "test1": check s.size() == 0 failed
*** 1 failure detected in test suite "stringtest"
让我们详细研究一下先前清单中单元测试的创建。 基本思想是使用Boost提供的宏来测试各个类的功能。 BOOST_CHECK和BOOST_REQUIRE_EQUAL是Boost提供的用于验证代码输出的一些预定义宏(也称为测试工具 )。
Boost具有大量的测试工具,它们基本上是用于验证表达式的宏。 测试工具的三个主要类别是BOOST_WARN , BOOST_CHECK和BOOST_REQUIRE 。 BOOST_CHECK和BOOST_REQUIRE之间的区别在于,在前一种情况下,即使断言失败,测试仍将继续,而在后一种情况下,则认为是严重错误并且测试将停止。 清单4使用一个简单的C++代码片段来了解这些工具类别之间的区别。
清单4.使用Boost测试工具的三个变体
#define BOOST_TEST_MODULE enumtest
#include <boost/test/included/unit_test.hpp>
BOOST_AUTO_TEST_SUITE (enum-test)
BOOST_AUTO_TEST_CASE (test1)
{
typedef enum {red = 8, blue, green = 1, yellow, black } color;
color c = green;
BOOST_WARN(sizeof(green) > sizeof(char));
BOOST_CHECK(c == 2);
BOOST_REQUIRE(yellow > red);
BOOST_CHECK(black != 4);
}
BOOST_AUTO_TEST_SUITE_END( )
第一个BOOST_CHECK失败,第一个BOOST_REQUIRE也失败。 但是,由于BOOST_REQUIRE失败时代码将退出,因此第二个BOOST_CHECK尚未到达。 清单5显示了清单4代码的输出。
清单5.了解BOOST_REQUIRE和BOOST_CHECK之间的区别
[arpan@tintin] ./a.out
Running 1 test case...
e2.cpp(11): error in "test1": check c == 2 failed
e2.cpp(12): fatal error in "test1": critical check yellow > red failed
*** 2 failures detected in test suite "enumtest"
在类似的行上,如果您需要检查一些个别的函数或类方法是否存在极端情况,那么最简单的方法是创建一个新测试,并使用参数和期望值调用该例程。 清单6提供了一个示例。
清单6.使用Boost测试来检查函数和类方法
BOOST_AUTO_TEST(functionTest1)
{
BOOST_REQUIRE(myfunc1(99, ‘A’, 6.2) == 12);
myClass o1(“hello world!\n”);
BOOST_REQUIRE(o1.memoryNeeded( ) < 16);
}
模式匹配
通常针对“黄金日志”测试某些函数生成的输出。 BOOST_CHECK在这里也很方便,您还需要使用Boost库的output_test_stream类。 用黄金日志文件(以下示例中的run.log)初始化output_test_stream 。 C/C++函数的输出被馈送到该output_test_stream对象,然后调用该对象的match_pattern例程。 清单7提供了详细信息。
清单7.针对黄金日志文件的模式匹配
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
#include <boost/test/output_test_stream.hpp>
using boost::test_tools::output_test_stream;
BOOST_AUTO_TEST_SUITE ( test )
BOOST_AUTO_TEST_CASE( test )
{
output_test_stream output( "run.log", true );
output << predefined_user_func( );
BOOST_CHECK( output.match_pattern() );
}
BOOST_AUTO_TEST_SUITE_END( )
浮点比较
回归设置中最棘手的检查之一是进行浮点比较。 看一下清单8中的代码,一切似乎都进行得很好-至少从表面上看。
清单8.无效的浮点比较
#define BOOST_TEST_MODULE floatingTest
#include <boost/test/included/unit_test.hpp>
#include <cmath>
BOOST_AUTO_TEST_SUITE ( test )
BOOST_AUTO_TEST_CASE( test )
{
float f1 = 567.0102;
float result = sqrt(f1); // this could be my_sqrt; faster implementation
// for some specific DSP like hardware
BOOST_CHECK(f1 == result * result);
}
BOOST_AUTO_TEST_SUITE_END( )
在运行此测试时,尽管您正在使用作为标准库的一部分提供的sqrt函数,但BOOST_CHECK宏仍然失败。 那么怎么了? 浮点比较的问题是精度问题f1和result*result开始的result*result与小数点后的几个位置不同。 为了解决这种情况,Boost测试实用程序提供了BOOST_WARN_CLOSE_FRACTION , BOOST_CHECK_CLOSE_FRACTION和BOOST_REQUIRE_CLOSE_FRACTION宏。 要使用这三个宏中的任何一个,必须包含预定义的Boost头float_point_comparison.hpp。 这三个宏的语法都相同,因此本文仅讨论check变体(请参见清单9 )。
清单9. BOOST_CHECK_CLOSE_FRACTION宏的语法
BOOST_CHECK_CLOSE_FRACTION (left-value, right-value, tolerance-limit);
而不是使用的BOOST_CHECK在上市9 ,尝试BOOST_CHECK_CLOSE_FRACTION与公差限制为0.0001。 清单10显示了代码的外观。
清单10.有效的浮点比较
#define BOOST_TEST_MODULE floatingTest
#include <boost/test/included/unit_test.hpp>
#include <boost/test/floating_point_comparison.hpp>
#include <cmath>
BOOST_AUTO_TEST_SUITE ( test )
BOOST_AUTO_TEST_CASE( test )
{
float f1 = 567.01012;
float result = sqrt(f1); // this could be my_sqrt; faster implementation
// for some specific DSP like hardware
BOOST_CHECK_CLOSE_FRACTION (f1, result * result, 0.0001);
}
BOOST_AUTO_TEST_SUITE_END( )
这段代码运行正常。 现在,将清单10中的公差极限替换为0.0000001。 清单11显示了输出。
清单11.一个示例,该示例由于公差限制超出可接受范围而失败
[arpan@tintin] ./a.out
Running 1 test case...
sq.cpp(18): error in "test": difference between f1{567.010132} and
result * result{567.010193} exceeds 1e-07
*** 1 failure detected in test suite "floatingTest"
在生产软件中不断重复出现的另一个常见(恶性)问题是double和float类型的变量比较。 BOOST_CHECK_CLOSE_FRACTION一个不错的功能是它不允许您进行这种比较。 宏中的左值和右值必须是同一类型float或double 。 在清单12中 ,如果f1是一个double并result一个float,则在编译期间会看到一个错误。
清单12.错误:BOOST_CHECK_CLOSE_FRACTION的左右参数类型不同
[arpan@tintin] g++ sq.cpp -I/u/c/lib/boost
/u/c/lib/boost/boost/test/test_tools.hpp:
In function
`bool boost::test_tools::tt_detail::check_frwd(Pred,
const boost::unit_test::lazy_ostream&,
boost::test_tools::const_string, size_t,
boost::test_tools::tt_detail::tool_level,
boost::test_tools::tt_detail::check_type,
const Arg0&, const char*,
const Arg1&, const char*, const Arg2&, const char*)
[with Pred = boost::test_tools::check_is_close_t, Arg0 = double,
Arg1 = float, Arg2 = boost::test_tools::fraction_tolerance_t<double>]':
sq.cpp:18: instantiated from here
/u/c/lib/boost/boost/test/test_tools.hpp:523: error: no match for call to
`(boost::test_tools::check_is_close_t) (const double&, const float&,
const boost::test_tools::fraction_tolerance_t<double>&)'
定制谓词支持
Boost测试工具验证布尔条件。 您可以扩充测试工具以支持更复杂的检查-例如,确定两个列表的内容是否相同或某个条件在向量的所有元素上是否有效。 您还可以扩展BOOST_CHECK宏以执行自定义谓词支持。 让我们对由用户定义的C函数生成的列表的内容执行自定义检查:检查结果是否具有大于1的所有元素。自定义检查函数需要返回类型boost::test_tools::predicate_result 。 清单13显示了详细信息。
清单13.使用Boost测试工具验证复杂谓词
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
boost::test_tools::predicate_result validate_list(std::list<int>& L1)
{
std::list<int>::iterator it1 = L1.begin( );
for (; it1 != L1.end( ); ++it1)
{
if (*it1 <= 1) return false;
}
return true;
}
BOOST_AUTO_TEST_SUITE ( test )
BOOST_AUTO_TEST_CASE( test )
{
std::list<int>& list1 = user_defined_func( );
BOOST_CHECK( validate_list(list1) );
}
BOOST_AUTO_TEST_SUITE_END( )
predicate_result对象具有一个接受布尔值的隐式构造函数,这说明了即使validate_list的预期返回类型和实际返回类型不同,代码仍可以正常工作的原因。
还有另一种可以通过Boost测试复杂谓词的方法: BOOST_CHECK_PREDICATE宏。 使用此宏的好处在于,它不使用predicate_result 。 另一方面,语法有点粗糙。 用户需要将函数名称和参数传递给BOOST_CHECK_PREDICATE宏。 清单14具有与清单13相同的功能,只是使用了不同的宏。 注意, validate_result的返回类型现在为Boolean。
清单14. BOOST_CHECK_PREDICATE宏
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
bool validate_list(std::list<int>& L1)
{
std::list<int>::iterator it1 = L1.begin( );
for (; it1 != L1.end( ); ++it1)
{
if (*it1 <= 1) return false;
}
return true;
}
BOOST_AUTO_TEST_SUITE ( test )
BOOST_AUTO_TEST_CASE( test )
{
std::list<int>& list1 = user_defined_func( );
BOOST_CHECK_PREDICATE( validate_list, list1 );
}
BOOST_AUTO_TEST_SUITE_END( )
一个文件中有多个测试套件
可以在单个文件中包含多个测试套件。 每个测试套件必须在文件中定义一对BOOST_AUTO_TEST_SUITE... BOOST_AUTO_TEST_SUITE_END宏。 清单15显示了在同一文件中定义的两个不同的测试套件。 运行回归时,请使用预定义的–log_level=test_suite选项运行可执行文件。 从清单16中可以看到,使用此选项生成的输出更加冗长,可以进行快速调试。
清单15.在一个文件中使用多个测试套件
#define BOOST_TEST_MODULE Regression
#include <boost/test/included/unit_test.hpp>
typedef struct {
int c;
char d;
double e;
bool f;
} Node;
typedef union {
int c;
char d;
double e;
bool f;
} Node2;
BOOST_AUTO_TEST_SUITE(Structure)
BOOST_AUTO_TEST_CASE(Test1)
{
Node n;
BOOST_CHECK(sizeof(n) < 12);
}
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE(Union)
BOOST_AUTO_TEST_CASE(Test1)
{
Node2 n;
BOOST_CHECK(sizeof(n) == sizeof(double));
}
BOOST_AUTO_TEST_SUITE_END()
这是清单15中代码的输出:
清单16.使用–log_level选项运行多个测试套件
[arpan@tintin] ./a.out --log_level=test_suite
Running 2 test cases...
Entering test suite "Regression"
Entering test suite "Structure"
Entering test case "Test1"
m2.cpp(23): error in "Test1": check sizeof(n) < 12 failed
Leaving test case "Test1"
Leaving test suite "Structure"
Entering test suite "Union"
Entering test case "Test1"
Leaving test case "Test1"
Leaving test suite "Union"
Leaving test suite "Regression"
*** 1 failure detected in test suite "Regression"
了解测试套件的组织
到目前为止,本文已经讨论了带有无层次结构的测试套件的Boost测试实用程序。 现在,让我们尝试使用Boost创建一个测试套件,该套件以外部工具用户通常认为完成的方式测试软件产品。 在测试框架本身中,通常会有多个套件,每个套件都检查某些产品功能。 例如,文字处理器的回归框架应具有检查字体支持,不同文件格式等的套件。 每个单独的测试套件将具有多个单元测试。 清单17提供了一个测试框架的示例。 请注意,代码的入口点必须是(适当地)名为init_unit_test_suite的例程。
清单17.创建用于运行回归的主测试套件
#define BOOST_TEST_MODULE MasterTestSuite
#include <boost/test/included/unit_test.hpp>
using boost::unit_test;
test_suite*
init_unit_test_suite( int argc, char* argv[] )
{
test_suite* ts1 = BOOST_TEST_SUITE( "test_suite1" );
ts1->add( BOOST_TEST_CASE( &test_case1 ) );
ts1->add( BOOST_TEST_CASE( &test_case2 ) );
test_suite* ts2 = BOOST_TEST_SUITE( "test_suite2" );
ts2->add( BOOST_TEST_CASE( &test_case3 ) );
ts2->add( BOOST_TEST_CASE( &test_case4 ) );
framework::master_test_suite().add( ts1 );
framework::master_test_suite().add( ts2 );
return 0;
}
每个测试套件(例如, 清单17中的 ts1 )都是使用宏BOOST_TEST_SUITE创建的。 宏需要一个字符串,它是测试套件的名称。 最终所有的测试套件都使用add方法添加到主测试套件中。 同样,您可以使用宏BOOST_TEST_CASE创建每个测试,然后再次使用add方法将每个测试添加到测试套件中。 您也可以将单元测试添加到主测试套件中,尽管不建议这样做。 master_test_suite方法定义为boost::unit_test::framework命名空间的一部分:它在内部实现单例。 清单18中的代码来自Boost源本身,解释了它是如何工作的。
清单18.了解master_test_suite方法
master_test_suite_t&
master_test_suite()
{
if( !s_frk_impl().m_master_test_suite )
s_frk_impl().m_master_test_suite = new master_test_suite_t;
return *s_frk_impl().m_master_test_suite;
}
使用BOOST_TEST_CASE宏创建的单元测试接受函数指针作为其输入参数。 因此,在清单17中 , test_case1 , test_case2等都是无效函数,用户可以自由地按自己喜欢的方式进行编码。 但是请注意,Boost测试设置在堆上使用了相当多的内存。 每次对BOOST_TEST_SUITE调用BOOST_TEST_SUITE归结为一个新的boost::unit_test::test_suite(<test suite name>) 。
治具
从概念上讲, 测试固定装置旨在在执行测试之前设置环境,并在测试完成后进行清理。 清单19提供了一个简单的示例。
清单19.基本的Boost固定装置
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
#include <iostream>
struct F {
F() : i( 0 ) { std::cout << "setup" << std::endl; }
~F() { std::cout << "teardown" << std::endl; }
int i;
};
BOOST_AUTO_TEST_SUITE( test )
BOOST_FIXTURE_TEST_CASE( test_case1, F )
{
BOOST_CHECK( i == 1 );
++i;
}
BOOST_AUTO_TEST_SUITE_END()
清单20显示了输出。
清单20. Boost夹具使用情况的输出
[arpan@tintin] ./a.out
Running 1 test case...
setup
fix.cpp(16): error in "test_case1": check i == 1 failed
teardown
*** 1 failure detected in test suite "example"
此代码不使用BOOST_AUTO_TEST_CASE宏,而是使用BOOST_FIXTURE_TEST_CASE ,它带有一个附加参数。 此对象的constructor方法和destructor方法进行必要的设置和清除。 窥探升压头unit_test_suite.hpp可以确认这一点(请参见清单21 )。
清单21.来自标头unit_test_suite.hpp的Boost夹具定义
#define BOOST_FIXTURE_TEST_CASE( test_name, F ) \
struct test_name : public F { void test_method(); }; \
\
static void BOOST_AUTO_TC_INVOKER( test_name )() \
{ \
test_name t; \
t.test_method(); \
} \
\
struct BOOST_AUTO_TC_UNIQUE_ID( test_name ) {}; \
\
BOOST_AUTO_TU_REGISTRAR( test_name )( \
boost::unit_test::make_test_case( \
&BOOST_AUTO_TC_INVOKER( test_name ), #test_name ), \
boost::unit_test::ut_detail::auto_tc_exp_fail< \
BOOST_AUTO_TC_UNIQUE_ID( test_name )>::instance()->value() ); \
\
void test_name::test_method() \
在内部,Boost公开从struct F派生一个类(请参见清单19 ),然后从该类中创建一个对象。 根据C++的公共继承规则,可在随后的函数中直接访问struct类的所有受保护变量和公共变量。 注意,在清单19中 ,要修改的变量i是属于F类型内部对象t的变量(请参见清单20 )。 拥有一套回归套件是完全可以的,在该套件中,只有几个测试需要某种显式初始化,因此可以使用fixture功能。 清单22有一个测试套件,其中只有三分之二的测试在使用夹具。
清单22.结合了夹具测试和非夹具测试的Boost测试套件
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
#include <iostream>
struct F {
F() : i( 0 ) { std::cout << "setup" << std::endl; }
~F() { std::cout << "teardown" << std::endl; }
int i;
};
BOOST_AUTO_TEST_SUITE( test )
BOOST_FIXTURE_TEST_CASE( test_case1, F )
{
BOOST_CHECK( i == 1 );
++i;
}
BOOST_AUTO_TEST_CASE( test_case2 )
{
BOOST_REQUIRE( 2 > 1 );
}
BOOST_AUTO_TEST_CASE( test_case3 )
{
int i = 1;
BOOST_CHECK_EQUAL( i, 1 );
++i;
}
BOOST_AUTO_TEST_SUITE_END()
清单22具有在单个测试用例上定义和使用的固定装置。 Boost还允许用户通过宏BOOST_GLOBAL_FIXTURE (<Fixture Name>)定义和使用全局装置。 您可以定义任意数量的全局固定装置,从而可以拆分初始化代码。 清单23使用了全局夹具。
清单23.使用全局夹具初始化回归
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
#include <iostream>
struct F {
F() { std::cout << "setup" << std::endl; }
~F() { std::cout << "teardown" << std::endl; }
};
BOOST_AUTO_TEST_SUITE( test )
BOOST_GLOBAL_FIXTURE( F );
BOOST_AUTO_TEST_CASE( test_case1 )
{
BOOST_CHECK( true );
}
BOOST_AUTO_TEST_SUITE_END()
对于多个灯具,安装和拆卸按照您声明它们的顺序进行。 在清单24中 , F的构造函数在F2之前调用; 同样对于破坏者。
清单24.在回归中使用多个全局夹具
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
#include <iostream>
struct F {
F() { std::cout << "setup" << std::endl; }
~F() { std::cout << "teardown" << std::endl; }
};
struct F2 {
F2() { std::cout << "setup 2" << std::endl; }
~F2() { std::cout << "teardown 2" << std::endl; }
};
BOOST_AUTO_TEST_SUITE( test )
BOOST_GLOBAL_FIXTURE( F );
BOOST_GLOBAL_FIXTURE( F2 );
BOOST_AUTO_TEST_CASE( test_case1 )
{
BOOST_CHECK( true );
}
BOOST_AUTO_TEST_SUITE_END()
请注意,您不能将全局夹具用作单个测试中的对象。 您也不能在测试中直接访问其公共/受保护的非静态方法或变量。
结论
而已。 本文向您介绍了最强大的开源回归框架之一:Boost。 您了解了基本的Boost检查,模式匹配,浮点比较,自定义检查,测试套件组织(手动和自动)以及固定装置。 请务必查看Boost文档以获取更多信息。 本系列的其他文章将介绍其他开源回归框架,例如cppUnit。
翻译自: https://www.ibm.com/developerworks/aix/library/au-ctools1_boost/index.html