您的位置:首页 > 编程语言 > C语言/C++

Google C++测试框架系列高级篇:第一章 更多关于断言的知识

2013-11-21 11:48 561 查看
原始链接:More Assertions

词汇表

现在你应该已经读完了入门篇并且会使用GTest来写测试。是时候来学一些新把戏了。这篇文档将教会你更多知识:用断言构造复杂的失败信息,传递致命失败,重用和加速你的test fixtures,以及在你的测试中使用不同的标志位。

版本号:v_0.1

更多关于断言的知识

1. 明确的声明成功或失败

2. 异常断言

3. 通过谓词断言提供更好的错误信息

3.1 使用已有的bool类型函数

3.2 使用返回AssertionResult对象的函数

3.3 使用格式化谓词

4. 浮点数比较

4.1 宏

4.2 浮点数格式化谓词函数

5. Windows平台处理HRESULT的断言

6. 类型断言

7. 断言的放置

这一章节将覆盖一些较少被使用,但非常重要的关于断言的知识。

1. 明确的声明成功或失败

以下我们提到的几个断言实际上并不对值或表达式进行测试。取而代之的是它们直接产生一个通过或失败。像大多数做做测试的宏一样,我们可以定制失败信息并且流定向到它们。

SUCCESS()直接产生一个通过。但这并不表明整个测试通过。仅当所有断言都通过这个测试才被认为是通过。

注:这个宏目前只存在于文档中,是一个空操作,不会产生任何用户可见的输出。但是以后我们会考虑添加有关信息。

FAIL();ADD_FAILURE();ADD_FAILURE_AT("file_path", line_number);
FAIL()产生一个致命失败,而ADD_FAILURE和ADD_FAILURE_AT生成非致命失败。在类似于switch-case的控制流程中决定测试是否通过,用这些宏比用bool表达式更直接方便。例如以下代码:

switch(expression) {
case 1: ... some checks ...
case 2: ... some other checks
...
default: FAIL() << "We shouldn't get here.";
}


在Linux,Windows和Mac上可用。

2. 异常断言

以下断言用来判断代码是否抛出异常。

致命断言非致命断言通过条件
ASSERT_THROW(statement, exception_type);EXPECT_THROW(statement, exception_type);抛出指定类型的异常
ASSERT_ANY_THROW(statement);EXPECT__ANY_THROW(statement);抛出任意类型的异常
ASSERT_NO_THROW(statement);EXPECT_NO_THROW(statement);没有抛出异常
例子:

ASSERT_THROW(Foo(5), bar_exception);

EXPECT_NO_THROW({
int n = 5;
Bar(&n);
});


在Linux,Windows和Mac上可用。

3. 通过谓词断言提供更好的错误信息

尽管GTest提供了丰富的断言,但还是不能覆盖我们在现实中所能遇到的场景,而且也不可能预见所有的情况(这不是一个好主意)。有时用户想使用EXPECT_TRUE来测试一个复杂的表达式,但是却没有合适的宏。这使得你无法显示表达式某一部分的值,当出错之后你很难找到问题到底出在哪里。某些用户选择自己构造失败信息,然后流定向到EXPECT_TRUE()。但这么做问题非常多,特别是当这个表达式有副作用或者开销巨大。

GTest提供了三个选项来解决这个问题。

3.1 使用已有的bool类型函数

如果你的函数或函数体返回bool类型(或者可以被隐式的转换为bool类型),在谓词断言中使用这些函数的话,参数将自动被打印。

致命断言非致命断言通过条件
ASSERT_PRED1(pred1, val1);EXPECT_PRED1(pred1, val1);pred1(val1)返回true
ASSERT_PRED2(pred1, val1, val2);EXPECT_PRED2(pred1, val1, val2);pred2(val1, val2)返回true
.........
在上表中,predn是一个有n个参数的谓词函数或函数体,val1, val2, ...直到valn代表它的参数。断言当谓词判断在给定参数返回true的情况下通过,不然失败。当断言失败后,它就会打印每一个参数。不管通过还是失败,这些参数的值只会被计算一次。

请看以下例子:

// Returns true if m and n have no common divisors except 1.
bool MutuallyPrime(int m, int n) { ... }
const int a = 3;
const int b = 4;
const int c = 10;


断言EXPECT_PRED2(MutuallyPrime, a, b)会通过,而断言EXPECT_PRED2(MutuallyPrime, b, c)会失败并打印以下信息:

!MutuallyPrime(b, c) is false, where

b is 4

c is 10

注:

当使用ASSERT_PRED*或EXPECT_PRED*遇到编译错误"no matching function to call"时,请参考这篇文章如何解决。

当前我们支持的参数数量小于等于5。如果你希望支持更多的参数,请让我们知道。

在Linux,Windows和Mac上可用。

3.2 使用返回AssertionResult对象的函数

虽然EXPECT_PRED*和它的小伙伴们用起来很方便,但是语法却不能让人满意。对于不同数量的参数你必须使用不同的宏,这看上去更向Lisp而不是C++。现在::testing::AssertionResult来帮助你解决这个问题。

一个AssertionResult对象代表一次断言的结果(通过或失败,以及相关的信息)。你可以用以下任意一个工厂函数来创建一个AssertionResult对象。

namespace testing {

// Returns an AssertionResult object to indicate that an assertion has
// succeeded.
AssertionResult AssertionSuccess();

// Returns an AssertionResult object to indicate that an assertion has
// failed.
AssertionResult AssertionFailure();

}


你可以使用"<<"操作符把信息流定向到AssertionResult对象。

为了在bool断言(例如EXPECT_TRUE())中提供可读性更好的信息,你可以实现一个返回AssertionResult的函数而不是仅仅返回bool类型。例如,你可以这样定义IsEven()函数:

::testing::AssertionResult IsEven(int n) {
if ((n % 2) == 0)
return ::testing::AssertionSuccess();
else
return ::testing::AssertionFailure() << n << " is odd";
}


而不是原来返回bool类型的:

bool IsEven(int n) {
return (n % 2) == 0;
}


断言EXPECT_TRUE(IsEven(Fib(4)))会失败并且打印以下信息:

Value of: !IsEven(Fib(4))

Actual: false (*3 is odd*)

Expected: true

如果用原来返回bool类型的函数,打印信息就比较简单:

Value of: !IsEven(Fib(4))

Actual: false

Expected: true

如果你希望EXPECT_FALSE和ASSERT_FALSE也能提供这样有意义的信息,并且不在乎谓词执行效率的话,再多打加一条信息好了:

::testing::AssertionResult IsEven(int n) {
if ((n % 2) == 0)
return ::testing::AssertionSuccess() << n << " is even";
else
return ::testing::AssertionFailure() << n << " is odd";
}


现在调用EXPECT_FALSE(IsEven(Fib(6)))将打印以下信息:

Value of: !IsEven(Fib(Fib(6)))

Actual: true (8 is even)

Expected: false

在Linux,Windows和Mac上可用。

3.3 使用格式化谓词

如果你发现(ASSERT|EXPECT)_PRED*和(ASSERT|EXPECT)_(TRUE|FALSE)提供的默认信息不能满足要求,或者谓词的某些参数不能被流定向到ostream,请使用格式化谓词断言来完全定制信息:

致命断言非致命断言通过条件
ASSERT_PRED_FORMAT1(pred_format1, val1);EXPECT_PRED1(pred_format1, val1);pred_format1(val1)通过
ASSERT_PRED_FPRMAT22(pred_format2, val1, val2);EXPECT_PRED2(pred_format2, val1, val2);pred_format2(val1, val2)通过
.........
与前面提到的两组谓词不同,(ASSERT|EXPECT)_PRED_FORMAT*提供了一个谓词格式化程序(pred_formatn),它是以下形式的函数或函数体:

::testing::AssertionResult PredicateFormattern(const char* expr1, const char* expr2, ... const char* exprn, T1 val1, T2 val2, ... Tn valn);


其中val1,val2,...直到valn表示谓词的参数,而expr1,expr2,...直到exprn是参数valn显示在代码中的形式(如果第n个参数我们使用变量valn,那么exprn对应的值就是"valn")。类型T1,T2,...直到Tn可以是任意值类型或引用类型。例如,某个参数的类型是Foo,你可以根据需要声明为Foo或者const Foo&。

格式化谓词返回一个::testing::AssertionResult对象表示断言通过或失败。

下面我们将展示如何基于前面使用EXPECT_PRED2()的例子改进失败信息。

// Returns the smallest prime common divisor of m and n,
// or 1 when m and n are mutually prime.
int SmallestPrimeCommonDivisor(int m, int n) { ... }

// A predicate-formatter for asserting that two integers are mutually prime.
::testing::AssertionResult AssertMutuallyPrime(const char* m_expr,
const char* n_expr,
int m,
int n) {
if (MutuallyPrime(m, n))
return ::testing::AssertionSuccess();

return ::testing::AssertionFailure()
<< m_expr << " and " << n_expr << " (" << m << " and " << n
<< ") are not mutually prime, " << "as they have a common divisor "
<< SmallestPrimeCommonDivisor(m, n);
}


使用格式化谓词:

EXPECT_PRED_FORMAT2(AssertMutuallyPrime, b, c);


我们可以获得以下信息

b and c (4 and 10) are not mutually prime, as they have a common divisor 2.

现在可能你已经意识到了,之前我们介绍的大多数断言是(EXPECT|ASSERT)_PRED_FORMAT*的特例。事实上也确实如此,大多数断言的实现依赖于(EXPECT|ASSERT)_PRED_FORMAT*。

在Linux,Windows和Mac上可用。

4. 浮点数比较

浮点数比较的坑比较多。因为四舍五入的关系,很难精确的比较两个浮点数。所以我们常用的ASSERT_EQ通常就不能工作了。因为浮点数的范围之大,没有一个固定的误差范围可以适应所有情况。除非要比较的值因为四舍五入非常接近0,用一个固定误差范围来做比较还是一个不错的选择。

通常你必须手工指定一个误差范围来做浮点数比较。如果你嫌麻烦也不要紧,使用默认的ULPs(Units in the Last Place)也能得到足够好的效果,GTest提供基于它实现的断言。关于ULPS的详细信息请查询这篇文章。

4.1 宏

致命断言非致命断言通过条件
ASSERT_FLOAT_EQ(expected, actual);EXPECT_FLOAT_EQexpected, actual);两个浮点数基本相等
ASSERT_DOUBLE_EQ(expected, actual);EXPECT_DOUBLE_EQexpected, actual);两个双精度浮点数基本相等
“基本相等”表示两个数的差距在4个ULPs以内。

以下断言允许你手工指定可接受的误差范围:

致命断言非致命断言通过条件
ASSERT_NEAR(val1, val2, abs_error);EXPECT_NEAR(val1, val2, abs_error);两个数差的绝对值不超过给定的绝对误差
在Linux,Windows和Mac上可用。

4.2 浮点数格式化谓词函数

有些浮点数操作虽然有用,但是使用频率很低。为了避免宏的数量爆炸,我们以格式化谓词函数的形式提供了一组功能,你可以方便的用在谓词断言里(例如EXPECT_PRED_FORMAT2)。

EXPECT_PRED_FORMAT2(::testing::FloatLE, val1, val2);
EXPECT_PRED_FORMAT2(::testing::DoubleLE, val1, val2);


在上面我们判断val1小于,或几乎等于val2。你也可以把上面的EXPECT_PRED_FORMAT2替换成ASSERT_PRED_FORMAT2。

在Linux,Windows和Mac上可用。

5. Windows平台处理HRESULT的断言

以下断言用于测试HRESULT类型值的通过或失败:

致命断言非致命断言通过条件
ASSERT_HRESULT_SUCCEEDED(expression);EXPECT_HRESULT_SUCCEEDED(expression);expression代表成功的HRESULT
ASSERT_HRESULT_FAILED(expression);EXPECT_HRESULT_FAILED(expression);expression代表失败的HRESULT
断言的输出中包含与expression返回的HRESULT代码相关的可读的错误信息。

你可以这样使用:

CComPtr shell;
ASSERT_HRESULT_SUCCEEDED(shell.CoCreateInstance(L"Shell.Application"));
CComVariant empty;
ASSERT_HRESULT_SUCCEEDED(shell->ShellExecute(CComBSTR(url), empty, empty, empty, empty));


在Windows上可用。

6. 类型断言

你可以调用以下函数

::testing::StaticAssertTypeEq<T1, T2>();


来判断类型T1和T2是否相同。如果断言通过这个函数什么都不会做。如果类型不一样,调用这个函数会导致编译错误,并且编译错误信息(不同编译器会有所变化)会告诉你T1和T2的实际类型。这个宏主要针对模板代码。

警告:如果在类模板的成员函数或者函数模板内使用,仅当函数被实例化后StaticAssertTypeEq<T1, T2>()才会生效。例如以下代码:

template <typename T> class Foo {
public:
void Bar() { ::testing::StaticAssertTypeEq<int, T>(); }
};


代码

void Test1() { Foo<bool> foo; }


不会产生编译错误,因为Foo::Bar()实际上没有被实例话。但是如果修改代码

void Test2() { Foo<bool> foo; foo.Bar(); }


就会产生一个编译错误。

在Linux,Windows和Mac上可用。

7. 断言的放置

你可以在任何C++函数内使用断言。它没有必要成为test fixture类的一个方法。唯一的限制条件是生成致命错误的断言(比如FAIL*和ASSERT_*)只能用于无返回值(void)的函数。这是因为GTest没有使用异常来实现断言。如果你在非void函数使用致命失败断言的话会收到一个很困惑的编译错误,比如"error: void value not ignored as it ought to be"。

如果你非要在非void函数内使用会导致致命失败的断言,一个解决方案就是用输出参数来取代函数的返回值。比如你可以把T2 Foo(T1 x)改造成void Foo(T1 x, T2 *result)。你必须保证即使函数意外返回,*result里的数据必须是有意义的(其实是什么不重要,不把程序搞崩溃才是真的)。因为现在函数返回void,所以任何断言都可以用了。

如果不能改变函数的形式,那么你就只能使用产生非致命失败的断言,比如ADD_FAULURE*和EXPECT_*。

注:构造函数和析构函数根据C++语言规范不被认为是void类型的函数,所以在它们内部你不能使用会产生致命失败的断言。如果尝试的话必然会得到编译错误。一个简单的解决方法是把构造函数和析构函数的内容提取出来放到单独的void类型函数中去。但是要牢记,构造函数中发生的致命失败并不会停止整个测试,它只是导致构造函数提前返回,而且还会使某些对象处于部分初始化状态。类似的,析构函数中的致命失败会导致某些对象处于部分析构状态。总之在这种情况下小心为上。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: