來源:公衆號【很酷的程序員/RealCoolEngineer】
編寫代碼有bug是很正常的,通過編寫完備的單元測試,可以及時發現問題,並且在後續的代碼改進中持續觀測是否引入了新的bug。對於追求質量的程序員,爲自己的代碼編寫全面的單元測試是必備的基礎技能,在編寫單元測試的時候也能覆盤自己的代碼設計,是提高代碼質量極爲有效的手段。
在本系列前序的文章中已經介紹了CMake很多內容,本文是針對單元測試的外延。本系列更多精彩文章敬請關注公衆號【很酷的程序員】的話題:CMake。
本文主要介紹以下幾個方面的內容:
- 何爲單元測試
- 何爲gtest
- 怎麼使用gtest
- 怎麼運行測試
本文仍以開源項目:https://gitee.com/RealCoolEngineer/cmake-template爲例,後續示例代碼基於上一篇文章的狀態進行修改,本文對應的commit id爲:c9f1c21
。
一 單元測試是什麼?
單元測試(Unit Testing),一般指對軟件中的最小可測試單元進行檢查和驗證。最小可測試單元可以是指一個函數、一次調用過程、一個類等,不同的語言可能有不同的測試方法,暫時不必深究。
對於C/C++語言,單元測試一般是針對一個函數而言,單元測試的目的就是檢測目標函數在所有可能的輸入下,函數的執行過程和輸出是否符合預期。可以說,單元測試是顆粒度最小的測試,對於軟件開發而言,保證每個小的函數執行正確,才能保證利用這些小模塊組合起來的系統能夠正常工作。
和測試相關的另外一個重要概念是測試用例(Test Case)。百度百科給的定義是,測試用例是對一項特定的軟件產品進行測試任務的描述,體現測試方案、方法、技術和策略,包括測試目標、測試環境、輸入數據、測試步驟、預期結果、測試腳本等。
這個定義是比較廣泛的,對於單元測試來說,就是測試在不同輸入下,目標函數(模塊)的預期執行過程和輸出(返回值),每個不同的情形可以有一個或多個測試用例。編寫測試用例需要儘量覆蓋所有輸入情況(尤其是邊界值、特殊值、異常值)。比如下列函數:
int fibo(int i) {
if (i == 1 || i == 2) {
return 1;
}
return fibo(i - 1) + fibo(i - 2);
}
這個函數是爲了實現斐波那契數列,所以輸入可以分爲幾類,就可以覆蓋所有情況:
- 小於等於0的整數
- 1和2
- 大於2的整數
對應地,可以設置以下測試用例:
- 輸入0,期望值是0
- 輸入1,期望值是1
- 輸入2,期望值是1
- 輸入3,期望值是2
- 輸入4,期望值是3
可以比較明顯地發現,如果輸入是小於等於0的整數,這個函數就一直遞歸下去了。這也是開發過程中需要注意的,代碼(功能)的使用者並不一定會遵循常規的思維(斐波那契數列不可能輸入負數),開發者只能相信自己的代碼,不要對輸入有任何假設。
上述test case在cmake-template項目的
test/c/test_gtest_demo.cc
中有示例
二 gtest簡介
Google Test
是Google開源的一個跨平臺的C++單元測試框架,簡稱gtest
,它提供了非常豐富的測試斷言、判斷宏,極大方便開發者編寫測試用例的流程,也是很多開源項目使用的測試框架。
在前面介紹CMake的測試功能時,每個單元測試都是一個可執行文件,實現了main
函數,在CMakeLists.txt
中使用add_test
命令來添加測試用例:
enable_testing()
add_executable(test_add test/c/test_add.c)
add_executable(test_minus test/c/test_minus.c)
target_link_libraries(test_add math)
target_link_libraries(test_minus math)
add_test(NAME test_add COMMAND test_add 10 24 34)
add_test(NAME test_minus COMMAND test_minus 40 96 -56)
通過使用gtest
可以簡化這個流程,讓開發者可以專注在測試用例的書寫上,而不用手動編寫大量的main
函數,以及一些判斷輸出是否符合預期的附加代碼。
三 集成gtest
1 將gtest源碼加入項目
gtest
是一個開源的框架,代碼位於github倉庫:google/googletest,本文介紹直接將gtest
加入到項目中,通過CMake
編譯使用。
首先在項目根目錄新建一個third_party
目錄,下載源碼的最新release版本,並解壓:
➜ # mkdir third_party
➜ # cd third_party
➜ # wget https://codeload.github.com/google/googletest/zip/refs/tags/release-1.10.0
➜ # unzip googletest-release-1.10.0.zip
2 將gtest添加爲子模塊
修改項目根目錄的CMakeLists.txt
文件,使用上一篇文章介紹的命令add_subdirectory
,在開啓單元測試時,添加gtest
爲子模塊,並將對應頭文件路徑添加進來:
enable_testing()
add_subdirectory(third_party/googletest-release-1.10.0)
include_directories(third_party/googletest-release-1.10.0/googletest/include)
此時執行命令:
➜ # cmake -B cmake-build
➜ # cmake --build cmake-build
可以看到構建目錄下多了一個目錄cmake-build/third_party/googletest-release-1.10.0
,並且gtest
編譯生成了4個新的庫文件(gtest
子模塊的編譯目標,位於目錄cmake-build/lib
下):
- libgtest.a
- libgtest_main.a
- libgmock.a
- libgmock_main.a
其中libgtest.a
提供單元測試相關的功能,libgtest_main.a
提供單元測試的主入口,只有鏈接該庫,測試用例就會編譯成可執行文件;兩個mock
庫也是類似的,主要提供數據庫交互,網絡連接等方面的模擬測試,這不是本文的重點。
此時就可以在鏈接其他目標時直接使用gtest
的這4個編譯目標(target)。
3 編寫測試用例
接下來直接修改先前的兩個測試用例源文件,實現相同的測試功能:
- test/c/test_add.c
- test/c/test_minus.c
因爲使用的是C++測試框架,所以上述兩個源文件修改爲.cc
後綴。
在源文件中include頭文件gtest/gtest.h
,使用gtest
測試用例定義宏來定義測試用例:
TEST(test_case_name, test_name) {}
一個test_case_name
下面可以包含多個不同(test_name
)的測試。
test/c/test_add.cc
內容爲:
#include "gtest/gtest.h"
#include "math/add.h"
TEST(TestAddInt, test_add_int_1) {
int res = add_int(10, 24);
EXPECT_EQ(res, 34);
}
test/c/test_minus.cc
內容爲:
#include "gtest/gtest.h"
#include "math/minus.h"
TEST(TestMinusInt, test_minus_int_1) {
int res = minus_int(40, 96);
EXPECT_EQ(res, -56);
}
顯而易見,測試用例的代碼量比之前少了很多,而且更加可讀,更加專業。
這裏使用了一個判斷值相等的斷言EXPECT_EQ
,gtest
中的斷言分成兩大類:
-
ASSERT_*
系列:如果檢測失敗就直接退出當前函數 -
EXPECT_*
系列:如果檢測失敗發出提示,並繼續往下執行
gtest
有很多類似的宏用來判斷數值的關係、判斷條件的真假、判斷字符串的關係。
對於條件判斷可以使用:
ASSERT_TRUE(condition); // 判斷條件是否爲真
ASSERT_FALSE(condition); // 判斷條件是否爲假
對於數值比較可以使用:
ASSERT_EQ(val1, val2); // 判斷是否相等
ASSERT_NE(val1, val2); // 判斷是否不相等
ASSERT_LT(val1, val2); // 判斷是否小於
ASSERT_LE(val1, val2); // 判斷是否小於等於
ASSERT_GT(val1, val2); // 判斷是否大於
ASSERT_GE(val1, val2); // 判斷是否大於等於
對於字符串比較可以使用:
ASSERT_STREQ(str1,str2); // 判斷字符串是否相等
ASSERT_STRNE(str1,str2); // 判斷字符串是否不相等
ASSERT_STRCASEEQ(str1,str2); // 判斷字符串是否相等,忽視大小寫
ASSERT_STRCASENE(str1,str2); // 判斷字符串是否不相等,忽視大小寫
4 添加測試用例
書寫好測試用例源文件後,需要修改項目根目錄的CMakeLists.txt
:
enable_testing()
add_subdirectory(third_party/googletest-release-1.10.0)
include_directories(third_party/googletest-release-1.10.0/googletest/include)
set(GTEST_LIB gtest gtest_main)
add_executable(test_add test/c/test_add.cc)
add_executable(test_minus test/c/test_minus.cc)
target_link_libraries(test_add math gtest gtest_main)
target_link_libraries(test_minus math gtest gtest_main)
add_test(NAME test_add COMMAND test_add)
add_test(NAME test_minus COMMAND test_minus)
對於一個單元測試來說,添加的步驟爲:
- 使用
add_executable
添加測試目標 - 使用
target_link_libraries
爲測試目標添加依賴gtest
和gtest_main
- 使用
add_test
添加到項目,以便可以使用ctest
命令執行測試
需要注意的不同就是,依舊將單元測試的源文件編譯爲可執行文件,並且鏈接的時候鏈接了gtest
和gtest_main
。必須要鏈接gtest_main
庫,才能給單元測試添加main函數主入口,否則在鏈接的時候將會報錯。
6 運行測試
在前面的文章中已經介紹過了,在構建編譯完成後,進入構建目錄,使用ctest
命令執行測試即可。
筆者常用的命令爲:
make test CTEST_OUTPUT_ON_FAILURE=TRUE GTEST_COLOR=TRUE
# 或者
GTEST_COLOR=TRUE ctest --output-on-failure
指定--output-on-failure
或者設置CTEST_OUTPUT_ON_FAILURE
變量爲TRUE
,讓單元測試失敗時輸出具體信息,而GTEST_COLOR
設置爲TRUE
可以讓輸出帶有顏色,可以在詳細輸出模式下(-VV)更快找到錯誤的輸出(如果有失敗的測試)。
上面即爲在CMake
項目中引入gtest
框架的示例,關於gtest
更多的信息可以閱讀gtest
的官方文檔:
這裏的單元測試也只是作爲示例,在真實的項目中,單元測試的編寫往往更加複雜,而且這也還只是提高的軟件魯棒性中的一環,追求極致還需要更多努力。