使用 GoogleTest 和 CTest 進行單元測試
本文是 使用 CMake 和 VSCodium 設置一個構建系統 的後續文章。
在上一篇文章中我介紹了基於 VSCodium 和 CMake 配置構建系統。本文我將介紹如何通過 GoogleTest 和 CTest 將單元測試集成到這個構建系統中。
首先克隆 這個倉庫,用 VSCodium 打開,切換到 devops_2
標籤。你可以通過點擊 main
分支符號(紅框處),然後選擇 devops_2
標籤(黃框處)來進行切換:
![VSCodium tag](/data/attachment/album/202308/02/111550ris8qikjc8jivjq8.png "VSCodium tag")
或者你可以通過命令行來切換:
$ git checkout tags/devops_2
GoogleTest
GoogleTest 是一個平台無關的開源 C++ 測試框架。單元測試是用來驗證單個邏輯單元的行為的。儘管 GoogleTest 並不是專門用於單元測試的,我將用它對 Generator
庫進行單元測試。
在 GoogleTest 中,測試用例是通過斷言宏來定義的。斷言可能產生以下結果:
- 成功: 測試通過。
- 非致命失敗: 測試失敗,但測試繼續。
- 致命失敗: 測試失敗,且測試終止。
致命斷言和非致命斷言通過不同的宏來區分:
ASSERT_*
: 致命斷言,失敗時終止。EXPECT_*
: 非致命斷言,失敗時不終止。
谷歌推薦使用 EXPECT_*
宏,因為當測試中包含多個的斷言時,它允許繼續執行。斷言有兩個參數:第一個參數是測試分組的名稱,第二個參數是測試自己的名稱。Generator
只定義了 generate(...)
函數,所以本文中所有的測試都屬於同一個測試組:GeneratorTest
。
針對 generate(...)
函數的測試可以從 GeneratorTest.cpp 中找到。
引用一致性檢查
generate(...) 函數有一個 std::stringstream 的引用作為輸入參數,並且它也將這個引用作為返回值。第一個測試就是檢查輸入的引用和返回的引用是否一致。
TEST(GeneratorTest, ReferenceCheck){
const int NumberOfElements = 10;
std::stringstream buffer;
EXPECT_EQ(
std::addressof(buffer),
std::addressof(Generator::generate(buffer, NumberOfElements))
);
}
在這個測試中我使用 std::addressof 來獲取對象的地址,並用 EXPECT_EQ
來比較輸入對象和返回對象是否是同一個。
檢查元素個數
本測試檢查作為輸入的 std::stringstream
引用中的元素個數與輸入參數中指定的個數是否相同。
TEST(GeneratorTest, NumberOfElements){
const int NumberOfElements = 50;
int nCalcNoElements = 0;
std::stringstream buffer;
Generator::generate(buffer, NumberOfElements);
std::string s_no;
while(std::getline(buffer, s_no, ' ')) {
nCalcNoElements++;
}
EXPECT_EQ(nCalcNoElements, NumberOfElements);
}
亂序重排
本測試檢查隨機化引擎是否工作正常。如果連續調用兩次 generate
函數,應該得到的是兩個不同的結果。
TEST(GeneratorTest, Shuffle){
const int NumberOfElements = 50;
std::stringstream buffer_A;
std::stringstream buffer_B;
Generator::generate(buffer_A, NumberOfElements);
Generator::generate(buffer_B, NumberOfElements);
EXPECT_NE(buffer_A.str(), buffer_B.str());
}
求和校驗
與前面的測試相比,這是一個大體量的測試。它檢查 1 到 n 的數值序列的和與亂序重排後的序列的和是否相等。 generate(...)
函數應該生成一個 1 到 n 的亂序的序列,這個序列的和應當是不變的。
TEST(GeneratorTest, CheckSum){
const int NumberOfElements = 50;
int nChecksum_in = 0;
int nChecksum_out = 0;
std::vector<int> vNumbersRef(NumberOfElements); // Input vector
std::iota(vNumbersRef.begin(), vNumbersRef.end(), 1); // Populate vector
// Calculate reference checksum
for(const int n : vNumbersRef){
nChecksum_in += n;
}
std::stringstream buffer;
Generator::generate(buffer, NumberOfElements);
std::vector<int> vNumbersGen; // Output vector
std::string s_no;
// Read the buffer back back to the output vector
while(std::getline(buffer, s_no, ' ')) {
vNumbersGen.push_back(std::stoi(s_no));
}
// Calculate output checksum
for(const int n : vNumbersGen){
nChecksum_out += n;
}
EXPECT_EQ(nChecksum_in, nChecksum_out);
}
你可以像對一般 C++ 程序一樣調試這些測試。
CTest
除了嵌入到代碼中的測試之外,CTest 提供了可執行程序的測試方式。簡而言之就是通過給可執行程序傳入特定的參數,然後用 正則表達式 對它的輸出進行匹配檢查。通過這種方式可以很容易檢查程序對於不正確的命令行參數的反應。這些測試定義在頂層的 CMakeLists.txt 文件中。下面我詳細介紹 3 個測試用例:
參數正常
如果輸入參數是一個正整數,程序應該輸出應該是一個數列:
add_test(NAME RegularUsage COMMAND Producer 10)
set_tests_properties(RegularUsage
PROPERTIES PASS_REGULAR_EXPRESSION "^[0-9 ]+"
)
沒有提供參數
如果沒有傳入參數,程序應該立即退出並提示錯誤原因:
add_test(NAME NoArg COMMAND Producer)
set_tests_properties(NoArg
PROPERTIES PASS_REGULAR_EXPRESSION "^Enter the number of elements as argument"
)
參數錯誤
當傳入的參數不是整數時,程序應該退出並報錯。比如給 Producer
傳入參數 ABC
:
add_test(NAME WrongArg COMMAND Producer ABC)
set_tests_properties(WrongArg
PROPERTIES PASS_REGULAR_EXPRESSION "^Error: Cannot parse"
)
執行測試
可以使用 ctest -R Usage -VV
命令來執行測試。這裡給 ctest
的命令行參數:
-R <測試名稱>
: 執行單個測試-VV
:列印詳細輸出
測試執行結果如下:
$ ctest -R Usage -VV
UpdatecTest Configuration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
UpdateCTestConfiguration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
Test project /home/stephan/Documents/cpp_testing sample/build
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph...
Checking test dependency graph end
在這裡我執行了名為 Usage
的測試。
它以無參數的方式調用 Producer
:
test 3
Start 3: Usage
3: Test command: /home/stephan/Documents/cpp testing sample/build/Producer
輸出不匹配 [^[0-9]+]
的正則模式,測試未通過。
3: Enter the number of elements as argument
1/1 test #3. Usage ................
Failed Required regular expression not found.
Regex=[^[0-9]+]
0.00 sec round.
0% tests passed, 1 tests failed out of 1
Total Test time (real) =
0.00 sec
The following tests FAILED:
3 - Usage (Failed)
Errors while running CTest
$
如果想要執行所有測試(包括那些用 GoogleTest 生成的),切換到 build
目錄中,然後運行 ctest
即可:
![CTest run](/data/attachment/album/202308/02/111550jadr74h0cresps6k.png "CTest run")
在 VSCodium 中可以通過點擊信息欄的黃框處來調用 CTest。如果所有測試都通過了,你會看到如下輸出:
![VSCodium](/data/attachment/album/202308/02/111550ba2sh1z4z2xp2axe.png "VSCodium")
使用 Git 鉤子進行自動化測試
目前為止,運行測試是開發者需要額外執行的步驟,那些不能通過測試的代碼仍然可能被提交和推送到代碼倉庫中。利用 Git 鉤子 可以自動執行測試,從而防止有瑕疵的代碼被提交。
切換到 .git/hooks
目錄,創建 pre-commit
文件,複製粘貼下面的代碼:
#!/usr/bin/sh
(cd build; ctest --output-on-failure -j6)
然後,給文件增加可執行許可權:
$ chmod +x pre-commit
這個腳本會在提交之前調用 CTest 進行測試。如果有測試未通過,提交過程就會被終止:
![Commit failed](/data/attachment/album/202308/02/111551v6fyahwaaf7j77fu.png "Commit failed")
只有所有測試都通過了,提交過程才會完成:
![Commit succeeded](/data/attachment/album/202308/02/111551rf63rllf3666sfm6.png "Commit succeeded")
這個機制也有一個漏洞:可以通過 git commit --no-verify
命令繞過測試。解決辦法是配置構建伺服器,這能保證只有正常工作的代碼才能被提交,但這又是另一個話題了。
總結
本文提到的技術實施簡單,並且能夠幫你快速發現代碼中的問題。做單元測試可以提高代碼質量,同時也不會打斷你的工作流。GoogleTest 框架提供了豐富的特性以應對各種測試場景,文中我所提到的只是一小部分而已。如果你想進一步了解 GoogleTest,我推薦你閱讀 GoogleTest Primer。
(題圖:MJ/f212ce43-b60b-4005-b70d-8384f2ba5860)
via: https://opensource.com/article/22/1/unit-testing-googletest-ctest
作者:Stephan Avenwedde 選題:lujun9972 譯者:toknow-gh 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive