Google C++測試框架系列高級篇:第一章 更多關於斷言的知識


原始鏈接:More Assertions

詞匯表

現在你應該已經讀完了入門篇並且會使用GTest來寫測試。是時候來學一些新把戲了。這篇文檔將教會你更多知識:用斷言構造復雜的失敗信息,傳遞致命失敗,重用和加速你的test fixtures,以及在你的測試中使用不同的標志位。

版本號:v_0.1

更多關於斷言的知識

這一章節將覆蓋一些較少被使用,但非常重要的關於斷言的知識。

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

注:

  1. 當使用ASSERT_PRED*或EXPECT_PRED*遇到編譯錯誤"no matching function to call"時,請參考這篇文章如何解決。
  2. 當前我們支持的參數數量小於等於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); 

其中val1val2,...直到valn表示謂詞的參數,而expr1expr2,...直到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類型函數中去。但是要牢記,構造函數中發生的致命失敗並不會停止整個測試,它只是導致構造函數提前返回,而且還會使某些對象處於部分初始化狀態。類似的,析構函數中的致命失敗會導致某些對象處於部分析構狀態。總之在這種情況下小心為上。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM