使用 Boost Test 開發 C++ 的單元測試:基本使用

| | 0 Comments| 10:11
Categories:

這邊來個久違的長篇技術文章連載吧~這次是介紹關於單元測試還有程式碼覆蓋率的東西;而首先,則是先來介紹一下用來開發 C++ 單元測試的 Boost Test 了。

Boost.Test(官方文件)是 Boost C++ Libraries 中,一套以巨集為基礎 C++ Unit Test 開發輔助用的函式庫;透過這個函式庫,可以快速地開發出 C++ 的單元測試。

在 Visual Studio 中,也有支援將 Boost Test 的測試整合到 Visual Studio 的測試管理員的功能(參考),所以還算是方便;再加上他也可以產生 GitLab 平台上 unit test 所需要的「JUnit report format」(參考),所以這也是 Heresy 這邊使用這套函式庫的原因。

而這篇呢,則就簡單紀錄一下,怎麼用 Boost.Test 來寫 unit test 了。


基本架構

首先,Boost.Test 的架構應該是分成 test module、test suite、test case、test 四個層級,基本上算是樹狀結構的概念。

基本上,一個測試程式(一個執行檔、或一個 DLL)會是一個 test module;而實際上,他也是 master test suite。

而一個 test module 可以有許多的 test case、每個 test case 會類似一個獨立的函式,裡面要去測試一段相對完整的流程;test case 裡面則會把流程再細分成許多小的 test,來做實際的測試。

當 test module 裡面的 test case 變多了,需要分類管理的時候,則就可以建立出 test suite 來建立 test case 的群組;而如果 test case 不多的話,其實可以直接省略掉這一層。

所以,Boost Test 的樹狀結構實際上會是下面的形式:

Test Module / Master test suite
 ⊢ Test Suite 1
 |   ⊢ Test case 1
 |   ⌞ Test case 2
 ⌞ Test Suite 2
     ⊢ Test case 3
     ⌞ Test case 4


最簡單的例子

下面就算是一個最簡單的 Boost Test 程式:

#define BOOST_TEST_MODULE My Test
#include <boost/test/included/unit_test.hpp>
 
BOOST_AUTO_TEST_CASE(first_test)
{
  BOOST_TEST(1 + 1 == 2);
}

一開始的「BOOST_TEST_MODULE」,就是定義這個程式的 test module / master test suite 名稱為「My Test」。

而這邊可以看到,這個測試程式並沒有 main() 函式,這是因為 Boost Test 在有定義 test module 的狀況下、會自己去產生對應的入口,然後去執行所有測試。

這邊的「BOOST_AUTO_TEST_CASE」也只是一個最簡單的巨集,使用的時候基本上就是把它當成一個函式來寫;而 Boost.Test 也有針對其他情境,提供不同的巨集(例如針對不同資料、或是不同型別來測試),如果更複雜的情境、也可以使用更為手動的方法來自行定義 test case。

在 test case 中則是會有許多測試,這邊就是用「BOOST_TEST」來檢查 1 + 1 == 2 是否成立;這些測試最好是拆分到足夠細的程度、以確保在出錯的時候能明確地知道錯誤的地方、原因。

而這邊省略了 test suite、直接以「BOOST_AUTO_TEST_CASE」來定義 test case、命名為「first_test」;如果 test case 多的話,可以用 test suite 來做群組。

如果測試都正確的話,在執行時會出現下面的訊息:

Running 1 test case...

*** No errors detected

而如果測試有錯誤的話,在執行測試後則會顯示下面的訊息:

Running 1 test case...
S:/Test/test.cpp(6): error: in "first_test": check 1 + 1 == 3 has failed [1 + 1 != 3]

*** 1 failure is detected in the test module "My Test"

他會說明死在哪一行、哪一個 test case、以及該行在進行的測試項目。


使用 Test suite 建立群組

至於要怎麼使用 test suite 來把 test case 分群呢?最簡單的方法,就是透過「BOOST_AUTO_TEST_SUITE」和「BOOST_AUTO_TEST_SUITE_END」這組巨集。

下面就是一個簡單的例子:

#define BOOST_TEST_MODULE My Test
#include <boost/test/included/unit_test.hpp>
 
BOOST_AUTO_TEST_SUITE(the_suiteA)
 
  BOOST_AUTO_TEST_CASE(test_1)
  {
    BOOST_TEST(1 + 1 == 2);
  }
  
  BOOST_AUTO_TEST_CASE(test_2)
  {
    int i = 1;
    BOOST_TEST(i == 2);
  }
 
BOOST_AUTO_TEST_SUITE_END()
 
 
BOOST_AUTO_TEST_SUITE(the_suiteB)
 
  BOOST_AUTO_TEST_CASE(test_2)
  {
    int i = 1;
    BOOST_TEST(i == 2);
  }
 
BOOST_AUTO_TEST_SUITE_END()

這邊應該可以看的出來,只要在「BOOST_AUTO_TEST_SUITE」和「BOOST_AUTO_TEST_SUITE_END」中定義的 test case,就會屬於該群 test suite,使用上還算簡單。

而在這邊定義了 test_suiteAtest_suiteB 兩個 test suite,其中 test_suiteA 裡面有 test1test2 兩個 test case,test_suiteB 裡面則只有 test2 一個。

這樣的執行後,則可以看到他的會顯示有兩個錯誤,名稱會是「test_suite/test_case」的形式:

Running 3 test cases...
S:/Test/test.cpp(14): error: in "the_suiteA/test_2": check i == 2 has failed [1 != 2]
S:/Test/test.cpp(25): error: in "the_suiteB/test_2": check i == 2 has failed [1 != 2]

*** 2 failures are detected in the test module "My Test"

同時這邊也可以看到,如果是在用變數去做檢驗的時候,當發生錯誤的時候,他也會把變數的值印出來、方便確認的。

至於為什麼要使用 test suite 呢?這邊主要是可以透過 test suite 來針對不同群的 test case 做資料、執行與否的控制,當 test case 多的時候,會比較方便。不過這部分就之後有機會再提了。


不同層級的測試函式

前面的測試都是用最簡單的「BOOST_TEST」來做 true / false 的確認,而實際上 Boost Test 也還有提供一系列、有分層級的函式來讓使用者選用。

他的層級分成:

回報為 錯誤計數器 後續處理
WARN warning 不計 繼續執行
CHECK error +1 繼續執行
REQUIRE fatal +1 中斷 test case

所以其實最基本的測試巨集就有三個(官方文件):

  • BOOST_TEST_WARN
  • BOOST_TEST_CHECK
  • BOOST_TEST_REQUIRE

而前面所使用的 BOOST_TEST 實際上就是 BOOST_TEST_CHECK 了。

這三個巨集是最通用的版本,算是目前使用 Boost Test 的時候會建議使用的形式;在使用的時候,第一個引數都是給他一個可以轉換成 bool 的陳述(statement),雖然還是有些限制,但是通用性算是很好的了。

比如說下面這個例子:

#define BOOST_TEST_MODULE My Test
#include <boost/test/included/unit_test.hpp>
 
BOOST_AUTO_TEST_CASE(test_1)
{
  int i = 3;
  BOOST_TEST_WARN(i == 2);
  BOOST_TEST_REQUIRE(i == 2);
  BOOST_TEST_CHECK(i == 2);
}

這邊三個測試都是失敗的,但是結果會類似下面的狀況、只顯示一個失敗:

Running 1 test case...
S:/CppTest/CppTest/Source.cpp(8): fatal error: in "test_1": critical check i == 2 has failed [3 != 2]

*** 1 failure is detected in the test module "My Test"

這是因為第一個測試是用 WARN 下去測試,所以當失敗的時候,他只會當作警告,不算真的錯誤;再加上 Boost Test 預設不顯示警告,所以這邊完全看不出來。

而第二個測試則是用最強的 REQUIRE,在失敗的時候就直接中斷了,所以就不會執行第三個測試。

這也就是為什麼理論上三個錯誤的測試,他只會回報一個失敗的原因了。


在最簡單的狀況下,這邊也可以把 TEST 拿掉,直接用 BOOST_WARNBOOST_CHECKBOOST_REQUIRE 來簡化一點點。

而除了這三種較通用的版本外,Boost Test 其實也有其他針對特定功能做測試的巨集,像是 BOOST_<level>_EQUAL 就是用來比較兩個變數是否相等,BOOST_<level>_CLOSE 是用來比較兩個浮點數是否夠接近。

不過比較有趣的,這些巨集在 Boost Test 的 header 裡面似乎是被標記成「old tools」(在 header 的路徑還有定義裡面)?個人還滿好奇以後是不是會被拿掉?

這部分就先不提了,有興趣的話請參考官方文件(連結)。


測試時的額外資訊

BOOST_TEST_<level> 這系列的函式來說,其實都還支援第二個引數,可以做額外的設定。

最基本的,就是透過第二個引數來自定義輸出的測試結果;例如:

#define BOOST_TEST_MODULE My Test
#include <boost/test/included/unit_test.hpp>
 
BOOST_AUTO_TEST_CASE(test_1)
{
  int i = 3;
  BOOST_TEST_CHECK(i == 2, "The value is " << i << ", not 2");
}

這邊第二個引數可以和操作 ostream 的時候一樣,透過 operator<< 來輸出文字與變數;以上面的例子來說,結果就會是:

Running 1 test case...
S:/CppTest/CppTest/Source.cpp(7): error: in "test_1": The value is 3, not 2

*** 1 failure is detected in the test module "My Test"

所以有必要的話,也可以將錯誤訊息寫得更為明確、易懂。

此外,這邊也可以透過第二個引數來加上一些設定,其中比較實用的一個,應該是設定浮點數比較時的差異條件了~

下面是一個簡單的例子:

#define BOOST_TEST_MODULE My Test
#include <boost/test/included/unit_test.hpp>
 
BOOST_AUTO_TEST_CASE(test_1)
{
  float f = 1.0f / 3;
  BOOST_TEST_CHECK(0.3333f == f, boost::test_tools::tolerance(0.001f));
}

不過,在測試的時候總覺得有的時候有點怪?感覺上應該是不能把 floatdouble 混用,如果沒分清楚的話,似乎會有非預期的結果。

另外,如果是要比對兩個不同型別的容器(例如 vectorarray),則也可以透過加上 boost::test_tools::per_element() 來以各項元素逐一比較;下面是一個例子:

#define BOOST_TEST_MODULE My Test
#include <vector>
#include <array>
#include <boost/test/included/unit_test.hpp>
 
BOOST_AUTO_TEST_CASE(test_1)
{
  std::vector<float> a{ 1,2,3 };
  std::array b{ 1,2,3 };
  BOOST_TEST_CHECK(a==b, boost::test_tools::per_element());
}

而這邊要混合使用的話,其實也可以寫成下面的形式:

BOOST_TEST_CHECK(a == b, 
  boost::test_tools::tolerance(0.1) << boost::test_tools::per_element());

只不過使用上似乎還有順序上的限制之類的?這部分個人也沒有搞得很懂,感覺也不是很好用…

其他也可以要求系統用 bitwise 等方法來檢查,這部分的完整說明可以參考官方文件


不同的使用方式

上面的例子實際上是透過 header-only 的模式來使用 Boost Test,而 Boost Test 也是可以透過預先建置好的函式庫來使用的,同時也可以選擇要使用動態函式庫(shared library、DLL)或靜態函式庫(static library)。

以 header-only 模式來說,他的 header 檔案路徑會是「boost/test/included/」,使用這個路徑的 header 就會是 header-only 的形式。

而如果使用「boost/test/」這個路徑下的 header 檔的話,則就會是使用預先建置好的函式庫,而且預設會是使用靜態函式庫。

如果想要用動態函式庫(DLL)的話,則是要在 include 相關的 header 前,加入 BOOST_TEST_DYN_LINK 這個巨集;下面就是一個例子:

#define BOOST_TEST_MODULE test module name
#define BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>

這部分的資料可以參考官方文件


基本的使用大概就先這樣了,這系列預計會有很多篇,但是會寫到什麼程度…就再看看吧。

下面是本系列的目錄(暫定):

  • 使用 Boost Test 開發 C++ 的單元測試:基本使用
  • 執行 Boost Test 的測試:Visual Studio 與 GitLab
  • Boost Test 的資料導向測試
  • Boost Test 的 template 測試

Leave a Reply

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