前面在基本使用中,大概紀錄了一下 Boost Test 比較基本的使用方法。
而這篇接下來,則是先來記錄一下,如果需要針對不同的資料、數值來進行大量的測試的時候,很實用的「Data-driven test cases」。
一般寫法的問題
在撰寫測試的時候,很多時候會碰到需要針對不同的參數個別去測試、才能完整涵蓋需要測試的程式的狀況。
這時候固然可以寫成函式、或是透過迴圈來跑,但是這樣的做法其實在出錯的時候,會不容易找到問題的所在。
就像下面的例子:
#define BOOST_TEST_MODULE SimpleTest #include <boost/test/included/unit_test.hpp> #include <array> BOOST_AUTO_TEST_CASE(compute) { std::array aData = {2, 3, 4, 5}; for (const int& i : aData) { BOOST_TEST(i % 2 == 0); BOOST_TEST(i > 2); } }
這樣測試的時候,執行時的錯誤訊息如下:
Running 1 test case... S:/BoostTest/Test/Test.cpp(12): error: in "compute": check i > 2 has failed [2 <= 2] S:/BoostTest/Test/Test.cpp(11): error: in "compute": check i % 2 == 0 has failed [3 % 2 != 0] S:/BoostTest/Test/Test.cpp(11): error: in "compute": check i % 2 == 0 has failed [5 % 2 != 0] *** 3 failures are detected in the test module "SimpleTest"
可以看到,他同樣的錯誤都指向同一行,雖然可以透過後面的變數來判斷、或是透過 BOOST_TEST_MESSAGE()
來加上額外的訊息,但是基本上還是不好判斷。
而如果中間有需要用到 BOOST_TEST_REQUIRE
的時候,也會變成把後面其他變數的測試也中斷掉…
基本使用
所以,Boost Test 也針對這種需求,提供了 dataset(資料集)的概念,讓系統可以根據不同的資料、自動產生出多個 test case。(官方文件)
下面就是把上面的例子修改後的版本:
#define BOOST_TEST_MODULE SimpleTest #include <boost/test/unit_test.hpp> #include <boost/test/data/test_case.hpp> #include <array> std::array aData = { 2, 3, 4, 5 }; BOOST_DATA_TEST_CASE(compute, boost::unit_test::data::make(aData)) { BOOST_TEST(sample % 2 == 0); BOOST_TEST(sample > 2); }
這邊需要 include /data/test_case.hpp
,並把用來定義 test case 的 BOOST_AUTO_TEST_CASE
換成 BOOST_DATA_TEST_CASE
;而他的第二個引數則就是要展開的 dataset,這邊是透過 boost::unit_test::data::make()
這個函式來處理標準的陣列。
而在 dataset 只有一個變數的時候,他預設的變書名稱會是 sample
。
這樣的話,他會針對 aData
的四筆資料產生出四個 test case:
>.\Test --list_content
compute*
_0*
_1*
_2*
_3*
這邊的 test case 名稱實際上會又多一層,變成「compute/_0
」這樣的形式。
執行的結果會是:
Running 4 test cases... S:/BoostTest/Test/Test.cpp(11): error: in "compute/_0": check sample > 2 has failed [2 <= 2] Failure occurred in a following context: sample = 2; S:/BoostTest/Test/Test.cpp(10): error: in "compute/_1": check sample % 2 == 0 has failed [3 % 2 != 0] Failure occurred in a following context: sample = 3; S:/BoostTest/Test/Test.cpp(10): error: in "compute/_3": check sample % 2 == 0 has failed [5 % 2 != 0] Failure occurred in a following context: sample = 5; *** 3 failures are detected in the test module "SimpleTest"
可以看到,在錯誤發生的時候,他會在下方明確列出這個 test case 的 sample
是多少。
雖然個人是覺得這樣的輸出其實不算很漂亮,但是其實算是堪用了;而如果拿到 Visual Studio 或 GitLab 裡面,則就可以很好地明確區隔出不同的資料的測試了。
而如果不想用 sample
這個預設的變數名稱的話,也可以在 BOOST_DATA_TEST_CASE
加上第三個引數、來定義自己要用的變數名稱、讓整個程式更為明確。
#define BOOST_TEST_MODULE SimpleTest #include <boost/test/unit_test.hpp> #include <boost/test/data/test_case.hpp> #include <array> std::array aData = { 2, 3, 4, 5 }; BOOST_DATA_TEST_CASE(compute, boost::unit_test::data::make(aData), val) { BOOST_TEST(val % 2 == 0); BOOST_TEST(val > 3); }
像是如果測試程式中有不同的檔名、不同的變數的時候,這樣就很夠用了~
產生測試用的數值資料
上面是自己定義了一個陣列來做為測試資料,不過實際上如果是有規則的資料的話,其實也可以透過 Boost Test 提供的 xrange()
這個函式(官方文件),來產生測試用的資料。
例如:
#define BOOST_TEST_MODULE SimpleTest #include <boost/test/unit_test.hpp> #include <boost/test/data/test_case.hpp> BOOST_DATA_TEST_CASE(compute, boost::unit_test::data::xrange(2), val) { BOOST_TEST(val < 0); }
這樣的話,就會產生 0、1 兩個測試資料。
而如果是 xrange(2,5)
的話,則是代表開始和結束的數值、會變成是 { 2, 3, 4 }
三筆資料。
如果再加上第三個數字的話,則是代表間隔;像是 xrange(2,10,2)
就會產生 {2, 4, 6, 8}
四筆資料;而在有指定間隔的情況下,也可以用來產生浮點數的測試資料。
測試資料的組合操作
有的時候要測試的參數不只一個,這時候也是可以透過 Boost Test 提供的機制,來組成多變數的 dataset。
Boost Test 針對 dataset 提供的操作有三個(官方文件):
+
:附加^
:zip(拉鍊)*
:grid(網格)
以 +
來說,就是把兩個 dataset 合併成一個,還是維持只有一個變數。下面是個簡單的例子:
#define BOOST_TEST_MODULE SimpleTest #include <boost/test/unit_test.hpp> #include <boost/test/data/test_case.hpp> namespace btd = boost::unit_test::data; BOOST_DATA_TEST_CASE(compute, btd::xrange(0,2) + btd::xrange(10,13), val) { BOOST_TEST(val < 0); }
這邊兩個 xrange()
會個別產生 {0, 1}
和 {10, 11, 12}
兩組資料,這邊透過 +
會把兩個合併起來,變成 {0, 1, 10, 11, 12}
。
而 ^
這個名為 zip(拉鍊)的操作,則是會把兩組數量相同的 dataset 一對一的組合在一起、就像拉鍊一樣;如果兩邊的 dataset 本來都是單一變數的話,產生的結果就會變成兩個變數。
下面是一個例子:
#define BOOST_TEST_MODULE SimpleTest #include <boost/test/unit_test.hpp> #include <boost/test/data/test_case.hpp> namespace btd = boost::unit_test::data; BOOST_DATA_TEST_CASE(compute, btd::xrange(0,3) ^ btd::xrange(10,13), val1, val2) { BOOST_TEST(false, "vals: (" << val1 << ", " << val2 << ")"); }
這邊兩個 xrange()
產生的資料分別是 {0, 1, 2}
和 {10, 11, 12}
,在透過 ^
組合後,會變成 { {0, 10}, {1, 11}, {2, 12} }
這樣的形式,總共會產生三個 test case,數值則可以透過 val1
和 val2
來讀取。
而如果要產生三個變數的 dataset 的話,也可以這樣繼續串聯。
最後,*
這個 grid 的操作,則是會把兩個 dataset 展開出所有的組合;基本上就像是兩層迴圈去跑過所有的資料組合一樣。
#define BOOST_TEST_MODULE SimpleTest #include <boost/test/unit_test.hpp> #include <boost/test/data/test_case.hpp> namespace btd = boost::unit_test::data; BOOST_DATA_TEST_CASE(compute, btd::xrange(2) * btd::xrange(3), val1, val2) { BOOST_TEST(false, "vals: (" << val1 << ", " << val2 << ")"); }
以上面例子的 {0, 1}
和 {0, 1, 2}
兩組資料來說,在經過 *
操作後,就會產生總計 2 * 3 = 6 組的資料,變成是 { {0,0}, {0, 1}, {0, 2}, {1,0}, {1, 1}, {1, 2} }
;而也因此,最後也會根據這些資料,產生獨立的 6 個 test case。
當要測試的資料很多的時候,透過這些操作,是可以快速、簡單地產生需要的測試資料、以及對應的 test case 的。
產生亂數資料
有的時候要測試的時候可以會希望使用亂數做為測試資料,而 Boost Test 也有提供亂數產生器,可以搭配其他 dataset 來使用。
這邊的亂數產生器函式是 random()
,預設會產生 0 ~ 1 之間的亂數(浮點數);由於他可以產生無限多組資料,所以不能單獨使用,而是需要透過 ^
和其他 dataset 來合併使用。下面是個簡單的例子:
#define BOOST_TEST_MODULE SimpleTest #include <boost/test/unit_test.hpp> #include <boost/test/data/test_case.hpp> namespace btd = boost::unit_test::data; BOOST_DATA_TEST_CASE(compute, btd::xrange(2) ^ btd::random(), val1, val2) { BOOST_TEST(false, "vals: (" << val1 << ", " << val2 << ")"); }
這樣的話,會產生兩個 test case,拿到的 val1
會是 {0, 1}
,而 val2
就會是 0 ~ 1 之間的亂數了。
而如果想要限制亂數的範圍的話,也可以給 random()
兩個引數,這樣就會變成是產生介於這兩個數值之間的亂數;例如 random(1, 10)
就會產生 1 ~ 10 之間的整數型別的亂數。
另外,如果要更詳細的設定亂數產生的方式的話,他也可以指定 random seed 和標準函示庫提供的亂數分布(參考),來進一步針對產生的亂數做控制。
下面是一個例子:
#define BOOST_TEST_MODULE SimpleTest #include <boost/test/unit_test.hpp> #include <boost/test/data/test_case.hpp> namespace btd = boost::unit_test::data; auto rdgen = btd::random( (btd::seed = 100UL, btd::distribution = std::normal_distribution<>(5., 2))); BOOST_DATA_TEST_CASE(compute, btd::xrange(5) ^ rdgen, val1, val2) { BOOST_TEST(false, "vals: (" << val1 << ", " << val2 << ")"); }
這邊大概就是這樣了?
雖然這樣的設計在撰寫測試的時候算是滿方便的,但是實際上還是有些缺點。主要就是不管在 Visual Studio 的測試總管、或是 GitLab pipeline 的測試報表中,針對變數自動生成的 test case 名稱其實都過度單純,也就是前面提到「compute/_0
」這樣的形式。
在這個狀況下,其實會比較難判斷到底是對應到哪組測試參數?雖然在出錯的時候,是可以點進去看錯誤訊息來做判斷,但是其實還是比較不方便。
個人是覺得,如果可以提供更有意義的 test case 名稱、應該會更好吧?