首頁:https://github.com/mity/acutest
Acutest 是一種 C/C++ 單元測試工具,旨在盡可能簡單,不妨礙開發人員並最大限度地減少任何外部依賴性。
為了實現這一點,完整的實作位於單一 C 頭檔中,其核心僅依賴少數標準 C 函式庫函數。
主要特點:
acutest.h
。main()
)。--tap
選項)。--xml-output=FILE
)。C++具體特徵:
std::exception
,則在錯誤訊息中寫出what()
。Unix/Posix 特定功能:
clock_gettime()
),使用者可以使用--time=real
(與--time
相同)和--time=cpu
測量測試執行時間。Linux具體特色:
Windows 特定功能:
--time
測量測試執行時間。macOS 特定功能:
任何實作一個或多個單元測試(包括acutest.h
)的C/C++模組都可以建構為獨立程式。出於本文檔的目的,我們將產生的二進位檔案稱為「測試套件」。然後執行該套件來運行測試,按照其命令列選項的指定。
我們說任何單元測試成功當且僅當:
要使用 Acutest,只需在實作一個或多個單元測試的 C/C++ 原始檔的開頭包含頭檔acutest.h
即可。請注意,標頭提供了main()
函數的實作。
#include "acutest.h"
每個測試都應該作為具有以下原型的函數來實現:
void test_example ( void );
測試可以使用一些預處理器巨集來驗證測試條件。它們可以多次使用,如果其中任何一個條件失敗,則特定測試被視為失敗。
TEST_CHECK
是最常用的測試宏,它僅測試布林條件,如果條件計算結果為 false(或零),則失敗。
例如:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_CHECK ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_CHECK ( mem != NULL );
}
請注意,測試應該完全相互獨立。每當呼叫測試套件時,使用者都可以以任何順序在套件中執行任意數量的測試。此外,預設情況下,在支援的平台上,每個單元測試都作為獨立(子)進程執行。
最後,測試套件來源檔必須使用宏TEST_LIST
列出單元測試。此清單指定每個測試的名稱(必須是唯一的)和指向實現該測試的函數的指標。我推薦在命令列上易於使用的名稱:特別是避免空格和其他可能需要在 shell 中轉義的特殊字元;也要避免將破折號 ( -
) 作為名稱的第一個字符,因為它可能會被解釋為命令列選項而不是測試名稱。
TEST_LIST = {
{ "example" , test_example },
...
{ NULL, NULL } /* zeroed record marking the end of the list */
};
請注意,測試清單必須以歸零記錄結束。
對於基本的測試套件,這或多或少是您需要了解的全部內容。然而,Acutest 提供了更多的宏,這些宏在某些特定情況下很有用。我們將在以下小節中介紹它們。
有一個巨集TEST_ASSERT
與TEST_CHECK
非常相似,但如果失敗,它會立即中止目前單元測試的執行。
例如:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_ASSERT ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_ASSERT ( mem != NULL );
}
失敗時的中止可以透過呼叫abort()
(如果測試是作為子程序執行)或透過longjmp()
(如果不是)來執行。
因此,只有當您了解如此殘酷的中止測試所帶來的成本時,才應使用它。根據您的單元測試的作用,它可能包括未刷新的檔案描述符、記憶體洩漏、在沒有呼叫其析構函數的情況下被破壞的 C++ 物件等等。
一般來說, TEST_CHECK
應該優於TEST_ASSERT
,除非您確切地知道自己在做什麼以及為什麼在某些特定情況下選擇TEST_ASSERT
。
對於 C++,有一個額外的巨集TEST_EXCEPTION
用於驗證給定程式碼(通常只是函數或方法呼叫)是否會拋出預期類型的例外。
如果函數沒有拋出任何異常或拋出任何不相容的內容,則檢查失敗。
例如:
void test_example ( void )
{
TEST_EXCEPTION ( CallSomeFunction (), std:: exception );
}
如果條件檢查失敗,提供一些有關情況的附加資訊通常很有用,這樣問題就更容易偵錯。 Acutest 為此提供了宏TEST_MSG
和TEST_DUMP
。
前一個輸出任何類似printf
的訊息,另一個輸出所提供記憶體區塊的十六進位轉儲。
例如:
void test_example ( void )
{
char produced [ 100 ];
char expected [] = "Hello World!" ;
SomeSprintfLikeFunction ( produced , "Hello %s!" , "world" );
TEST_CHECK ( strcmp ( produced , expected ) == 0 );
TEST_MSG ( "Expected: %s" , expected );
TEST_MSG ( "Produced: %s" , produced );
/* Or, if the function could provide some binary stuff, we might rather
* use TEST_DUMP instead in order to output a hexadecimal dump of the data.
*/
TEST_DUMP ( "Expected:" , expected , strlen ( expected ));
TEST_DUMP ( "Produced:" , produced , strlen ( produced ));
}
請注意,只有當最近檢查的巨集失敗時,兩個巨集才會輸出任何內容。換句話說,這兩者是等價的:
if (! TEST_CHECK ( some_condition != 0 ))
TEST_MSG ( "some message" );
TEST_CHECK ( some_condition != 0 );
TEST_MSG ( "some message" );
(請注意, TEST_MSG
需要具有可變參數巨集支援的編譯器。)
有時,將測試函數設計為資料循環,提供測試向量及其各自的預期輸出的集合是很有用的。例如,假設我們的單元測試應該驗證某種雜湊函數的實現,並且我們在雜湊規範中得到了它的測試向量集合。
在這種情況下,取得與每個測試向量相關的名稱並在輸出日誌中輸出該名稱非常有用,這樣如果任何檢查失敗,就可以輕鬆識別有罪的測試向量。然而,循環體可能會執行數十個檢查宏,因此添加這樣的名稱來自訂循環中的每個檢查訊息可能是不切實際的。
為了解決這個問題,Acutest 提供了宏TEST_CASE
。此巨集指定一個字串作為測試向量名稱。使用時,Acutest 確保在輸出日誌中提供的名稱位於後續條件檢查的任何訊息之前。
例如,假設我們正在測試SomeFunction()
,對於給定的某個大小的位元組數組,它應該會在新的malloc
緩衝區中傳回另一個位元組數組。然後我們可以做這樣的事情:
struct TestVector {
const char * name ;
const uint8_t * input ;
size_t input_size ;
const uint8_t * expected_output ;
size_t expected_output_size ;
};
struct TestVector test_vectors [] = {
/* some data */
};
void test_example ( void )
{
int i ;
const uint8_t * output ;
size_t output_size ;
for ( i = 0 ; i < sizeof ( test_vectors ) / sizeof ( test_vectors [ 0 ]); i ++ ) {
struct TestVector * vec = & test_vectors [ i ];
/* Output the name of the tested test vector. */
TEST_CASE ( vec . name );
/* Now, we can check the function produces what it should for the
* current test vector. If any of the following checking macros
* produces any output (either because the check fails, or because
* high `--verbose` level is used), Acutest also outputs the currently
* tested vector's name. */
output = SomeFunction ( vec -> input , vec -> input_size , & output_size );
if ( TEST_CHECK ( output != NULL )) {
TEST_CHECK ( output_size == vec -> expected_output_size );
TEST_CHECK ( memcmp ( output , vec -> expected_output , output_size ) == 0 );
free ( output );
}
}
}
指定的名稱適用於使用TEST_CASE
後執行的所有檢查
TEST_CASE
指定另一個名稱;或者TEST_CASE
並以NULL
作為參數明確重置名稱。前面幾節中提到的許多宏都有一個對應的宏,它允許輸出自訂訊息而不是一些預設訊息。
所有這些都與上述巨集具有相同的名稱,只是添加了下劃線後綴。有了後綴,他們就會期望類似printf
的字串格式和對應的附加參數。
因此,例如,而不是簡單的檢查宏
TEST_CHECK (a == b);
TEST_ASSERT (x < y);
TEST_EXCEPTION (SomeFunction(), std::exception);
我們可以將它們各自的對應部分與自訂訊息一起使用:
TEST_CHECK_ (a == b, " %d is equal to %d " , a, b);
TEST_ASSERT_ (x < y, " %d is lower than %d " , x, y);
TEST_EXCEPTION_ (SomeFunction(), std::exception, "SomeFunction() throws std::exception");
您應該對它們使用一些中性措辭,因為使用命令列選項--verbose
,即使相應的檢查成功通過,訊息也會被註銷。
(如果您需要在檢查失敗的情況下輸出一些診斷訊息,請使用巨集TEST_MSG
。這正是它的目的。)
同樣,代替
TEST_CASE ( "name" );
我們可以使用更豐富的
TEST_CASE_ ( "iteration #%d" , 42 );
但請注意,只有當您的編譯器支援可變參數預處理器巨集時,才能使用所有這些。隨著 C99 的出現,可變參數巨集成為 C 語言的標準部分。
當我們完成測試後,我們可以簡單地將其編譯為一個簡單的 C/C++ 程式。例如,假設cc
是您的 C 編譯器:
$ cc test_example.c -o test_example
編譯測試套件後,產生的測試二進位檔案可用於執行測試。
如果所有執行的單元測試都通過,則測試套件的退出代碼為 0;如果其中任何一個測試失敗,則測試套件的退出代碼為 1;如果發生內部錯誤,則測試套件的退出代碼為任何其他數字。
預設情況下(沒有任何命令列選項),它運行所有已實現的單元測試。它還可以僅運行命令列上指定的單元測試的子集:
$ ./test_example # Runs all tests in the suite
$ ./test_example test1 test2 # Runs only tests specified
$ ./test_example --exclude test3 # Runs all tests but those specified
請注意,單一命令列參數可以選擇一整組測試單元,因為 Acutest 實現了多個層級的單元測試選擇(使用至少匹配一個測試單元的第一個):
完全匹配:當參數與單元測試的整個名稱完全匹配時,則僅選擇給定的測試。
單字匹配:當參數與任何完整的測試名稱不匹配,但與一個或多個測試名稱中的整個單字匹配時,則選擇所有此類測試。
以下字元被辨識為單字分隔符號:空格
、製表符t
、破折號-
、底線_
、斜線/
、點.
, 逗號,
, 冒號:
, 分號;
。
子字串匹配:如果連單字匹配都無法選擇任何測試,則選擇名稱中包含參數作為其子字串的所有測試。
透過採用適當的測試命名策略,這允許使用者使用單一命令列參數輕鬆運行(或如果使用--exclude
則跳過)整個相關測試系列。
例如,考慮測試套件test_example
它實作了測試foo-1
、 foo-2
、 foomatic
、 bar-1
和bar-10
:
$ ./test_example bar-1 # Runs only the test 'bar-1' (exact match)
$ ./test_example foo # Runs 'foo-1' and 'foo-2' (word match)
$ ./test_example oo # Runs 'foo-1', 'foo-2' and 'foomatic' (substring match)
$ ./test_example 1 # Runs 'foo-1' and 'bar-1' (word match)
您可以使用--list
或-l
來僅列出給定測試套件實現的所有單元測試:
$ ./test_example --list
若要查看所有支援的命令列選項的說明,請使用選項--help
執行二進位檔案:
$ ./test_example --help
Q:這個專案不是叫「CUTest」嗎?
答:是的。由於發現原來的名稱過於重載,因此已對其進行重命名。
Q:我需要分發檔案README.md
和/或LICENSE.md
嗎?
答: acutest.h
。只要您保持這些完整,如果您僅將標頭添加到項目中,我們就完全沒問題。畢竟,它的簡單使用和一體化標頭性質是我們的主要目標。
Acutest 受 MIT 授權保護,請參閱文件LICENSE.md
或acutest.h
開頭以取得其全文。
該專案位於github上:
您可以在那裡找到最新版本的 Acutest,貢獻增強功能或報告錯誤。