C++ 測試庫 Catch2 入門教程


Catch 使用教程(入門,官方文檔翻譯)

原文地址:https://github.com/catchorg/Catch2/blob/master/docs/tutorial.md
譯者注:當前文檔並不是官方文檔的直譯。在翻譯的過程中我刪除部分原文中的內容並添加了一些自己的理解,可能有偏差,請見諒

  1. 獲得 Catch
  2. 如何使用?
  3. 編寫測試用例
  4. 測試用例和測試區段
  5. BDD-Style
  6. 小結
  7. 參數類型化測試
  8. 后續學習與使用

獲得 Catch

獲得Catch最簡單的方式是下載最新的 single header version。這個頭文件由若干其他獨立的頭文件合並而成。

你也可以使用其他方法獲得Catch,例如使用CMake來構建編譯版Catch,這可以提高項目的編譯速度。

完整的Catch包含測試、說明文檔等內容,你可以從GitHub下載完整的Catch。Catch官方鏈接為:http://catch-lib.net ,此鏈接將重定向到GitHub。

如何使用 Catch?

Catch是header-only的,故你只需要將Catch的頭文件放到編譯器可以發現的路徑既可。

下面的教程默認你的編譯器可以發現並使用 Catch。

如果你使用Catch的預編譯形式,即已經編譯並生成了Catch鏈接庫(.lib 或者 .a 文件),你的Catch頭文件包含形式應該形如:#include <catch2/catch.hpp>

編寫測試用例

讓我們從一個簡單的示例開始(examples/010-TestCase.cpp)。假設你已經寫了一個用於計算階乘的函數,現在准備測試它。(TDD的基本原則是先寫測試代碼,為了方便學習,這里先忽略這個原則)

unsigned int Factorial( unsigned int number ) {
    return number <= 1 ? number : Factorial(number-1)*number;
}

為了盡量簡單,我們把所有的代碼都放到一個源文件中。

#define CATCH_CONFIG_MAIN  // 當前宏強制Catch在當前編譯單元中創建 main(),這個宏只能出現在一個CPP文件中,因為一個項目只能有一個有效的main函數
#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 );
}

編譯結束后將生成一個可以接受運行時參數的可執行文件,具體可用參數請參考command-line.md。如果以不帶參數的方式執行可執行文件,所有測試用例都將被執行。詳細的測試報告將輸出到終端,測試報告包含失敗的測試用例、失敗的測試用例個數、成功的測試用例個數等信息。

執行上面代碼生成的可執行文件,所有測試用例都將通過。真的沒有錯誤嗎?不是的,上面的階乘函數是有錯誤的,我寫的第一版教程中就有這個Bug,感謝CTMacUser幫我指出了這個錯誤。

這個錯誤是什么呢?0的階乘是多少?——0的階乘是1而不是0,這就是上面階乘函數的錯誤之處。參考:0的階乘是1

讓我們把上面的規則寫入到測試用例中:

TEST_CASE( "Factorials are computed", "[factorial]" ) {
    REQUIRE( Factorial(0) == 1 );
    REQUIRE( Factorial(1) == 1 );
    REQUIRE( Factorial(2) == 2 );
    REQUIRE( Factorial(3) == 6 );
    REQUIRE( Factorial(10) == 3628800 );
}

現在測試失敗了,Catch輸出:

Example.cpp:9: FAILED:
  REQUIRE( Factorial(0) == 1 )
with expansion:
  0 == 1

Catch的測試報告會輸出期望值和Factorial(0)計算出的錯誤值0,這樣我們就可以很方便的找到錯誤。

讓我們修正階乘函數:

unsigned int Factorial( unsigned int number ) {
  return number > 1 ? Factorial(number-1)*number : 1;
}

現在所有的測試用例都通過了。

當然了上面的階乘函數依舊有不少問題,例如當number很大時計算的結果將溢出,不過我們暫不管這些。

我們做了什么?

雖然上面的測試比較簡單,但已經足夠展示如何使用Catch了。在進一步學習前,我們先解釋一下上面那段代碼。

  1. 我們定義了一個宏,並包含了Catch的頭文件,然后編譯這個源文件並生成了一個接受運行時參數的可執行文件。為了可執行,定義了宏#define CATCH_CONFIG_MAIN,強制Catch引入預定義main函數,你也可以編寫自己的main函數(參考:own-main.md)。
  2. 我們在宏TEST_CASE中編寫測試用例。這個宏可以包含一個或者兩個參數,其中一個參數是沒有固定格式的測試名,另一個參數則包含一個或多個標簽(下文介紹)。測試名必須唯一。參考command-line.md以獲得更多有關執行可執行文件的信息。
  3. 測試名和標簽都是字符串。
  4. 我們僅使用宏REQUIRE來編寫測試斷言。Catch沒有使用分立的測試函數表示不同的斷言(例如REQUIRE_TRUE、REQUIRE_FALSE、REQUIRE_EQUAL、REQUIRE_LESS等),而是直接使用C++表達式的真值結果。此外Catch使用模板表達式捕獲測試表達式的左側和右側(例如 exp_a == exp_b,Catch將捕獲exp_a和exp_b的表達式結果),從而在測試報告中顯示兩側的計算結果。

測試用例和測試區段(Test case and section)

大部分測試框架都有某種基於類的機制。例如,在很多框架(例如JUnit)的setup()階段可以創建一個在其他用例中使用的測試對象(可以是需要測試的對象,也可以是Mock對象),在teardown()階段銷毀這些對象,從而避免在每一個測試用例中創建與銷毀測試對象(或mock對象)。

使用上面傳統的測試方式有一定的缺陷,例如對於同一批測試用例你只能創建同一個測試對象,這樣的話測試粒度就比較大。(譯者注:其他缺陷可以參考原文)

Catch 使用全新的方式解決了上面的問題,如下:

TEST_CASE( "vectors can be sized and resized", "[vector]" ) {

    std::vector<int> v( 5 );

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

    SECTION( "resizing bigger changes size and capacity" ) {
        v.resize( 10 );

        REQUIRE( v.size() == 10 );
        REQUIRE( v.capacity() >= 10 );
    }
    SECTION( "resizing smaller changes size but not capacity" ) {
        v.resize( 0 );

        REQUIRE( v.size() == 0 );
        REQUIRE( v.capacity() >= 5 );
    }
    SECTION( "reserving bigger changes capacity but not size" ) {
        v.reserve( 10 );

        REQUIRE( v.size() == 5 );
        REQUIRE( v.capacity() >= 10 );
    }
    SECTION( "reserving smaller does not change size or capacity" ) {
        v.reserve( 0 );

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

對於每一個SECTIONTEST_CASE都將重新從當前TEST_CASE的起始部分開始執行並忽略其他SECTION。 (譯者注:這段原文簡單解釋了原因,Catch使用了if語句並把section看做子節點,每次執行TEST_CASE時Catch先執行起始部分的非SECTION代碼,然后選擇一個子節點並執行)。

到目前為止,Catch使用上述方式已經實現了大部分測試框架基於類(setup&teardown)的測試機制。

SECTION可以嵌套任意深度,每一個SECTION子節點都只會被執行一次,大量嵌套的SECTION會形成一棵“樹”,父節點執行失敗將不再執行對應的子節點。下面是嵌套使用SECTION的例子:

    SECTION( "reserving bigger changes capacity but not size" ) {
        v.reserve( 10 );

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

        SECTION( "reserving smaller again does not change capacity" ) {
            v.reserve( 7 );

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

BDD-Style

Catch可以使用BDD-Style形式的測試,具體請參考:test-cases-and-sections.md,下面是一個簡單的例子:

SCENARIO( "vectors can be sized and resized", "[vector]" ) {

    GIVEN( "A vector with some items" ) {
        std::vector<int> v( 5 );

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

        WHEN( "the size is increased" ) {
            v.resize( 10 );

            THEN( "the size and capacity change" ) {
                REQUIRE( v.size() == 10 );
                REQUIRE( v.capacity() >= 10 );
            }
        }
        WHEN( "the size is reduced" ) {
            v.resize( 0 );

            THEN( "the size changes but not capacity" ) {
                REQUIRE( v.size() == 0 );
                REQUIRE( v.capacity() >= 5 );
            }
        }
        WHEN( "more capacity is reserved" ) {
            v.reserve( 10 );

            THEN( "the capacity changes but not the size" ) {
                REQUIRE( v.size() == 5 );
                REQUIRE( v.capacity() >= 10 );
            }
        }
        WHEN( "less capacity is reserved" ) {
            v.reserve( 0 );

            THEN( "neither size nor capacity are changed" ) {
                REQUIRE( v.size() == 5 );
                REQUIRE( v.capacity() >= 5 );
            }
        }
    }
}

運行上面的測試用例將輸出以下內容:

Scenario: vectors can be sized and resized
     Given: A vector with some items
      When: more capacity is reserved
      Then: the capacity changes but not the size

小結

為了保證教程的簡潔性我們把所有代碼放在了一個文件中,在實際項目中這並不是好的方式。

比較好的方式是將下面這段代碼寫在一個獨立的源文件中,其他測試文件僅包含Catch頭文件和測試代碼。不要在其他測試文件中重復包含下面的#define語句。

#define CATCH_CONFIG_MAIN
#include "catch.hpp"

不要在頭文件中寫測試代碼!

類型參數化測試

Catch支持類型參數化測試,宏TEMPLATE_TEST_CASETEMPLATE_PRODUCT_TEST_CASE的行為和TEST_CASE類似,但測試用例會在不同類型下執行。下面代碼中TestType的取值依次為intfloatstd::stringBar,所有測試用例都將在這些類型下執行一遍。

struct Bar {}; 

TEMPLATE_TEST_CASE("Templated test","",int,float, std::string, Bar)
{
    std::vector<TestType> v( 5 );
    REQUIRE( v.size() == 5 );
    REQUIRE( v.capacity() >= 5 );
}

更多信息請參考:test-cases-and-sections.md中type-parametrised-test-cases小節

后續學習與使用

當前文檔簡要介紹了Catch,也指出了Catch和其他測試框架的一些區別。了解這些知識后你已經可以編寫一些實際的測試用例了。

當然還有很多東西需要學習,但你只需要在用到那些新特性的時候再學。你可以在 Readme.md 中找到Catch的所有特性。


免責聲明!

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



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