轉載時請表明出處
作者聯系方式:liuyuxin0829@qq.com
一、前言
??在大學的時候,寫代碼隨心所欲,想到什么就寫什么,只顧實現功能,也不會去驗證代碼的可行性和穩定性,往往都會在在后續的使用過程中出現各種各樣的問題,然后再去捉蟲,這樣寫出來的代碼質量差,在后期又耗費大量的時間修復舊代碼bug。
??參加工作后,接觸到了單元測試,在第一個月的考核項目(智能家居控制面板)中,通過請教同事和參考AWTK源碼中的單元測試代碼,磕磕絆絆得寫了一些單元測試,但由于沒有設計好項目基礎框架,業務邏輯和用戶界面沒有完全分離,因此只做了文件讀寫模塊和網絡通信模塊的單元測試。剛好在本周的培訓內容中又詳細講了單元測試的FIRST原則,以下記錄了單元測試的學習感悟,并使用Google公司開源的C/C++單元測試框架——GTest進進行單元測試。
AWTK是ZLG開源的GUI框架:https://github.com/zlgopen/awtkS
二、什么是單元測試?
??軟件測試包括單元測試、集成測試、系統測試。
- 單元測試:對軟件設計的最小單位進行正確性測試,以檢驗程序單元是否滿足功能、性能、接口、設計規約等要求。
- 集成測試:將各個程序單元進行有序的、遞增的組合進行測試,以檢驗各個程序單元的配合情況。
- 系統測試:對集合了應用軟件、系統軟件、硬件的產品進行測試,以驗證產品在實際應用中的功能、性能等特性。
??根據傳統的開發模型,如瀑布模型,軟件開發過程和軟件測試活動的關系可以反映為經典的軟件測試V模型人,如下圖:
??其中單元測試中的單元指軟件中承擔單一職責的單位,通常在程序中體現為一個函數、一個文件、一個類、一個模塊等。單元測試都是以自動化方式執行,所以在大量回歸測試的場景下更能帶來高收益。并且單元測試代碼里會提供函數的使用示例,因為單元測試的具體表現形式就是對各種函數以各種不同輸入參數組合進行調用。
三、為什么需要單元測試?
??在我隨心所欲寫程序時,經常會遇到一些問題,例如:
- 編譯通過,但是要調試好久才能正常運行;
- 好不容易調試好了,但是一測試就會出一堆bug;
- 修復已有的bug,總會產生新的bug;
- bug難以重現,又無法定位;
- 等等......
??以上問題總結一下,就是“你寫的代碼并不是你想要的結果”,而單元測試則是能保證“你寫的代碼是你想要的結果”的最有效辦法。
??單元測試階段發現的bug更容易定位,并且由于單元測試自動化的特點,更加方便重現bug。在單元測試階段發現bug,立即修復,不會將各種問題留到最后的系統測試階段,讓代碼更可靠、更容易維護,減少后期測試、維護的成本。
??總的來講,單元測試能夠提升代碼質量,減少程序整體的調試時間。
四、如何做好單元測試?
??單元測試需要遵循FIRST原則:
F-FAST(快速原則):單元測試應該是可以快速運行的,在各種測試方法中,單元測試的運行速度是最快的,大型項目的單元測試通常應該在幾分鐘內運行完畢。
I-Independent(獨立原則):單元測試應該是可以獨立運行的,單元測試用例互相無強依賴,無對外部資源的強依賴。
R-Repeatabl(可重復原則):單元測試應該可以穩定重復的運行,并且每次運行的結果都是穩定可靠的。
S-Self Validating(自我驗證原則):單元測試應該是用例自動進行驗證的,不能依賴人工驗證。
T-Timely(及時原則):單元測試必須及時進行編寫,更新和維護,以保證用例可以隨著業務代碼的變化動態的保障質量。
??下面會基于GTest框架來說明這五個原則。
五、基于GTest進行單元測試
1、快速了解GTest
??GTest全稱GoogleTest,是Google公司發布的一款非常優秀的開源C/C++單元測試框架,已被應用于多個開源項目及Google內部項目中,包括ChromeWeb瀏覽器、LLVM編譯器框架等。
??下載或克隆源碼后,可以看見目錄結構如下圖,通常我們進行單元測試時需要用到目錄是include和src。配置工程需要做以下三件事:
包含目錄:[GTest目錄名]\googletest\include; [GTest目錄名]\googletest;
添加源文件:[GTest目錄名]\googletest\src\gtest-all.cc;
包含頭文件:#include<gtest/gtest.h>
??進行單元測試前我們需要了解兩個概念:測試用戶、測試用例集。
測試用例:為了驗證代碼的行為與預期是否相符而進行的一系列活動,在單元測試中,這一系列的活動依靠代碼來完成。
測試用例集:多個相似或相關的測試用例的集合,是為了方便我們對測試用例進行管理而產生的一個概念。通俗一點,測試用例集就是對測試用例進行分組。
TEST(IsLeapYearTest, leapYear) /* 用例集IsLeapYearTest,用例leapYear */
{
EXPECT_TRUE(IsLeapYear(2000)); /* 測試IsLeapYear函數,傳入參數2000 */
EXPECT_TRUE(IsLeapYear(1996));
}
??寫好測試用例后,需要運行測試用例,代碼如下:
int main(int argc, char** argv)
{
testing::FLAGS_gtest_filter = “*”; /* 選擇需要運行的用例 */
testing::InitGoogleMock(&argc, argv); /* 初始化測試框架 */
return RUN_ALL_TESTS(); /* 運行所選測試用例 */
}
??寫好測試用例后,GTest中可以用一下方式表示測試用例:
“用例集.用例”,例如: “IsLeapYearTest. leapYear ”
可以使用通配符“*”和“?”,例如:“IsLeapYearTest.*”
使用“:”連接多個匹配條件,例如:“*. leapYear : *. commonYear”
使用“-”排除用例,例如:“-IsLeapYearTest.*”
2、斷言
??斷言可以理解為判斷一個值或多個值是否滿足指定條件,例如:
說明 | 斷言的宏調用 |
---|---|
判斷一個值是否為真 | EXPECT_TRUE(val) |
判斷一個值是否與期望值相等 | EXPECT_EQ(exp, val) |
判斷兩個值的大小 | EXPECT_LE(val1, val2) |
判斷一個字符串是否與期望值相等 | EXPECT_STREQ(exp, val) |
更多GTest中斷言的宏請查閱文檔:[GTest目錄名]\googletest\docs\primer.md
??當判定通過時,無輸出,如下圖:
??當判定失敗時,GTest會輸出斷言位置和失敗原因,如下圖:
??GTest中斷言的宏可以理解為兩類:ASSERT、EXPECT。
ASSERT_*:當檢查點失敗時,退出當前函數(執行return操作)。
EXPECT_*:當檢查點失敗時,繼續往下執行。
3、使用GTest說明FIRST原則
??看完以上內容,應該對GTest有了簡單的認識,接下來就使用GTest框架來舉例說明FIRST原則。
(1)F-FAST(快速原則)
??在調試程序的過程中,需要多次運行單元測試去驗證被測試模塊是否正確,應該為了節省時間,單元測試必須可以快速執行。
TEST(Fs, basic) { /* 測試文件接口的基本功能 */
ASSERT_EQ(fs_test(os_fs()), RET_OK); /* 測試獲取文件系統對象函數os_fs() */
}
(2)I-Independent(獨立原則)
??單元測試可獨立運行,測試用例直接無依賴,對外部資源無依賴,測試順序不影響測試結果,測試過程中產生的外部資源文件需要在測試完成后銷毀。
TEST(Fs, read_part) { /* 測試文件接口的讀取功能 */
char buff[128];
uint32_t size = 0;
const char* str = "hello world";
const char* filename = "test.bin";
/* 測試前要創建被讀取的文件 */
file_write(filename, str, strlen(str));
char* ret = (char*)file_read(filename, &size);
/* 進行測試,對讀取到的結果ret進行驗證 */
ASSERT_EQ(file_read_part(filename, buff, sizeof(buff), 0), strlen(str));
ASSERT_EQ(strcmp(ret, str), 0);
ASSERT_EQ(size, strlen(str));
/* 測試完成后刪除被讀取的文件,并釋放緩沖區 */
file_remove(filename);
TKMEM_FREE(ret);
}
(3)R-Repeatabl(可重復原則)
??單元測試需要可以穩定重復的運行,每次得到的結果需要保持一致,如果連測試結果都不穩定,或者測試過程經常出現失敗的情況,那么單元測試也沒有意義了。
TEST(RandomNumber, Compared) { /* 測試隨機數比較大小 */
ASSERT_TRUE((rand() % 100) > (rand() % 100)); /* 無法保證每次結果都一致 */
}
(4)S-Self Validating(自我驗證原則)
??單元測試由用例自動進行驗證的,不依賴人工驗證,這是因為人工驗證耗費不必要的時間,而且沒有辦法保證驗證結果的準確性,通常來說單元測試的自我驗證就是由測試程序直接告訴開發者通過或不通過,不需要讓開發者通過輸出結果來判斷自己的測試用例是否通過。例如GTest的測試用例輸出,輸出OK表示通過,如下所示:
[ RUN ] Fs.basic /* RUN表示執行Fs.basic測試用例 */
[ OK ] Fs.basic (4 ms) /* OK表示Fs.basic用例測試通過 */
[ RUN ] Fs.read_part
[ OK ] Fs.read_part (0 ms)
(5)T-Timely(及時原則)
??單元測試必須及時進行編寫,更新和維護,以保證用例可以隨著業務代碼的變化動態的保障質量。單元測試通常是在寫函數實現前就需要寫好的,這樣能讓單元測試在開發者寫函數實現的過程中起到校驗的作用,避免開發者犯錯。
TEST(Module, Function) { /* 測試某模塊的功能(函數) */
ASSERT_TRUE(function()); /* 先按照期望結果測試function(),再去寫function()的實現 */
}