[原文地址:http://blog.guorongfei.com/2016/08/22/cpp-unit-test-catch/]
如果你平常使用 Java 語言做開發(fā)碌秸,當你聽到單元測試工具的時候吼具,你很可能馬上會想起 JUnit渤早。作為一名C++軟件工程師谤专,當我第一次打算給我的程序做單元測試的時候训措,我的第一 想法是:有這樣的工具嗎郑临?經(jīng)過一段時間的搜索之后颜骤,我的反應變成了:我該用哪一個聂渊?
我在學校的時候孝宗,很少聽說C++的單元測試工具穷躁,以至于我一直認為這樣的工具是不存在。 后來慢慢的發(fā)現(xiàn)我們可以選擇的遠比你想象中的要多得多: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? 有興趣的可以去看看灾茁。對我來說,它最吸引我的地方主要是:
- 幾乎不用配置谷炸,它是一個單頭文件的測試框架北专,壓根不要什么<header class="post-header" style="opacity: 1; display: block; transform: translateY(0px);">
C++ 的單元測試工具 —— Catch
發(fā)表于 <time title="創(chuàng)建于" itemprop="dateCreated datePublished" datetime="2016-08-22T15:37:30+08:00">2016-08-22</time> | 閱讀次數(shù): 2356
</header>
如果你平常使用 Java 語言做開發(fā),當你聽到單元測試工具的時候,你很可能馬上會想起 JUnit。作為一名C++軟件工程師借跪,當我第一次打算給我的程序做單元測試的時候,我的第一 想法是:有這樣的工具嗎驶睦?經(jīng)過一段時間的搜索之后,我的反應變成了:我該用哪一個匿醒?
我在學校的時候场航,很少聽說C++的單元測試工具,以至于我一直認為這樣的工具是不存在青抛。 后來慢慢的發(fā)現(xiàn)我們可以選擇的遠比你想象中的要多得多: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” 它就可以了。然后你就可以像下面 這樣寫測試代碼:
|
<pre>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 );
}
}
}
}
</pre>
|
這幾乎是不需要解釋就可以理解的讀懂的代碼罢坝。這種測試方式稱為 BDD(Behaviour Driven Development)廓握,是最新的一種測試方式,它強調(diào)的是“行為”而不是“測試”嘁酿,有興趣可以看 看這篇文章隙券。
如果你習慣傳統(tǒng)的TDD測試,你可以像下面這樣寫測試代碼:
|
<pre>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 );
}
}
</pre>
|
實際上這兩種方式是等價闹司,SCENARIO 只是 TEST_CASE
的別名娱仔,GIVEN、WHEN游桩、THEN 最終 也是 map 到 SECTION 上面的牲迫。這其中的差異只是存在于測試的思維不同而已,你完全可以 根據(jù)自己的喜好使用你最喜歡的方式即可众弓。
SECTION 的執(zhí)行順序
上面的代碼很清晰易懂恩溅,不過有一個地方需要注意隔箍,那就是 SECTION 的執(zhí)行方式谓娃。在上一 小節(jié)的代碼中,TEST_CASE
中有 4 個 SECTION蜒滩,它們并不是單純的順序執(zhí)行關(guān)系滨达。在第 一個 SECTION 執(zhí)行完成之后,會重頭開始執(zhí)行并跳過已經(jīng)執(zhí)行過的 SECTION俯艰。也就是說上 面的代碼的執(zhí)行路徑大概是這樣的(去掉了 SECTION 宏之后):
|
<pre>// 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 );
</pre>
|
測試代碼的執(zhí)行入口
在C++中任何代碼需要執(zhí)行捡遍,都需要通過 main 函數(shù)這個入口,測試代碼也不例外竹握。Catch 不需要我們自己編寫 main 函數(shù)去調(diào)用這些測試代碼画株。它提供了默認的 main 函數(shù)入口,你 只需要在(而且僅在)一個文件中加入下面的配置宏:
|
<pre>#define CATCH_CONFIG_MAIN
include "catch.hpp"
</pre>
|
最佳實踐
最佳實踐是單獨用一個文件放這兩行代碼,把測試代碼寫在其他的文件中谓传。
之所以這樣做是因為Catch是單頭文件庫,這意味著它里面的內(nèi)容會最終出現(xiàn)在所有的包含 這個頭文件的編譯單元中续挟。如果我們把測試代碼和上面兩行代碼放在一起會導致每次編譯測 試代碼的時候都需要編譯 Catch 的內(nèi)核紧卒,這會導致編譯速度非常非常的慢。如果把兩者分 開诗祸,Catch 的內(nèi)核只需要在一個文件中編譯一次(因為 Catch 內(nèi)部做了判斷跑芳,如果內(nèi)核編 譯過了是不需要再次編譯的,即使你在多個文件中使用了 #include “catch.hpp”)直颅。這個 文件的編譯速度相對較慢博个,但是這個文件不會改動所以整個開發(fā)周期中它只需要編譯一次, 而不斷更新的測試代碼的編譯速度會因此快很多功偿。
命令行參數(shù)
Catch 提供的這個 main 函數(shù)實現(xiàn)的另一個強大的功能是豐富的命令行參數(shù)坡倔,你可以選擇執(zhí) 行其中的某些 TEST_CASE
,也可以選擇不執(zhí)行其中的某些 TEST_CASE
脖含,你可以用它調(diào)整 輸出到 xml 文件罪塔,也可以用它從文件中讀取需要測試的用例。這些命令的具體使用請參考 Catch 的官方文檔Command line一節(jié)养葵。
TAG
需要注意的是征堪,這些強大的命令行大多數(shù)是基于 TAG 的,也就是 TEST_CASE
定義中的第 二個參數(shù)关拒。
|
<pre>TEST_CASE( "vectors can be sized and resized", "[vector]" )
</pre>
|
上面的定義中 “[vector]” 就是一個 TAG佃蚜,你可以提供多個 TAG:
|
<pre>TEST_CASE( "D", "[widget][gadget]" ) { /* ... */ }
</pre>
|
這樣的話你可以在命令行中根據(jù) TAG 去選擇是否需要執(zhí)行該 TEST_CASE
。比如:
|
<pre>./catch "[vector]" // 只執(zhí)行那些標記為 vector 的測試用例
</pre>
|
此外你還可以使用一些特殊的字符着绊,比如 [.]
表示隱藏谐算。[.integration]
則表示默認 隱藏,但是可以在命令行中使用 [.integration]
這個 TAG 執(zhí)行归露。其他的一些特殊的字符 請參考官方文檔的Test cases and sections一節(jié)
|
<pre>./catch // 默認不執(zhí)行 integration
./catch "[.integration]" // 使用 TAG 執(zhí)行 integration
</pre>
|
提供自己的 main 函數(shù)入口
如果你不喜歡上面的處理方式洲脂,想要自己提供 main 函數(shù),你可以使用 CATCH_CONFIG_RUNNER
剧包,具體的細節(jié)請查看官方文檔中的 Supplying main() yourself一節(jié)恐锦。
其他內(nèi)容
其實 Catch 本身相對來說比較簡單,不需要太多其他的學習疆液,大部分的用法是非常的直觀 的一铅,看完它的官方教程之后基本上可以上手了,然后有時間慢慢的讀一讀它的官方文 檔集合
額外的配置就可以使用
- 語法非常簡單明了堕油,用它寫的測試代碼和自然語言一樣易懂潘飘。
如何使用它
Catch
是單頭文件庫肮之,你直接 #include “catch.hpp” 它就可以了。然后你就可以像下面 這樣寫測試代碼:
|
<pre>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 );
}
}
}
}
</pre>
|
這幾乎是不需要解釋就可以理解的讀懂的代碼卜录。這種測試方式稱為 BDD(Behaviour Driven Development)局骤,是最新的一種測試方式,它強調(diào)的是“行為”而不是“測試”暴凑,有興趣可以看 看這篇文章峦甩。
如果你習慣傳統(tǒng)的TDD測試,你可以像下面這樣寫測試代碼:
|
<pre>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 );
}
}
</pre>
|
實際上這兩種方式是等價现喳,SCENARIO 只是 TEST_CASE
的別名凯傲,GIVEN、WHEN嗦篱、THEN 最終 也是 map 到 SECTION 上面的冰单。這其中的差異只是存在于測試的思維不同而已,你完全可以 根據(jù)自己的喜好使用你最喜歡的方式即可灸促。
SECTION 的執(zhí)行順序
上面的代碼很清晰易懂诫欠,不過有一個地方需要注意,那就是 SECTION 的執(zhí)行方式浴栽。在上一 小節(jié)的代碼中荒叼,TEST_CASE
中有 4 個 SECTION,它們并不是單純的順序執(zhí)行關(guān)系典鸡。在第 一個 SECTION 執(zhí)行完成之后被廓,會重頭開始執(zhí)行并跳過已經(jīng)執(zhí)行過的 SECTION。也就是說上 面的代碼的執(zhí)行路徑大概是這樣的(去掉了 SECTION 宏之后):
|
<pre>// 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 );
</pre>
|
測試代碼的執(zhí)行入口
在C++中任何代碼需要執(zhí)行萝玷,都需要通過 main 函數(shù)這個入口嫁乘,測試代碼也不例外。Catch 不需要我們自己編寫 main 函數(shù)去調(diào)用這些測試代碼球碉。它提供了默認的 main 函數(shù)入口蜓斧,你 只需要在(而且僅在)一個文件中加入下面的配置宏:
|
<pre>#define CATCH_CONFIG_MAIN
include "catch.hpp"
</pre>
|
最佳實踐
最佳實踐是單獨用一個文件放這兩行代碼,把測試代碼寫在其他的文件中睁冬。
之所以這樣做是因為Catch是單頭文件庫挎春,這意味著它里面的內(nèi)容會最終出現(xiàn)在所有的包含 這個頭文件的編譯單元中。如果我們把測試代碼和上面兩行代碼放在一起會導致每次編譯測 試代碼的時候都需要編譯 Catch 的內(nèi)核痴突,這會導致編譯速度非常非常的慢搂蜓。如果把兩者分 開狼荞,Catch 的內(nèi)核只需要在一個文件中編譯一次(因為 Catch 內(nèi)部做了判斷辽装,如果內(nèi)核編 譯過了是不需要再次編譯的,即使你在多個文件中使用了 #include “catch.hpp”)相味。這個 文件的編譯速度相對較慢拾积,但是這個文件不會改動所以整個開發(fā)周期中它只需要編譯一次, 而不斷更新的測試代碼的編譯速度會因此快很多。
命令行參數(shù)
Catch 提供的這個 main 函數(shù)實現(xiàn)的另一個強大的功能是豐富的命令行參數(shù)拓巧,你可以選擇執(zhí) 行其中的某些 TEST_CASE
斯碌,也可以選擇不執(zhí)行其中的某些 TEST_CASE
,你可以用它調(diào)整 輸出到 xml 文件肛度,也可以用它從文件中讀取需要測試的用例傻唾。這些命令的具體使用請參考 Catch 的官方文檔Command line一節(jié)。
TAG
需要注意的是承耿,這些強大的命令行大多數(shù)是基于 TAG 的冠骄,也就是 TEST_CASE
定義中的第 二個參數(shù)。
|
<pre>TEST_CASE( "vectors can be sized and resized", "[vector]" )
</pre>
|
上面的定義中 “[vector]” 就是一個 TAG加袋,你可以提供多個 TAG:
|
<pre>TEST_CASE( "D", "[widget][gadget]" ) { /* ... */ }
</pre>
|
這樣的話你可以在命令行中根據(jù) TAG 去選擇是否需要執(zhí)行該 TEST_CASE
凛辣。比如:
|
<pre>./catch "[vector]" // 只執(zhí)行那些標記為 vector 的測試用例
</pre>
|
此外你還可以使用一些特殊的字符,比如 [.]
表示隱藏职烧。[.integration]
則表示默認 隱藏扁誓,但是可以在命令行中使用 [.integration]
這個 TAG 執(zhí)行。其他的一些特殊的字符 請參考官方文檔的Test cases and sections一節(jié)
|
<pre>./catch // 默認不執(zhí)行 integration
./catch "[.integration]" // 使用 TAG 執(zhí)行 integration
</pre>
|
提供自己的 main 函數(shù)入口
如果你不喜歡上面的處理方式蚀之,想要自己提供 main 函數(shù)蝗敢,你可以使用 CATCH_CONFIG_RUNNER
,具體的細節(jié)請查看官方文檔中的 Supplying main() yourself一節(jié)足删。
其他內(nèi)容
其實 Catch 本身相對來說比較簡單前普,不需要太多其他的學習,大部分的用法是非常的直觀 的壹堰,看完它的官方教程之后基本上可以上手了拭卿,然后有時間慢慢的讀一讀它的官方文 檔集合