單元測試
Godot 引擎允許直接用 C++ 撰寫單元測試。引擎整合了 doctest 單元測試框架,能讓你在生產程式碼旁撰寫測試套件與測試案例。不過由於 Godot 的測試經由不同的 main 進入點執行,因此測試改放在引擎原始碼根目錄下專用的 tests/ 資料夾。
平台與目標支援
C++ 單元測試可在 Linux、macOS 與 Windows 作業系統上執行。
僅在啟用編輯器 tools 時才能執行測試,代表目前無法針對匯出範本進行測試。
執行測試
在實際執行測試前,必須以啟用 tests 編譯選項(以及你慣用的其他選項)來編譯引擎,因為預設情況下測試不會納入引擎本身:
scons tests=yes
編譯完成後,使用 --test 命令列選項執行測試:
./bin/<godot_binary> --test
測試執行時可透過多種 doctest 專用命令列選項來設定。若要取得所有支援的選項,可在 --test 命令後加上 --help:
./bin/<godot_binary> --test --help
--test 命令之後的所有選項與參數,都會作為 doctest 的參數。
備註
如果你在 SCons 編譯時加上 dev_mode=yes,測試會自動編譯。如果你有意貢獻引擎開發,建議使用 dev_mode=yes,因為它會把編譯警告自動視為錯誤。持續整合(CI)系統偵測到任何警告都會讓檢查失敗,因此請於發送 pull request 前修正所有警告。
篩選測試
預設狀況下,若在 --test 指令後未加任何參數,會執行所有測試。但若你正在撰寫新測試,或想針對特定測試進行偵錯並觀察其成功判斷提示輸出,可以搭配 doctest 提供的各種篩選選項來執行特定測試。
支援萬用字元 * 語法,可在測試組、測試案例、原始檔案名稱中配對任意字元:
篩選選項 |
縮寫 |
範例 |
|
|
|
|
|
|
|
|
|
例如,只執行 String 單元測試時,請執行:
./bin/<godot_binary> --test --test-case="*[String]*"
可加上 --success``(縮寫 ``-s)選項來顯示成功判斷提示的輸出,也可以與前述篩選選項組合,例如:
./bin/<godot_binary> --test --source-file="*test_color*" --success
可用對應的 -exclude 選項來略過特定測試。目前有部分測試屬於隨機壓力測試,執行時間較長。若要略過這些測試,可執行以下命令:
./bin/<godot_binary> --test --test-case-exclude="*[Stress]*"
撰寫測試
測試組對應 C++ 標頭檔(header),必須包含在主要測試進入點 tests/test_main.cpp 之中。大多數測試組直接放在 tests/ 資料夾下。
所有測試標頭檔名稱都以 test_ 為前綴,這是 Godot 編譯系統用來自動偵測引擎測試的命名慣例。
以下是一個僅含一個測試案例的最基本測試組範例:
#pragma once
#include "tests/test_macros.h"
namespace TestString {
TEST_CASE("[String] Hello World!") {
String hello = "Hello World!";
CHECK(hello == "Hello World!");
}
} // namespace TestString
備註
你可以用 tests/ 目錄下的 create_test.py 腳本快速產生新測試。這個腳本會在正確的位置自動建立含有必要模板程式碼的新測試檔案,也可用侵入式模式(-i 參數)自動在 tests/test_main.cpp 加入新標頭檔。欲查看使用說明,請加上 -h 參數執行該腳本。
tests/test_macros.h 標頭檔包涵了在 Godot 撰寫 C++ 單元測試所需的一切,包括如上例的 doctest 判斷提示宏(如 CHECK),以及撰寫測試案例所需的定義。
也參考
tests/test_macros.h 提供目前所有巨集與別名的原始碼。
測試案例需使用 TEST_CASE 類函式巨集建立。每個測試案例必須在括號內加上簡要描述,也可選擇性附加自訂標籤(如 [String]、[Stress]),以便於執行時過濾測試。
建議將測試案例寫在專用命名空間內,雖然不是強制,但這樣能避免你為了重複測試流程(如共用測試資料、參數化測試等)編寫輔助函式時發生命名衝突。
Godot 支援針對 C++ 模組撰寫測試。如何撰寫模組測試,請參考 撰寫自訂單元測試。
子案例
若多個測試案例僅有細微差異且共用大部分設定,可善用子案例(subcases)簡化程式碼。以下為範例:
TEST_CASE("[SceneTree][Node] Testing node operations with a very simple scene tree") {
// ... common setup (e.g. creating a scene tree with a few nodes)
SUBCASE("Move node to specific index") {
// ... setup and checks for moving a node
}
SUBCASE("Remove node at specific index") {
// ... setup and checks for removing a node
}
}
每個 SUBCASE 都會讓 TEST_CASE 從頭開始執行。子案例可任意巢狀,但建議最多只巢狀一層。
判斷提示
以下為 Godot 測試常用的判斷提示,依嚴重性排序。
判斷提示 |
說明 |
|
測試條件是否為真。若條件不成立,立即使整個測試失敗。 |
|
測試條件是否為假。若條件成立,立即使整個測試失敗。 |
|
測試條件是否為真。若失敗會標記該測試為失敗,但仍繼續執行其他判斷提示。 |
|
測試條件是否為假。若失敗會標記該測試為失敗,但仍繼續執行其他判斷提示。 |
|
測試條件是否為真。不論條件如何都不會使測試失敗,但若條件不成立則會記錄警告訊息。 |
|
測試條件是否為假。不論條件如何都不會使測試失敗,但若條件成立則會記錄警告訊息。 |
上述所有判斷提示皆有對應 *_MESSAGE 巨集,可用來輸出補充說明訊息。
若判斷提示意義明確,建議用 CHECK,較複雜且需補充說明之處則用 CHECK_MESSAGE。
也參考
記錄
測試輸出由 doctest 處理,完全不依賴 Godot 內建的列印或記錄功能,因此建議使用專用巨集以 doctest 格式記錄測試輸出。
巨集 |
說明 |
|
輸出一則訊息。 |
|
將測試標記為失敗,但會繼續執行。可用於條件判斷中處理複雜檢查。 |
|
立即使測試失敗。可用於條件判斷中進行複雜檢查。 |
執行時可指定不同輸出格式,舉例來說,以下指令可將輸出導向 XML 檔案:
./bin/<godot_binary> --test --source-file="*test_validate*" --success --reporters=xml --out=doctest.txt
也參考
測試失敗路徑
有時無法總是測試「預期」結果。根據 Godot 的開發理念,引擎不應當機,遇到非致命錯誤時須能正常恢復,因此必須確認這些失敗路徑確實能安全執行,不會導致引擎當機。
「非預期」行為同樣可以用一般方式測試。唯一的問題是,這些錯誤訊息會讓測試輸出混入引擎本身產生的錯誤(即使測試結論是成功)。
為避免上述問題,可在測試案例中直接用 ERR_PRINT_OFF 與 ERR_PRINT_ON 巨集,暫時關閉來自引擎的錯誤輸出,例如:
TEST_CASE("[Color] Constructor methods") {
ERR_PRINT_OFF;
Color html_invalid = Color::html("invalid");
ERR_PRINT_ON; // Don't forget to re-enable!
CHECK_MESSAGE(html_invalid.is_equal_approx(Color()),
"Invalid HTML notation should result in a Color with the default values.");
}
測試訊號
可用下列巨集來測試訊號:
巨集 |
說明 |
|---|---|
|
開始監看指定物件的訊號。 |
|
停止監看指定物件的訊號。 |
|
檢查所有已發出的訊號參數。外層 Vector 代表每次發出的訊號,內層 Vector 則為該次訊號帶入的參數清單。訊號的順序具有意義。 |
|
檢查指定訊號是否未被發送。 |
|
丟棄指定訊號的所有紀錄。 |
以下為這些巨集用法的範例:
//...
SUBCASE("[Timer] Timer process timeout signal must be emitted") {
SIGNAL_WATCH(test_timer, SNAME("timeout"));
test_timer->start(0.1);
SceneTree::get_singleton()->process(0.2);
Array signal_args;
signal_args.push_back(Array());
SIGNAL_CHECK(SNAME("timeout"), signal_args);
SIGNAL_UNWATCH(test_timer, SNAME("timeout"));
}
//...
測試工具
測試工具是進階用法,可讓你執行自訂流程,方便手動測試與偵錯引擎內部運作。
這些工具可在 --test 命令列選項後指定工具名稱來執行。例如 GDScript 模組便實作並註冊了多個工具,協助偵錯分詞器、語法剖析器與編譯器:
./bin/<godot_binary> --test gdscript-tokenizer test.gd
./bin/<godot_binary> --test gdscript-parser test.gd
./bin/<godot_binary> --test gdscript-compiler test.gd
若有偵測到這類工具,其餘單元測試會被略過。
測試工具可於引擎各處註冊,註冊機制與 doctest 註冊測試案例(採用動態初始化)類似,通常建議在各模組或核心的 register_types.cpp 檔案中註冊。
以下是 GDScript 在 modules/gdscript/register_types.cpp 註冊測試工具的範例:
#ifdef TESTS_ENABLED
void test_tokenizer() {
TestGDScript::test(TestGDScript::TestType::TEST_TOKENIZER);
}
void test_parser() {
TestGDScript::test(TestGDScript::TestType::TEST_PARSER);
}
void test_compiler() {
TestGDScript::test(TestGDScript::TestType::TEST_COMPILER);
}
REGISTER_TEST_COMMAND("gdscript-tokenizer", &test_tokenizer);
REGISTER_TEST_COMMAND("gdscript-parser", &test_parser);
REGISTER_TEST_COMMAND("gdscript-compiler", &test_compiler);
#endif
測試工具也可自行透過 OS 的 get_cmdline_args 方法進行自訂命令列解析。
GDScript 整合測試
Godot 利用 doctest 防止 GDScript 開發過程中出現回歸錯誤。可撰寫多種類型的測試腳本:
預期錯誤測試;
警告測試;
功能測試。
因此,GDScript 的整合測試流程如下:
選擇欲撰寫的測試腳本類型,在
modules/gdscript/tests/scripts下對應子資料夾建立新的 GDScript 檔案。撰寫 GDScript 程式碼。測試腳本需有一個無參數的
test()函式,該函式將由測試運作器自動呼叫。測試不應有外部依賴,除非該依賴本身也是測試的一部分。全域類別(使用class_name)會在運作器啟動前註冊,因此可於測試內使用。以下為範例測試腳本:
func test(): if true # Missing colon here. print("true")
切換目錄至 Godot 原始碼倉庫根目錄。
cd godot
產生
*.out檔案以更新預期輸出結果:bin/<godot_binary> --gdscript-generate-tests modules/gdscript/tests/scripts
你可以加上 --print-filenames 選項來顯示產生測試輸出時的檔案名稱。若在開發新功能出現嚴重當機,可用此選項快速找出是哪個測試檔造成當機,方便後續偵錯。
以以下指令執行 GDScript 測試:
./bin/<godot_binary> --test --test-suite="*GDScript*"
此指令同樣支援 --print-filenames 選項(見上文)。
若未出現錯誤訊息且一切順利,代表測試完成!
警告
提交 pull request 前,請先確保輸出結果皆為預期值。若 --gdscript-generate-tests 產生的 *.out 檔與新測試無關,請勿提交,僅須提交與新測試相關的 *.out 檔案。
備註
GDScript 測試運作器僅用於測試 GDScript 實作本身,並非用來測試使用者腳本或透過腳本測試引擎本體。建議針對 GitHub 上已解決的 GDScript 相關議題 撰寫新測試,或針對現有功能撰寫測試。
備註
若你的測試案例需要腳本中不含 test() 函式,可將腳本命名為 *.notest.gd 來停用執行階段測試區段,例如:test_empty_file.notest.gd。