年中的時候有整理了幾篇關於 Boost Test 這個建立 C++ 單元測試的函式庫的使用方法了,當時基本上是把簡單的使用情境都做了一些說明;當然,其實 Boost Test 還有很多東西,所以這邊基本上不太可能寫完,像是 fixture 這東西(官方文件)Heresy 就還沒有提過。 :p
而這一篇呢,則是 Heresy 這邊自己在想,如果有一個 template 的測試函式:
template<typename T1, typename T2, typename T3> void test_func(T1 v1, T2 v2) {}
那要怎麼樣可以快速地針對不同的型別組合、還有數值資料的組合、來建立出多個 test case 呢?
雖然最後寫出來的東西感覺不是很實用,不過基本上還是來記錄一下吧~ XD
多型別的 template test case
首先,相較於資料導向的測試來說,Boost Test 在 template test case 提供的功能算是相對少的,而且似乎也沒辦法針對多組型別來展開,算是滿可惜的。
當然啦,這邊還是可以透過巢狀式的 std::tuple
(參考)來完成來多個型別的玩法,但是就是比較麻煩一點了。
下面算是一個玩法:
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <tuple> #include <typeinfo> typedef std::tuple< std::tuple<int, float>, std::tuple<int, double>, std::tuple<float, int> > test_types; BOOST_AUTO_TEST_CASE_TEMPLATE(TypedTest, T, test_types) { using T1 = std::tuple_element<0, T>::type; using T2 = std::tuple_element<1, T>::type; std::cout << typeid(T1).name() << "/" << typeid(T2).name() << "\n"; BOOST_TEST(sizeof(T1) == (unsigned)4); }
透過這個方法,就可以處理多種型別的組合了。
而如果要更簡單地產生型別的排列組合其實也是做得到的,但是就是相對更麻煩一點了,所以這邊先跳過。
使用 C++ API 建立 test case
至於有沒有辦法把 dataset 和 template 混合使用呢?以 Boost Test 提供的巨集來看應該是沒辦法的。
所以如果真的有碰到這種需求,大概就得放棄這種以巨集為主的方法,而是改用 C++ API 來自己建立 test case 了~而這個過程…其實還滿麻煩的。
如果要自己建立 test case 的話,基本的範例會是像下面這樣:
#include <boost/test/included/unit_test.hpp> using namespace boost::unit_test; void test_case1() {} test_suite* init_unit_test_suite(int /*argc*/, char* /*argv*/[]) { test_suite* ts1 = BOOST_TEST_SUITE("test_suite1"); ts1->add(BOOST_TEST_CASE(&test_case1)); ts1->add(BOOST_TEST_CASE_NAME([]() {}, "lambda")); return ts1; }
在只有一個 test suite 的狀況下,只要定義 init_unit_test_suite()
這個函式、回傳 test_suite
的指標就可以了。
而 test_suite
的建立,則可以靠 BOOST_TEST_SUITE
這個巨集完成,之後再透過他的 add()
函式加入個別的 test case。
至於 test case 的建立,則可以透過 BOOST_TEST_CASE_NAME
來產生,如果不想自己命名的話,也可以改用 BOOST_TEST_CASE
,這樣它會用傳入的可呼叫物件名稱作為 test case 的名字。
以上面的程式碼來說,產生出來的 test case 結構會是:
test_suite1* test_case1* lambda*
而如果有多個 test suite 的話,似乎則就需要自己先加到 master test suite 裡了?下面是官方的範例:
#include <boost/test/included/unit_test.hpp> using namespace boost::unit_test; void test_case1() { /* ... */ } void test_case2() { /* ... */ } void test_case3() { /* ... */ } void test_case4() { /* ... */ } test_suite* init_unit_test_suite(int /*argc*/, char* /*argv*/[]) { test_suite* ts1 = BOOST_TEST_SUITE("test_suite1"); ts1->add(BOOST_TEST_CASE(&test_case1)); ts1->add(BOOST_TEST_CASE(&test_case2)); test_suite* ts2 = BOOST_TEST_SUITE("test_suite2"); ts2->add(BOOST_TEST_CASE(&test_case3)); ts2->add(BOOST_TEST_CASE(&test_case4)); framework::master_test_suite().add(ts1); framework::master_test_suite().add(ts2); return 0; }
透過這樣的方法,基本上就可以手動建立出各有兩個 test case 的 test suite 了。
test_suite1* test_case1* test_case2* test_suite2* test_case3* test_case4*
根據 tuple 型別來建立 template test case
在知道怎麼透過 C++ API 來建立 test case 後,接下來就是來試試看要怎麼樣用這樣的形式來建立出 template 的 test case 了~
如果以這種手動建立 test case 的方法為基礎,要透過 std::tuple
來建立多組型別的 test case 的話,可以寫成下面的形式:
#include <boost/test/included/unit_test.hpp> #include <tuple> #include <typeinfo> #include <sstream> using namespace boost::unit_test; // Test function template<typename T1, typename T2> void test_func() {} // Type set to test std::tuple< std::tuple<int, float>, std::tuple<int, double> > testTypes; // generate test cases test_suite* init_unit_test_suite(int, char* []) { test_suite* ts = BOOST_TEST_SUITE("test_suite"); std::apply([ts](auto& ... types) { ([ts]<typename ...Ts>(const std::tuple<Ts...>&) { // generate test name std::ostringstream oss; oss << "test<"; ((oss << typeid(Ts).name() << "-"), ...); oss << ">"; // generate test function auto f = []() {test_func<Ts...>(); }; // add test case ts->add(BOOST_TEST_CASE_NAME(f, oss.str())); }(types), ...); }, testTypes); return ts; }
這邊的 test case 是 test_func<>()
這個 templae 函式需要兩個型別;而 testTypes
就是透過巢狀的 tuple
定義要有那些型別的組合,這邊是兩組。
之後,就是透過 std::apply()
把 testTypes
的資料展開。這邊由於 std::tuple<>
包了兩層、所以這邊也要透過兩層 lambda 來展開。
第一層的部分是:
std::apply([ts](auto& ... types) { ... }, testTypes);
他得到的引數是 parameter pack 的形式,根據上面的定義會是兩個 std::tuple<>
。
第二層的部分則是在 Fold Expression 內,它的內容基本上是:
([ts]<typename ...Ts>(const std::tuple<Ts...>&) { ... }(types), ...);
他會被執行兩次、第一次得到的引數型別是 std::tuple<int, float>
、第二次則是
std::tuple<int, double>
。
這邊會先拿引數的型別 std::tuple<Ts...>
來建立 test case 的名稱、然後建立出一個去執行指定型別的 test_func<>()
的 lambda f
,並把他加到 test suite 裡面。
這樣的程式就會產生兩個指定型別組合的 test case 了~
> .\Test.exe --list_content test_suite* test<int-float->* test<int-double->*
而理論上,他也可以對應任何數量的 template 函式。
加入要測試的資料
型別的問題解決了,接下來就是把測試資料也加進來了。由於 Boost Test 的 dataset 算是滿好用的,可以簡單地組合,所以這邊是打算延用他的形式。
最後寫出來的結果是:
#include <boost/test/included/unit_test.hpp> #include <boost/test/data/test_case.hpp> #include <tuple> #include <typeinfo> #include <sstream> // Test function template<typename T1, typename T2, typename T3> void test_func(T1 v1, T2 v2) {} // Type set to test std::tuple< std::tuple<int, char, float>, std::tuple<uint8_t, char, double> > testTypes; // Data set to test namespace btd = boost::unit_test::data; auto testData = btd::xrange(0, 3) ^ btd::make({ 'a', 'b', 'c' }); // generate test cases using namespace boost::unit_test; test_suite* init_unit_test_suite(int /*argc*/, char* /*argv*/[]) { test_suite* ts = BOOST_TEST_SUITE("test_suite1"); std::apply([ts](auto& ... types) { ([ts]<typename ...Ts>(const std::tuple<Ts...>&) { // Generate test name with types std::ostringstream oss; oss << "test<"; ((oss << typeid(Ts).name() << "-"), ...); oss << ">"; std::string sName = oss.str(); // for each test data auto it = testData.begin(); for (size_t i = 0; i < testData.size(); ++i) { auto tupleData = *it; ++it; // Generate test name with values std::ostringstream ossv; ossv << sName << "("; std::apply([&ossv](const auto& ... Vs) { ((ossv << Vs << "-"), ...); }, tupleData); ossv << ")"; // generate test function auto f = [tupleData]() { std::apply([](const auto& ... Vs) { test_func<Ts...>(Vs...); }, tupleData); }; // add test case ts->add(BOOST_TEST_CASE_NAME(f, ossv.str())); } }(types), ...); }, testTypes); return ts; }
這邊和前面的差別,就在於在確定型別後、會再針對 testData
裡面的資料、用迴圈的形式來建立出 test case;這樣建立出來的 test case 會如下:
test_suite1* test<int-char-float->(1-a-)* test<int-char-float->(2-b-)* test<int-char-float->(3-c-)* test<unsigned char-char-double->(1-a-)* test<unsigned char-char-double->(2-b-)* test<unsigned char-char-double->(3-c-)*
這邊其實也會發現,程式中很大一部分都是在處理 test case 的名稱,而且如果要更好看的話,可能還得寫得更細緻一點。
雖然到這邊,算是可以透過 testData
來指定要測試的資料、並透過 testTypes
來指定要測試的型別、藉此展開多個 test case 了,但是老實說,程式碼的複雜度比想像的高了不少、可讀性也不是很好。
而且,這邊其實還有一個比較大的問題,是一個 template 函式在沒有明確指定型的情況下,是相當難以傳遞的…也就是說,這邊沒辦法把實際要執行的測試函式「test_func()
」參數化。
也由於這個問題,所以上面 init_unit_test_suite()
中這串內容很難抽出來變成一個函式、讓他可以再多個地方使用…
個人覺得真的要讓他可以真的通用的話,可能也是得搭配巨集來寫了吧?當然啦,應該也還有一些稍微簡單一點的玩法可以繞過這個問題,但是就會變得相對地沒有那麼直覺了。
不過…Heresy 這部分就不太想玩下去了…這也是為什麼一開始說這邊寫出來的東西實用性不高的原因了。