Java单元测试实践-01.单元测试概述与示例
Java單元測試實踐-00.目錄(9萬多字文檔+700多測試示例)
https://blog.csdn.net/a82514921/article/details/107969340
1. 前言
以下內容為本人以開發人員的視角,在平時進行單元測試過程中的總結。主要內容為通用的,不限制具體業務場景的單元測試實踐總結。由于能力有限,經驗不足,難免會有差錯存在,希望與大家一起探討。
2. 示例工程
以下所述示例工程為UnitTest,地址為( https://github.com/Adrninistrator/UnitTest) 或( https://gitee.com/adrninistrator/UnitTest ),包含測試類400多個,測試方法700多個。
示例工程UnitTest中還提供了以下單元測試可以使用的公共方法:
| TestCommonUtil.getTestNum() | 獲取指定測試類的@Test方法數量 |
| TestCommonUtil.checkObjectValue() | 檢查對象字段值是否等于預期值的簡化方法 |
| TestReplaceUtil.replaceMockMember() | 將對象中的指定類型成員變量替換為Mock對象并返回的簡化操作 |
| TestReplaceUtil.replaceSpyMember() | 將對象中的指定類型成員變量替換為Spy對象并返回的簡化操作 |
| TestCommonExecutionListener.afterTestMethod() | 當前測試類所有測試方法執行完畢時,關閉數據源 |
| TransactionalTestErrorSkipExecutionListener.afterTestMethod() | 使單元測試使用事務,未出現異常時回滾數據庫操作,出現異常時不回滾數據操作 |
使用JUnit5的示例工程為UnitTest_JUnit5,地址為( https://github.com/Adrninistrator/UnitTest_JUnit5 )或( https://gitee.com/adrninistrator/UnitTest_JUnit5 )。
根據數據庫表生成JPA Entity的Java組件增強版,地址為 https://github.com/Adrninistrator/jpa-entity-generator-enhance ,在開源項目的基礎上進行了優化。
3. 依賴環境版本
以下為使用的依賴環境的相關版本:
| 操作系統 | Windows | Windows 7 |
| Java | JDK | 1.8.0_144 |
| IDE | IntelliJ IDEA | 2019.2.4 (Community Edition) |
| IDE | Eclipse | 2019-06 (4.12.0) |
| 構建工具 | Gradle | 6.2 |
| 數據庫 | MariaDB | 10.0.36 |
| 數據庫 | H2 | 1.4.200 |
| Java組件 | JUnit | 4.13 |
| Java組件 | Spring | 4.3.26 |
| Java組件 | Mockito | 3.4.6 |
| Java組件 | Powermock | 2.0.7 |
4. 單元測試可以做什么
單元測試可以完成以下目標。
4.1. 提高代碼質量
單元測試可以對被測試的方法逐個驗證是否符合預期,從而提高整個功能模塊或系統的代碼質量。
缺陷發現得越早,修復起來越簡單。單元測試可以在開發階段盡量提前發現缺陷,降低缺陷修復成本。
在編寫單元測試用例的過程中,開發人員需要重新查看被測試的代碼,檢查代碼結構是否合理。如果單元測試用例不容易編寫,說明代碼結構可能需要調整。編寫單元測試可以促使開發人員優化代碼結構。
單元測試代碼雖然不在生產環境運行,但可以提升生產代碼的質量。
4.2. 提升開發效率
單元測試使得開發人員可以方便地在開發環境執行編寫的代碼,降低執行被測試代碼的操作復雜度與時間消耗,提高自測時效性,對于明顯的異常和錯誤能夠快速地發現并及時修復。
相比其他類型的測試,單元測試有以下優勢:
- 可以免去每次進行測試前提交代碼的步驟,在本地進行編譯、打包、發布等操作更便捷;
- 執行測試操作及測試結果檢查均通過程序執行,不需要人工操作;
- 單元測試用例中保存了測試數據及預期結果,便于獲取測試數據,及理解被測試代碼的功能。
在單元測試過程中,通過Mock等方式,可以盡量降低對外部系統的依賴,只需要專注于自身系統的功能。
單元測試可以通過程序檢查測試結果是否與預期一致,減少了對于人工檢查的依賴,提高了執行結果驗證的效率與準確率。
有的代碼執行對時間存在依賴,如定時任務、批處理等,通過單元測試可以隨時對此類代碼進行測試,不受時間的限制。
編寫單元測試用例也需要時間,從整體階段來看,單元測試還是能夠提升效率的。
4.3. 降低異常情況的測試復雜度
單元測試可以對被測試代碼進行Mock,通過相對簡單的操作就可以模擬各種異常情況,不需要依賴其他關聯系統進行調整。相比其他類型的測試,通過單元測試對異常情況的測試更便捷,可以覆蓋更多代碼分支。
4.4. 起到部分回歸測試的作用
對現有代碼的變更是不可避免的,若相關功能的代碼擁有對應的單元測試用例,可以通過單元測試快速驗證代碼變更是否對現有功能產生了影響,對存量功能修改的影響更可控,質量更有保障,起到部分回歸測試的作用。只需要編寫一次單元測試代碼,就可以重復執行,對代碼進行驗證,獲取單元測試來帶的便利( 代碼有變動時,對應的單元測試代碼也要修改 )。
單元測試也可以與CI/CD相關的工具結合,自動化運行,減少人工干預。
4.5. 以直觀的方式展示重要功能
單元測試代碼中,能夠以直觀的方式(對方法的Mock及對執行結果的檢查等),展示被測試代碼的重要功能,例如遠程調用、數據庫操作,及執行結果、執行完畢后的數據庫數據等。
4.6. 處理開發相關的安全問題
應用程序可能會由于開發不當產生一些安全問題,如跨站、SQL注入、任意文件下載、危險文件上傳、越權、其他的邏輯漏洞等。
通常需要安全人員通過代碼掃描或滲透測試等方式發現以上問題,向開發人員反饋問題及解決方法,開發人員再根據安全人員的建議對問題進行修復。
開發人員通常并不了解安全人員使用的滲透測試工具與方法,如果需要直接使用安全人員的滲透測試工具與方法,學習成本會比較高,因此導致開發人員很難參與此類安全問題的檢測。由于開發人員缺乏參與安全問題檢測的有效實踐方法,使得開發人員難以對以上安全問題得到進一步的了解。因此形成了一個死結,因為難以實踐,所以缺乏了解;因為缺乏了解,因此容易出現安全問題。
單元測試可以作為解決以上問題的一個突破口。當開發人員了解了安全問題的原理后,可以通過單元測試驗證上述問題,在經過實際的安全問題驗證后,對于安全問題的原因、檢測及修復方法,都會有更深刻的理解,安全問題的出現機率應會逐漸下降,形成良性的循環。
對于開發人員,掌握單元測試比掌握安全人員使用的滲透測試工具和方法更容易,通過單元測試驗證開發相關的安全問題,是開發人員更熟悉,更容易接受的方法,實現難度與學習成本更低。
開發人員通過單元測試驗證安全問題,是通過白盒的方式進行測試,開發人員對于程序功能、代碼邏輯和可能出現的問題更清楚,存在一定的優勢。
5. 哪些代碼需要進行單元測試
最理想的情況下,對所有的代碼都進行單元測試,代碼質量是比較有保證的。但在實際的開發過程中,時間可能會比較緊張,對于工具類或公共代碼以及重要的業務代碼,最好能進行單元測試,保證重要的方法是經過測試符合預期的。
在最初的階段,即使在單元測試用例中只是簡單地調用了被測試的方法,還不滿足AIR原則( 如沒有通過Mock解決環境依賴問題,需要手工解決;沒有通過程序檢查執行結果,還需要人工檢查等 ),相比沒有單元測試的代碼也是提高。先解決有無問題,再進行優化。在使用單元測試的過程中,會逐漸熟悉所使用的單元測試框架,編寫單元測試代碼會逐步變得更加高效,并得到單元測試帶來的便利,從而獲得正反饋。
6. 單元測試需要關注的場景
6.1. 方法入口參數檢查
對于方法入口的請求參數,需要進行單元測試,測試正常及異常情況下的處理是否符合預期。如果對于請求參數通過注解或配置的方式進行檢查,則可以轉變為對應的公共檢測方法進行單元測試。
6.2. 業務功能的主要流程
對于程序提供的業務功能的主要流程,需要進行單元測試。根據程序設計的結果分析并選擇需要測試的場景,是一種比較有效的方式,所選擇的場景是有依據的,不是主觀臆斷得來的。
可以通過思維導圖、表格等形式記錄需要進行單元測試的各個場景,方便檢查是否有出現重復或遺漏,至少保證能夠覆蓋大部分重要的場景。
需要進行單元測試的場景包含包括正常場景與異常場景,最好能夠覆蓋全部的場景。需要檢查被測試代碼在不同場景下的執行結果是否符合預期,主要包括如下內容:
- 方法返回值或對于入參的修改是否符合預期;
- 數據庫記錄中的重要屬性值是否符合預期;
- 生成文件的情況,及文件內容是否符合預期;
- 發起的遠程調用,及請求數據內容是否符合預期;
- 作為服務提供方時,向上游系統返回數據的重要屬性是否符合預期。
7. 單元測試代碼編寫建議
7.1. AIR原則
參考(阿里巴巴)Java開發手冊,“【強制】 好的單元測試必須遵守 AIR 原則”。其中AIR分別代表A:Automatic,自動化;I:Independent,獨立性;R:Repeatable,可重復。
7.1.1. A-自動化
自動化是指單元測試是自動執行,非交互式的,執行過程中不需要人工介入。
自動化可以大致分為執行過程自動化、執行結果檢查自動化與單元測試執行動作觸發自動化,其中執行過程自動化與執行結果檢查自動化可以通過單元測試用例實現,單元測試執行動作觸發自動化依賴其他條件,在后續內容中討論。
7.1.1.1. 執行過程自動化
使單元測試執行過程自動化,不需要進行交互,不依賴人工處理,擺脫對執行環境的依賴,可能需要使用Mock等方式。
7.1.1.2. 執行結果檢查自動化
在單元測試中可以使用斷言等方式,自動對執行結果進行檢查,不需要人工干預,結果更準確,效率更高。
執行結果檢查包括但不限于檢查方法返回值、檢查方法執行完畢后數據庫記錄、檢查遠程調用方法是否執行及請求數據是否符合預期。
7.1.2. I-獨立性
單元測試用例需要滿足獨立性,每個用例都需要是簡單、明確的。一個單元測試用例,通常是針對一個方法,或者一個功能的一組方法進行測試。
各單元測試用例之間不能相互調用,在執行順序上也不能有依賴。若單元測試執行時存在依賴數據,需要在各單元測試用例中自己解決,如設置Mock,或通過代碼插入所需數據等。
7.1.3. R-可重復
單元測試用例需要是可重復的,執行結果應與執行次數無關。各個測試用例之間需要是相互獨立的,測試用例也需要獨立于運行環境,可能需要使用Mock。
7.2. BCDE原則
參考(阿里巴巴)Java開發手冊,“【推薦】編寫單元測試代碼遵守BCDE原則,以保證被測試模塊的交付質量。”。其中BCDE分別代表B:Border,邊界值測試;C:Correct,正確的輸入;D:Design,與設計文檔相結合;E:Error,強制錯誤信息輸入。
7.3. 其他建議
當發現代碼缺陷時,需要檢查是否有對應單元測試用例,若無則需要補充測試用例;若有則需要檢查用例,是否覆蓋不全,并進行優化。
通過對某個方法或功能的正常、異常情況的各種場景編寫單元測試用例,判斷實際執行結果與單元測試中預期的結果是否一致,可以驗證單元測試代碼本身是否正確。
8. 單元測試與Mock
以下不明確區分Mock與Stub等概念,以下所述的Mock均是指在不修改代碼的情況下,對被測試代碼的功能進行動態的修改。不同的開發語言通常都存在對應的Mock框架。
- Mock代碼對正常功能的影響
Java的Mock代碼通常在test模塊中編寫,在發布時不會影響main模塊,避免出現Mock代碼影響正式環境的問題。
- Mock范圍選擇
在進行Mock時,需要克制,只Mock必須的最小范圍,避免范圍過大時導致部分內容未被測試。
- 了解Mock框架的目的
需要對使用的Mock框架加深了解,避免使用時出現錯誤,便于選擇最合適的使用方式,提高單元測試開發效率,便于后續維護。
8.1. 使用Mock的目的
使用Mock,是為了能夠低成本任意改變被測試代碼行為,構造所需的測試場景,且能夠對方法調用的請求參數等進行檢查。
以上的低成本,是指能夠高效快速地編寫所需的Mock代碼,且便于后續維護。
為了達到以上目的,需要盡量了解mock框架,在需要Mock時選擇合適的實現方式。
8.2. 需要Mock的代碼
從場景看,主要包括以下幾類代碼需要進行Mock:
-
被測試代碼所依賴的環境配置相關代碼,用于屏蔽環境依賴,如從本地文件或數據庫等獲取配置參數;
-
影響被測試執行分支相關代碼,主要用于測試不同場景下代碼執行結果是否符合預期;
-
通過遠程服務調用訪問其他系統的相關代碼,用于測試依賴的其他系統返回不同數據時執行是否正確。
從操作類型看,操作以下類型的代碼均可能需要進行Mock:
進程內方法調用、操作系統配置、文件、網絡訪問、數據庫相關操作等。
8.3. 不需要Mock的代碼
Mock應最小化,被測試代碼的重要邏輯處理相關代碼不需要Mock,若Mock后會導致相關功能無法被驗證。例如對重要交易數據執行的數據庫插入、修改操作,不需要Mock,可在操作執行完畢后從數據庫查詢對應數據,判斷是否符合預期。
8.4. 數據依賴與Mock的使用
在進行單元測試時,存在依賴數據庫記錄的情況,比如需要從數據庫查詢用戶的開戶信息,交易流水信息等。
當只需要從數據庫查詢,不需要執行修改或刪除操作時,可以將對應的讀取操作進行Mock。相比插入數據的方式,不需要進行數據清理;
當需要從數據庫查詢,并進行修改或刪除操作時,可以提前通過代碼插入對應的數據,在測試執行完畢后查詢最新的數據,檢查程序執行是否符合預期。
9. 單元測試執行步驟
單元測試執行步驟(Java)如下圖所示:
9.1. 執行測試代碼
執行測試代碼時,可以使用JUnit、spring-test、Gradle,以及IDE工具。
9.2. 數據初始化
數據初始化包括但不限于以下內容:
- 準備執行被測試方法的請求數據;
- 準備被測試代碼需要使用(更新、刪除等,讀取可以使用Mock)的數據庫記錄;
- 準備Mock指定的返回數據。
對于數據初始化,個人傾向使用Java代碼實現,與使用配置文件、數據庫等方式相比,使用Java代碼實現并不復雜,配合DTO的setter方法生成插件使用,代碼編寫效率并不低,且使用Java代碼實現數據初始化還有以下優勢:
- 使用Java代碼更加通用,不依賴特定組件或處理邏輯,不限制數據格式;
- 當DTO中的變量發生變化時,Java代碼編譯時會報錯,能夠明確影響范圍,避免出現測試代碼與原始代碼使用變量不一致的問題;
- 使用Java代碼,可以使用常量定義,以及代碼復用,減少重復代碼;
- 使用Java代碼時,能夠方便地按照規則生成所需變量值,如流水號、隨機數、當前時間等。
9.3. 代碼Mock
需要進行代碼Mock的范圍包括但不限于以下內容:
- 進程內方法調用
- 系統變量讀取
- 文件讀寫
- 遠程服務調用
- 數據庫操作
代碼Mock可以使用Mockito、PowerMock,能夠滿足單元測試對代碼進行Mock的要求。
對代碼Mock的請求參數及返回數據管理,也傾向使用Java代碼實現,原因同上。
9.4. 擺脫數據庫環境依賴
在某些執行單元測試的環境(例如CI/CD服務器),可能無法訪問數據庫服務器(MySQL等),可以使用本地的文件或內存形式數據庫替代數據庫服務器,例如H2數據庫。
9.5. 檢查執行結果
檢查執行結果是否符合預期,包括但不限于以下內容:
- 檢查方法調用返回值
- 檢查方法調用參數
- 檢查方法調用次數
- 檢查數據庫記錄
對預期的執行結果管理,也傾向使用Java代碼實現,原因同上。
9.6. 生成測試結果報告
使用Gradle執行單元測試完畢后,會生成測試結果報告,可以查看單元測試執行的各個類的執行結果,執行耗時,報錯信息等。
9.7. 生成代碼覆蓋率報告
使用Gradle執行單元測試,可以使用JaCoCo生成代碼覆蓋率報告。
9.8. 清理測試數據
在進行單元測試時,可以使用自動回滾的方式使單元測試對數據庫的修改回滾,對于執行失敗的測試方法,建議不回滾,便于通過數據庫中保留的記錄分析出現問題的原因。在后續內容“數據庫操作自動回滾處理”中有詳細說明。
若不使用數據庫操作自動回滾的方式,可參考以下處理。
單元測試生成的數據,需要有明顯的標志與非單元測試生成的數據進行區分,便于清理。
通常情況下,單元測試在數據庫中生成的記錄內容及數量,不會對其他測試造成影響,可以不定期進行手工或自動化清理。
當使用本地的文件或內存形式的數據庫進行單元測試時,不需要考慮清理測試數據的問題,可以直接刪除數據庫文件。
10. 其他內容
10.1. 單元測試的階段
根據經驗,單元測試可以大致分為以下幾個階段:
10.2. 單元測試能否替代其他類型的測試
單元測試只是對于其他類型的測試的補充,并不能替代。
10.3. 單元測試維護
當功能代碼發生變化時,可能需要修改對應的單元測試代碼。
為了降低單元測試代碼維護成本,以及提高單元測試代碼編寫效率,可以像編寫功能代碼一樣編寫單元測試代碼,合理設計單元測試代碼的結構,合理使用封裝、繼承、多態等面向對象編程特性,使用公共方法,避免重復代碼。將Mock條件設置收斂到公共方法或測試類的抽象基類中,便于后期持續維護。熟練掌握Mock框架,使用最便捷有效的方式設置Mock條件。
因功能代碼發生變化而修改對應的單元測試代碼是常見的,與功能代碼本身的調整類似。
10.4. 檢查單元測試效果
假如在實施了單元測試后發現對于提升開發質量或效率沒有幫助,只是增加了開發時間,需要檢查出現了什么問題。
10.5. 單元測試時間占比
參考《Test Early and Often》( https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/ee330950(v=vs.110) ),“Writing unit tests should take about 40% of team members’ time.”,編寫單元測試可能占團隊成員大約40%的時間。
10.6. 單元測試與集成測試
以下涉及的部分測試方法或內容屬于集成測試的范圍,未進行嚴格區分。
11. 參考資料
| 阿里巴巴Java開發規范 | https://102.alibaba.com/downloadFile.do?file=1561031481870/Java-huashanxinban.pdf |
| 測試金字塔實戰 | https://insights.thoughtworks.cn/practical-test-pyramid/ |
| 單元測試之道Java版——使用JUnit | |
| Unit Testing Guidelines | https://petroware.no/unittesting.html |
| Test Early and Often | https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/ee330950(v=vs.110) |
總結
以上是生活随笔為你收集整理的Java单元测试实践-01.单元测试概述与示例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 今日头条面试题(二)
- 下一篇: 计算机网络学习1:因特网概述