使用 CMake 和 CTest 進行測試¶
測試是產生和維護穩健、有效軟體的關鍵工具。本章將探討 CMake 中用於支援軟體測試的工具。我們將從簡要討論測試方法開始,然後討論如何使用 CMake 將測試新增到您的軟體專案中。
軟體套件的測試可以採用多種形式。在最基本的層面上,有冒煙測試,例如簡單地驗證軟體是否編譯的測試。雖然這看起來像是一個簡單的測試,但由於平台和配置的多樣性,冒煙測試比任何其他類型的測試都能發現更多的問題。另一種形式的冒煙測試是驗證測試是否在沒有崩潰的情況下執行。這對於開發人員不想花時間創建更複雜的測試,但願意執行一些簡單測試的情況非常方便。大多數時候,這些簡單的測試可以是小型範例程式。執行它們不僅驗證了建置是否成功,還驗證了是否可以載入任何必要的共享程式庫(對於使用它們的專案),並且至少可以執行某些程式碼而不崩潰。
從基本的冒煙測試進一步發展,可以進行更具體的測試,例如迴歸測試、黑箱測試和白箱測試。它們各自都有其優點。迴歸測試驗證測試結果不會隨著時間或平台而改變。當頻繁執行時,這非常有用,因為它可以快速檢查軟體的行為和結果是否發生變化。當迴歸測試失敗時,快速查看最近的程式碼變更通常可以找出問題所在。不幸的是,迴歸測試通常比其他測試需要更多的工作才能建立。
白箱測試和黑箱測試是指分別在知道和不知道這些單元如何實作的情況下,編寫用於測試程式碼單元(在不同整合層級)的測試。白箱測試旨在了解程式碼的編寫方式及其弱點,來強調程式碼中潛在的失敗點。與迴歸測試一樣,這可能需要大量的工作才能創建好的測試。黑箱測試通常對軟體的實作知之甚少,僅了解其公共 API。黑箱測試可以在不費太多力氣開發測試的情況下,提供大量的程式碼覆蓋率。對於 API 定義良好的物件導向軟體庫而言,尤其如此。可以編寫黑箱測試來遍歷並調用軟體中所有類別上的許多典型方法。
我們將討論的最後一種測試類型是軟體標準合規性測試。雖然我們討論的其他測試類型側重於確定程式碼是否正常運作,但合規性測試試圖確定程式碼是否符合軟體專案的編碼標準。這可以是檢查以驗證所有類別是否都實作了某些關鍵方法,或者所有函式是否都具有共同的前綴。此類型測試的選項是無限的,並且有多種方法可以執行此類測試。可以使用軟體分析工具,也可以編寫專門的測試程式(可能是 Python 腳本等)。要意識到的關鍵點是,測試不一定需要執行軟體的某些部分。測試可能會在原始程式碼本身上執行其他工具。
將測試支援整合到建置過程中有很多原因。首先,複雜的軟體專案可能具有許多配置或平台相關的選項。建置系統知道哪些選項可以啟用,然後可以針對這些選項啟用適當的測試。例如,Visualization Toolkit (VTK) 包含對名為 MPI 的並行處理程式庫的支援。如果使用 MPI 支援建置 VTK,則會啟用其他使用 MPI 的測試,並驗證 VTK 中特定於 MPI 的程式碼是否按預期運作。其次,建置系統知道可執行檔將放置在哪裡,並且它具有尋找其他所需可執行檔(例如 perl、python 等)的工具。第三個原因是,在 UNIX Makefiles 中,通常會在 Makefile 中設定測試目標,以便開發人員可以輸入 make test 並執行測試。為了使此功能正常運作,建置系統必須對測試過程有一定的了解。
CMake 如何促進測試?¶
CMake 通過特殊的測試命令和 CTest
可執行檔來促進您的軟體測試。首先,我們將討論 CMake 中的關鍵測試命令。要將測試新增到基於 CMake 的專案,只需 include(CTest)
並使用 add_test
命令。add_test
命令具有如下簡單語法
add_test(NAME TestName COMMAND ExecutableToRun arg1 arg2 ...)
第一個引數只是測試的字串名稱。這是測試程式將顯示的名稱。第二個引數是要執行的可執行檔。可執行檔可以作為專案的一部分建置,也可以是獨立的可執行檔,例如 python、perl 等。其餘引數將傳遞給正在執行的可執行檔。使用 add_test
命令進行測試的典型範例將如下所示
add_executable(TestInstantiator TestInstantiator.cxx)
target_link_libraries(TestInstantiator vtkCommon)
add_test(NAME TestInstantiator
COMMAND TestInstantiator)
add_test
命令通常放置在具有測試的目錄的 CMakeLists 檔案中。對於大型專案,可能有多個 CMakeLists 檔案在其中包含 add_test
命令。一旦專案中存在 add_test
命令,使用者可以透過調用 Makefile 的「測試」目標,或 Visual Studio 或 Xcode 的 RUN_TESTS
目標來執行測試。以下是在 Linux 上使用 Makefile 產生器執行 CMake 測試的範例
$ make test
Running tests...
Test project
Start 2: kwsys.testEncode
1/20 Test #2: kwsys.testEncode .......... Passed 0.02 sec
Start 3: kwsys.testTerminal
2/20 Test #3: kwsys.testTerminal ........ Passed 0.02 sec
Start 4: kwsys.testAutoPtr
3/20 Test #4: kwsys.testAutoPtr ......... Passed 0.02 sec
其他測試屬性¶
預設情況下,如果滿足以下所有條件,則測試通過
已找到測試可執行檔
測試執行時沒有例外
測試以傳回碼 0 結束
也就是說,可以使用 set_property
命令來修改這些行為
set_property(TEST test_name
PROPERTY prop1 value1 value2 ...)
此命令將為指定的測試設定其他屬性。範例屬性包括
ENVIRONMENT
指定執行測試時應定義的環境變數。如果設定為
MYVAR=value
形式的環境變數和值的清單,則這些環境變數將在測試執行時定義。測試完成後,環境將還原為先前的狀態。LABELS
指定與測試相關聯的文字標籤清單。這些標籤可用於根據它們測試的內容將測試分組在一起。例如,您可以將 MPI 標籤新增到所有執行 MPI 程式碼的測試中。
WILL_FAIL
如果將此選項設定為 true,則當傳回碼不是 0 時測試將通過,如果是 0 則測試將失敗。這會反轉通過要求的第三個條件。
PASS_REGULAR_EXPRESSION
如果指定此選項,則會根據提供的正規表示式檢查測試的輸出(也可以傳入正規表示式的清單)。如果沒有任何正規表示式符合,則測試將失敗。如果至少有一個符合,則測試將通過。
FAIL_REGULAR_EXPRESSION
如果指定此選項,則會根據提供的正規表示式檢查測試的輸出(也可以傳入正規表示式的清單)。如果沒有任何正規表示式符合,則測試將通過。如果至少有一個符合,則測試將失敗。
如果同時指定 PASS_REGULAR_EXPRESSION
和 FAIL_REGULAR_EXPRESSION
,則 FAIL_REGULAR_EXPRESSION
優先。以下範例說明如何使用 PASS_REGULAR_EXPRESSION
和 FAIL_REGULAR_EXPRESSION
add_test (NAME outputTest COMMAND outputTest)
set (passRegex "^Test passed" "^All ok")
set (failRegex "Error" "Fail")
set_property (TEST outputTest
PROPERTY PASS_REGULAR_EXPRESSION "${passRegex}")
set_property (TEST outputTest
PROPERTY FAIL_REGULAR_EXPRESSION "${failRegex}")
使用 CTest 進行測試¶
當您從建置環境執行測試時,實際發生的情況是建置環境執行 CTest
。CTest
是一個隨 CMake 一起提供的可執行檔;它處理專案的測試執行。雖然 CTest 與 CMake 配合良好,但您不必為了使用 CTest 而使用 CMake。CTest 的主要輸入檔案稱為 CTestTestfile.cmake
。此檔案將在每個由 CMake 處理的目錄中建立(通常是每個具有 CMakeLists 檔案的目錄)。CTestTestfile.cmake
的語法類似於常規 CMake 語法,但只有部分命令可用。如果使用 CMake 來產生測試檔案,它們將列出需要處理的任何子目錄以及任何 add_test
呼叫。子目錄是那些由 add_subdirectory
命令加入的目錄。然後 CTest 可以解析這些檔案,以決定要執行的測試。下面顯示了這樣一個檔案的範例。
# CMake generated Testfile for
# Source directory: C:/CMake
# Build directory: C:/CMakeBin
#
# This file includes the relevant testing commands required
# for testing this directory and lists subdirectories to
# be tested as well.
add_test (SystemInformationNew ...)
add_subdirectory (Source/kwsys)
add_subdirectory (Utilities/cmzlib)
...
當 CTest 解析 CTestTestfile.cmake
檔案時,它會從中提取測試清單。這些測試將會被執行,而對於每個測試,CTest 將顯示測試的名稱及其狀態。請考慮以下範例輸出:
$ ctest
Test project C:/CMake-build26
Start 1: SystemInformationNew
1/21 Test #1: SystemInformationNew ...... Passed 5.78 sec
Start 2: kwsys.testEncode
2/21 Test #2: kwsys.testEncode .......... Passed 0.02 sec
Start 3: kwsys.testTerminal
3/21 Test #3: kwsys.testTerminal ........ Passed 0.00 sec
Start 4: kwsys.testAutoPtr
4/21 Test #4: kwsys.testAutoPtr ......... Passed 0.02 sec
Start 5: kwsys.testHashSTL
5/21 Test #5: kwsys.testHashSTL ......... Passed 0.02 sec
...
100% tests passed, 0 tests failed out of 21
Total Test time (real) = 59.22 sec
CTest 從您的建置樹內執行。它將執行在目前目錄中找到的所有測試,以及 CTestTestfile.cmake
中列出的任何子目錄。對於執行的每個測試,CTest 都會報告測試是否通過以及執行測試所花費的時間。
CTest 可執行檔包含一些方便的命令列選項,讓測試更容易進行。我們將首先看看您通常會從命令列使用的選項。
-R <regex> Run tests matching regular expression
-E <regex> Exclude tests matching regular expression
-L <regex> Run tests with labels matching the regex
-LE <regex> Run tests with labels not matching regexp
-C <config> Choose the configuration to test
-V,--verbose Enable verbose output from tests.
-N,--show-only Disable actual execution of tests.
-I [Start,End,Stride,test#,test#|Test file]
Run specific tests by range and number.
-H Display a help message
-R
選項可能是最常用的選項。它允許您指定一個正規表示式;只會執行名稱與正規表示式相符的測試。將 -R
選項與測試的名稱(或部分名稱)一起使用是執行單一測試的快速方法。-E
選項類似,只是它會排除所有與正規表示式相符的測試。-L
和 -LE
選項與 -R
和 -E
類似,只是它們適用於先前使用 set_property
命令設定的測試標籤。 -C
選項主要用於 IDE 建置,在同一樹中可能有多個組態,例如 Release 和 Debug。-C
後面的參數決定要測試哪個組態。-V
參數在您嘗試確定測試失敗的原因時非常有用。使用 -V
,CTest 將列印出用來執行測試的命令列,以及測試本身的任何輸出。-V
選項可以與 CTest 的任何調用一起使用,以提供更詳細的輸出。-N
選項在您想要查看 CTest 會執行哪些測試,但實際上不執行它們時非常有用。
在將任何變更提交到軟體之前,執行測試並確保它們全部通過是提高軟體品質和開發流程的可靠方法。不幸的是,對於大型專案,測試數量和執行它們所需的時間可能會令人望而卻步。在這些情況下,可以使用 CTest 的 -I
選項。-I
選項允許您彈性地指定要執行的測試子集。例如,以下 CTest 調用將執行每第七個測試。
ctest -I ,,7
雖然這不如執行每個測試那麼好,但它比不執行任何測試要好,並且對於許多開發人員來說可能是一個更實際的解決方案。請注意,如果未指定開始和結束參數,如本範例中所示,則它們將預設為第一個和最後一個測試。在另一個範例中,假設您總是想執行一些測試加上其他測試的子集。在這種情況下,您可以將這些測試明確地加到 -I
的參數末尾。例如:
ctest -I ,,5,1,2,3,10
將執行測試 1、2、3 和 10,加上每第五個測試。您可以在步幅參數後傳遞任意數量的測試編號。
使用 CTest 來驅動複雜測試¶
有時,為了正確測試專案,您需要在測試階段實際編譯程式碼。這有幾個原因。首先,如果測試程式是作為主專案的一部分編譯的,它們最終會佔用大量的建置時間。此外,如果測試建置失敗,則主建置也不應失敗。最後,IDE 專案可能會很快變得太大而無法載入和使用。CTest 命令支援一組命令列選項,允許將其用作要執行的測試可執行檔。當用作測試可執行檔時,CTest 可以執行 CMake、執行編譯步驟,最後執行已編譯的測試。我們現在將看看 CTest 的命令列選項,這些選項支援建置和執行測試。
--build-and-test src_directory build_directory
Run cmake on the given source directory using the specified build directory.
--test-command Name of the program to run.
--build-target Specify a specific target to build.
--build-nocmake Run the build without running cmake first.
--build-run-dir Specify directory to run programs from.
--build-two-config Run cmake twice before the build.
--build-exe-dir Specify the directory for the executable.
--build-generator Specify the generator to use.
--build-project Specify the name of the project to build.
--build-makeprogram Specify the make program to use.
--build-noclean Skip the make clean step.
--build-options Add extra options to the build step.
例如,請考慮以下取自 CMake 本身 CMakeLists.txt 檔案的 add_test
命令。它顯示如何使用 CTest 來編譯和執行測試。
add_test(simple ${CMAKE_CTEST_COMMAND}
--build-and-test "${CMAKE_SOURCE_DIR}/Tests/Simple"
"${CMAKE_BINARY_DIR}/Tests/Simple"
--build-generator ${CMAKE_GENERATOR}
--build-makeprogram ${CMAKE_MAKE_PROGRAM}
--build-project Simple
--test-command simple)
在本範例中,add_test
命令首先傳遞測試的名稱「simple」。在測試名稱之後,指定要執行的命令。在這種情況下,要執行的測試命令是 CTest。CTest 命令透過 CMAKE_CTEST_COMMAND
變數引用。此變數總是會由 CMake 設定為來自用於建置專案的 CMake 安裝的 CTest 命令。接下來,指定來源目錄和二進位目錄。CTest 的下一個選項是 --build-generator
和 --build-makeprogram
選項。這些是使用 CMake 變數 CMAKE_MAKE_PROGRAM
和 CMAKE_GENERATOR
指定的。CMAKE_MAKE_PROGRAM
和 CMAKE_GENERATOR
都是由 CMake 定義的。這是一個重要的步驟,因為它確保用於建置測試的產生器與用於建置專案本身的產生器相同。--build-project
選項會傳遞 Simple
,這對應於 Simple 測試中使用的 project
命令。最後一個參數是 --test-command
,它告訴 CTest 一旦成功建置後要執行的命令,並且應該是要由測試編譯的可執行檔的名稱。
處理大量測試¶
當單一專案中存在大量測試時,為每個測試提供個別的可執行檔是很麻煩的。也就是說,不應要求專案開發人員建立具有複雜參數解析的測試。這就是為什麼 CMake 提供了一個方便命令來建立測試驅動程式的原因。此命令稱為 create_test_sourcelist
。測試驅動程式是一個程式,它將許多小測試連結到單一可執行檔中。當使用大型程式庫建置靜態可執行檔以縮小所需的總大小時,此功能很有用。create_test_sourcelist
的簽章如下:
create_test_sourcelist (SourceListName
DriverName
test1 test2 test3
EXTRA_INCLUDE include.h
FUNCTION function
)
第一個參數是變數,它將包含必須編譯以產生測試可執行檔的來源檔案清單。`DriverName` 是測試驅動程式的名稱(例如,產生的可執行檔的名稱)。其餘的參數則是由測試來源檔案組成的清單。每個測試來源檔案都應該有一個與檔案名稱相同(不含副檔名)的函式(例如,`foo.cxx
` 應該有 `int foo(argc, argv);
`)。產生的可執行檔將能夠透過命令列依名稱呼叫每個測試。`EXTRA_INCLUDE
` 和 `FUNCTION
` 參數支援對測試驅動程式進行額外的自訂。請參考以下 CMakeLists 檔案片段,以了解如何使用此命令
# create the testing file and list of tests
set (TestToRun
ObjectFactory.cxx
otherArrays.cxx
otherEmptyCell.cxx
TestSmartPointer.cxx
SystemInformation.cxx
...
)
create_test_sourcelist (Tests CommonCxxTests.cxx ${TestToRun})
# add the executable
add_executable (CommonCxxTests ${Tests})
# Add all the ADD_TEST for each test
foreach (test ${TestsToRun})
get_filename_component (TName ${test} NAME_WE)
add_test (NAME ${TName} COMMAND CommonCxxTests ${TName})
endforeach ()
create_test_sourcelist
命令被調用以建立測試驅動程式。在本例中,它會建立並將 `CommonCxxTests.cxx
` 寫入專案的二元樹中,並使用其餘的參數來決定其內容。接下來,使用 add_executable
命令將該可執行檔加入建置中。然後,建立一個名為 `TestsToRun
` 的新變數,其初始值為測試驅動程式所需的來源。接著,使用 foreach
命令來迴圈處理其餘的來源。對於每個來源,會提取其不含副檔名的名稱並放入變數 `TName
` 中,然後為 `TName
` 新增一個測試。最終結果是,對於 create_test_sourcelist
中的每個來源檔案,都會使用測試名稱呼叫 add_test
命令。當更多測試被加入到 create_test_sourcelist
命令時,foreach
迴圈會自動為每個測試呼叫 add_test
。
管理測試資料¶
除了處理大量測試外,CMake 還包含一個用於管理測試資料的系統。它封裝在 ExternalData
CMake 模組中,可依需求下載大量資料、保留版本資訊,並允許分散式儲存。
ExternalData
的設計遵循使用基於雜湊的檔案識別碼和物件儲存的分散式版本控制系統,但它也利用了基於依賴項的建置系統的存在。下圖說明了該方法。原始碼樹包含輕量的「內容連結」,透過其內容的雜湊引用遠端儲存中的資料。ExternalData
模組會產生建置規則,將資料下載到本機儲存中,並透過符號連結(在 Windows 上是複製)從建置樹中引用它們。

圖 1:ExternalData 模組流程圖¶
內容連結是一個小的純文字檔案,其中包含實際資料的雜湊。它的名稱與其資料檔案相同,並帶有一個額外的擴展名來識別雜湊演算法,例如 `img.png.md5`。無論實際資料大小如何,內容連結在原始碼樹中總是佔用相同(小)的空間。CMakeLists.txt CMake 組態檔案在呼叫 ExternalData
模組 API 時,使用 `DATA{}` 語法來參考資料。例如,`DATA{img.png}` 會告知 ExternalData
模組使 `img.png` 在建置樹中可用,即使原始碼樹中僅出現 `img.png.md5` 內容連結。
ExternalData
模組實作了一個靈活的系統,以防止重複擷取和儲存內容。物件會從 ExternalData
CMake 組態中指定的(可能冗餘的)本機和遠端位置清單中檢索,作為「URL 範本」清單。遠端儲存系統的唯一要求是能夠從 URL 擷取內容,該 URL 透過指定雜湊演算法和雜湊值來定位內容。例如,本機或網路檔案系統、Apache FTP 伺服器或 Midas 伺服器都具有此功能。每個 URL 範本都具有 `%(algo)` 和 `%(hash)` 預留位置,供 ExternalData
替換為內容連結中的值。
透過設定 `ExternalData_OBJECT_STORES
` CMake 建置組態變數,持久性本機物件儲存可以快取下載的內容,以便在建置樹之間共用。這有助於消除多個建置樹的重複內容。它還解決了在回歸測試環境中一個重要的實際問題;當許多機器同時啟動夜間儀表板建置時,它們可以使用其本機物件儲存,而不是讓資料伺服器超載並造成網路流量擁塞。
檢索與基於依賴項的建置系統整合,因此只有在需要時才會擷取資源。例如,如果系統用於檢索測試資料,且 `BUILD_TESTING
` 關閉,則不會不必要地檢索資料。當原始碼樹更新且內容連結變更時,建置系統會依需求擷取新資料。
由於所有離開原始碼樹的參考都通過雜湊,因此它們不依賴任何外部狀態。遠端和本機物件儲存可以重新定位,而不會使舊版本原始碼中的內容連結失效。原始碼樹中的內容連結可以重新定位或重新命名,而無需修改物件儲存。原始碼樹中可能存在重複的內容連結,但只會下載一次。專案歷史記錄中具有相同原始碼樹檔案名稱的多個資料版本會在物件儲存中被唯一識別。
基於雜湊的系統允許使用不受信任的連線來連線遠端資源,因為下載的內容會在擷取後進行驗證。URL 範本清單的設定可透過允許多個冗餘的遠端儲存資源來提高穩健性。儲存資源也可以依需求隨著時間推移而變更。如果專案的遠端儲存隨著時間推移而移動,則始終可以透過調整為建置樹組態的 URL 範本或手動填入本機物件儲存,來建置舊版本的原始碼。
ExternalData
模組的簡單應用如下
include(ExternalData)
set(midas "http://midas.kitware.com/MyProject")
# Add standard remote object stores to user's
# configuration.
list(APPEND ExternalData_URL_TEMPLATES
"${midas}?algorithm=%(algo)&hash=%(hash)"
"ftp://myproject.org/files/%(algo)/%(hash)"
)
# Add a test referencing data.
ExternalData_Add_Test(MyProjectData
NAME SmoothingTest
COMMAND SmoothingExe DATA{Input/Image.png}
SmoothedImage.png
)
# Add a build target to populate the real data.
ExternalData_Add_Target(MyProjectData)
`ExternalData_Add_Test
` 函式是 CMake 的 add_test
命令的包裝函式。原始碼樹會搜尋包含資料 MD5 雜湊的 `Input/Image.png.md5` 內容連結。在檢查本機物件儲存後,會依序對 `ExternalData_URL_TEMPLATES
` 清單中的每個 URL 發出包含資料雜湊的請求。一旦找到,就會在建置樹中建立符號連結。`DATA{Input/Image.png}` 路徑將在測試命令列中展開為建置樹路徑。當建置 `MyProjectData` 目標時,就會檢索資料。