cmake應用:集成gtest進行單元測試

來源:公衆號【很酷的程序員/RealCoolEngineer】

編寫代碼有bug是很正常的,通過編寫完備的單元測試,可以及時發現問題,並且在後續的代碼改進中持續觀測是否引入了新的bug。對於追求質量的程序員,爲自己的代碼編寫全面的單元測試是必備的基礎技能,在編寫單元測試的時候也能覆盤自己的代碼設計,是提高代碼質量極爲有效的手段。

在本系列前序的文章中已經介紹了CMake很多內容,本文是針對單元測試的外延。本系列更多精彩文章敬請關注公衆號【很酷的程序員】的話題:CMake

本文主要介紹以下幾個方面的內容:

  1. 何爲單元測試
  2. 何爲gtest
  3. 怎麼使用gtest
  4. 怎麼運行測試

本文仍以開源項目: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);
}

這個函數是爲了實現斐波那契數列,所以輸入可以分爲幾類,就可以覆蓋所有情況:

  1. 小於等於0的整數
  2. 1和2
  3. 大於2的整數

對應地,可以設置以下測試用例:

  1. 輸入0,期望值是0
  2. 輸入1,期望值是1
  3. 輸入2,期望值是1
  4. 輸入3,期望值是2
  5. 輸入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下):

  1. libgtest.a
  2. libgtest_main.a
  3. libgmock.a
  4. libgmock_main.a

其中libgtest.a提供單元測試相關的功能,libgtest_main.a提供單元測試的主入口,只有鏈接該庫,測試用例就會編譯成可執行文件;兩個mock庫也是類似的,主要提供數據庫交互,網絡連接等方面的模擬測試,這不是本文的重點。

此時就可以在鏈接其他目標時直接使用gtest的這4個編譯目標(target)。

3 編寫測試用例

接下來直接修改先前的兩個測試用例源文件,實現相同的測試功能:

  1. test/c/test_add.c
  2. 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_EQgtest中的斷言分成兩大類:

  1. ASSERT_*系列:如果檢測失敗就直接退出當前函數
  2. 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)

對於一個單元測試來說,添加的步驟爲:

  1. 使用add_executable添加測試目標
  2. 使用target_link_libraries爲測試目標添加依賴gtestgtest_main
  3. 使用add_test添加到項目,以便可以使用ctest命令執行測試

需要注意的不同就是,依舊將單元測試的源文件編譯爲可執行文件,並且鏈接的時候鏈接了gtestgtest_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的官方文檔:

  1. GoogleTest Primer
  2. GoogleTest User's Guide

這裏的單元測試也只是作爲示例,在真實的項目中,單元測試的編寫往往更加複雜,而且這也還只是提高的軟件魯棒性中的一環,追求極致還需要更多努力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章