C++ 的單元測試工具 —— Catch


【轉帖】

C++ 的單元測試工具 —— Catch

 

如果你平常使用 Java 語言做開發,當你聽到單元測試工具的時候,你很可能馬上會想起 JUnit。作為一名C++軟件工程師,當我第一次打算給我的程序做單元測試的時候,我的第一想法是:有這樣的工具嗎?經過一段時間的搜索之后,我的反應變成了:我該用哪一個?

我在學校的時候,很少聽說C++的單元測試工具,以至於我一直認為這樣的工具是不存在。后來慢慢的發現我們可以選擇的遠比你想象中的要多得多:Catch, Boost.Test, UnitTest++, lest, bandit, igloo, xUnit++, CppTest, CppUnit, CxxTest, cpputest, googletest, cute。

那我們應該使用哪一個呢?如果你在 Google 里面搜索:best c++ unit testing framework。頭兩條,一條是 stackoverflow 的問答,另一條是 reddit 的問答。這兩個問題都指向同一個單元測試框架:Catch

為什么使用 Catch

在 Catch 的官方文檔中有一篇:Why do we need yet another C++ test framework? 有興趣的可以去看看。對我來說,它最吸引我的地方主要是:

  • 幾乎不用配置,它是一個單頭文件的測試框架,壓根不要什么額外的配置就可以使用
  • 語法非常簡單明了,用它寫的測試代碼和自然語言一樣易懂。

如何使用它

Catch 是單頭文件庫,你直接 #include "catch.hpp" 它就可以了。然后你就可以像下面這樣寫測試代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 );
}
}
}
}

這幾乎是不需要解釋就可以理解的讀懂的代碼。這種測試方式稱為 BDD(Behaviour Driven Development),是最新的一種測試方式,它強調的是“行為”而不是“測試”,有興趣可以看看這篇文章

如果你習慣傳統的TDD測試,你可以像下面這樣寫測試代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 );
}
}

實際上這兩種方式是等價,SCENARIO 只是 TEST_CASE 的別名,GIVEN、WHEN、THEN 最終也是 map 到 SECTION 上面的。這其中的差異只是存在於測試的思維不同而已,你完全可以根據自己的喜好使用你最喜歡的方式即可。

SECTION 的執行順序

上面的代碼很清晰易懂,不過有一個地方需要注意,那就是 SECTION 的執行方式。在上一小節的代碼中,TEST_CASE 中有 4 個 SECTION,它們並不是單純的順序執行關系。在第一個 SECTION 執行完成之后,會重頭開始執行並跳過已經執行過的 SECTION。也就是說上面的代碼的執行路徑大概是這樣的(去掉了 SECTION 宏之后):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// SECTION 1
std::vector<int> v( 5 );
 
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
 
v.resize( 10 );
 
REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );
 
// SECTION 2
std::vector<int> v( 5 );
 
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
 
v.resize( 0 );
 
REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );
 
// SECTION 3
std::vector<int> v( 5 );
 
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
 
v.reserve( 10 );
 
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
 
// SECTION 4
std::vector<int> v( 5 );
 
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
 
v.reserve( 0 );
 
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

測試代碼的執行入口

在C++中任何代碼需要執行,都需要通過 main 函數這個入口,測試代碼也不例外。Catch 不需要我們自己編寫 main 函數去調用這些測試代碼。它提供了默認的 main 函數入口,你只需要在(而且僅在)一個文件中加入下面的配置宏:

1
2
#define CATCH_CONFIG_MAIN
#include "catch.hpp"

最佳實踐

最佳實踐是單獨用一個文件放這兩行代碼,把測試代碼寫在其他的文件中。

之所以這樣做是因為Catch是單頭文件庫,這意味着它里面的內容會最終出現在所有的包含這個頭文件的編譯單元中。如果我們把測試代碼和上面兩行代碼放在一起會導致每次編譯測試代碼的時候都需要編譯 Catch 的內核,這會導致編譯速度非常非常的慢。如果把兩者分開,Catch 的內核只需要在一個文件中編譯一次(因為 Catch 內部做了判斷,如果內核編譯過了是不需要再次編譯的,即使你在多個文件中使用了 #include "catch.hpp")。這個文件的編譯速度相對較慢,但是這個文件不會改動所以整個開發周期中它只需要編譯一次,而不斷更新的測試代碼的編譯速度會因此快很多。

命令行參數

Catch 提供的這個 main 函數實現的另一個強大的功能是豐富的命令行參數,你可以選擇執行其中的某些 TEST_CASE,也可以選擇不執行其中的某些 TEST_CASE,你可以用它調整輸出到 xml 文件,也可以用它從文件中讀取需要測試的用例。這些命令的具體使用請參考 Catch 的官方文檔Command line一節。

TAG

需要注意的是,這些強大的命令行大多數是基於 TAG 的,也就是 TEST_CASE 定義中的第二個參數。

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

上面的定義中 "[vector]" 就是一個 TAG,你可以提供多個 TAG:

1
TEST_CASE( "D", "[widget][gadget]" ) { /* ... */ }

這樣的話你可以在命令行中根據 TAG 去選擇是否需要執行該 TEST_CASE。比如:

1
./catch "[vector]" // 只執行那些標記為 vector 的測試用例

此外你還可以使用一些特殊的字符,比如 [.] 表示隱藏。[.integration] 則表示默認隱藏,但是可以在命令行中使用 [.integration] 這個 TAG 執行。其他的一些特殊的字符請參考官方文檔的Test cases and sections一節

1
2
3
./catch // 默認不執行 integration
 
./catch "[.integration]" // 使用 TAG 執行 integration

提供自己的 main 函數入口

如果你不喜歡上面的處理方式,想要自己提供 main 函數,你可以使用 CATCH_CONFIG_RUNNER,具體的細節請查看官方文檔中的 Supplying main() yourself一節。

其他內容

其實 Catch 本身相對來說比較簡單,不需要太多其他的學習,大部分的用法是非常的直觀的,看完它的官方教程之后基本上可以上手了,然后有時間慢慢的讀一讀它的官方文檔集合


免責聲明!

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



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