Boost Test 的資料導向測試

| | 0 Comments| 10:44
Categories:

前面在基本使用中,大概紀錄了一下 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,數值則可以透過 val1val2 來讀取。

而如果要產生三個變數的 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 名稱、應該會更好吧?

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *