本人以前在Java項(xiàng)目開(kāi)發(fā)中有一大痛點(diǎn)就是寫(xiě)單元測(cè)試,因?yàn)椴渴鹕暇€時(shí),在 CI/CD 流水線中在對(duì)代碼行覆蓋率有強(qiáng)卡點(diǎn),代碼行覆蓋率必須達(dá)到90%才能繼續(xù)推進(jìn)部署?;叵胍幌乱郧芭懦鈱?xiě)單元測(cè)試的主要原因有如下幾點(diǎn):
1、心理上排斥寫(xiě)單元測(cè)試,覺(jué)得很繁瑣,為了代碼行覆蓋率去寫(xiě)測(cè)試
2、寫(xiě)單元測(cè)試沒(méi)有比較好的實(shí)踐經(jīng)驗(yàn),遇到不好覆蓋的代碼,不知道如何處理,也不知道如何重構(gòu)代碼
如何克服這兩個(gè)問(wèn)題?第一個(gè)是心理問(wèn)題,需要真正認(rèn)識(shí)到寫(xiě)單元測(cè)試的好處,這樣才能夠接受寫(xiě)單元測(cè)試是開(kāi)發(fā)過(guò)程中的必要步驟。第二個(gè)是方法問(wèn)題,需要學(xué)習(xí)寫(xiě)單元測(cè)試的方法和最佳實(shí)踐,這樣在寫(xiě)單元測(cè)試時(shí)才能知道如何下手。
本文主要從理論和實(shí)踐兩個(gè)方面介紹一下如何在 Java 項(xiàng)目中寫(xiě)單元測(cè)試,在理論篇中重點(diǎn)說(shuō)明寫(xiě)單元測(cè)試的好處與規(guī)范,在實(shí)踐篇中介紹一下寫(xiě)單元測(cè)試的一些實(shí)踐方法,重點(diǎn)介紹在單元測(cè)試和集成測(cè)試中如何使用 Mock 對(duì)象。
通過(guò)本文的學(xué)習(xí),你將了解到寫(xiě)單元測(cè)試的方法論和最佳實(shí)踐,也許能克服前面提到的兩個(gè)問(wèn)題,也就不會(huì)排斥寫(xiě)單元測(cè)試了,并且能夠?qū)懗龈咝?、更可靠的單元測(cè)試和集成測(cè)試,進(jìn)而提高整體的軟件質(zhì)量。
理論篇
單元測(cè)試和集成測(cè)試
在Java項(xiàng)目中,單元測(cè)試和集成測(cè)試是軟件測(cè)試方法中的兩個(gè)重要組成部分,它們?cè)跍y(cè)試的范圍、目的和實(shí)現(xiàn)方法上有所區(qū)別:
單元測(cè)試 (Unit Testing)
- 范圍:?jiǎn)卧獪y(cè)試通常關(guān)注在最小的代碼單元級(jí)別上的測(cè)試,通常是類和方法。目的是驗(yàn)證單個(gè)組件的功能是否按預(yù)期工作。
- 隔離性:在單元測(cè)試中,被測(cè)試的代碼通常與其依賴項(xiàng)隔離,依賴項(xiàng)可能被模擬(Mocking)或樁(Stubbing)以確保測(cè)試的獨(dú)立性。
- 快速執(zhí)行:?jiǎn)卧獪y(cè)試應(yīng)該快速執(zhí)行,這使它們適合頻繁運(yùn)行,例如在持續(xù)集成環(huán)境中。
- 工具:JUnit、TestNG、Mockito、EasyMock和PowerMock是常用的單元測(cè)試工具。
- 代碼覆蓋率:?jiǎn)卧獪y(cè)試通常用來(lái)提高代碼覆蓋率,確保每一行代碼都被測(cè)試到。
- 示例:測(cè)試一個(gè)方法是否返回正確的計(jì)算結(jié)果,或者模擬一個(gè)數(shù)據(jù)庫(kù)接口來(lái)測(cè)試數(shù)據(jù)訪問(wèn)邏輯。
集成測(cè)試 (Integration Testing)
- 范圍:集成測(cè)試關(guān)注多個(gè)組件或系統(tǒng)的協(xié)同工作,它驗(yàn)證了組件間的接口和相互作用是否正確。
- 系統(tǒng)性:集成測(cè)試通常在更接近生產(chǎn)環(huán)境的設(shè)置中執(zhí)行,可能包括數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)服務(wù)和其他應(yīng)用程序接口。
- 執(zhí)行速度:集成測(cè)試通常比單元測(cè)試慢,因?yàn)樗鼈兩婕暗礁嗟南到y(tǒng)組件和配置。
- 工具:Spring Test、TestContainers、JUnit、RestAssured和Selenium可以用于集成測(cè)試。
- 測(cè)試真實(shí)性:集成測(cè)試更貼近用戶的實(shí)際使用場(chǎng)景,可以捕捉到單元測(cè)試可能忽略的問(wèn)題。
- 示例:測(cè)試Web服務(wù)的REST API與數(shù)據(jù)庫(kù)的交互,或者測(cè)試不同模塊/服務(wù)之間的數(shù)據(jù)交換。
總的來(lái)說(shuō),單元測(cè)試和集成測(cè)試在測(cè)試策略中扮演著互補(bǔ)的角色。單元測(cè)試通過(guò)快速、頻繁地驗(yàn)證小塊功能來(lái)保證代碼質(zhì)量,而集成測(cè)試通過(guò)在更復(fù)雜的環(huán)境中驗(yàn)證組件協(xié)同工作的情形來(lái)確保整個(gè)系統(tǒng)的穩(wěn)定性和可靠性。在現(xiàn)代軟件開(kāi)發(fā)實(shí)踐中,單元測(cè)試和集成測(cè)試常常被結(jié)合起來(lái)使用,以實(shí)現(xiàn)更全面的測(cè)試覆蓋。
編寫(xiě)測(cè)試的好處
單元測(cè)試和集成測(cè)試是軟件開(kāi)發(fā)過(guò)程中保證整個(gè)系統(tǒng)穩(wěn)定性和可靠性的關(guān)鍵實(shí)踐,它們帶來(lái)了許多好處:
單元測(cè)試的好處
- 提高代碼質(zhì)量:?jiǎn)卧獪y(cè)試有助于發(fā)現(xiàn)代碼中的錯(cuò)誤并使其更加健壯,這樣可以降低生產(chǎn)環(huán)境中出現(xiàn)問(wèn)題的風(fēng)險(xiǎn)。
- 促進(jìn)設(shè)計(jì):編寫(xiě)可測(cè)試的代碼通常需要良好的設(shè)計(jì)。單元測(cè)試鼓勵(lì)開(kāi)發(fā)者遵循SOLID原則,比如單一責(zé)任原則和依賴倒置原則。
- 文檔作用:?jiǎn)卧獪y(cè)試可以作為代碼的活文檔,說(shuō)明代碼應(yīng)如何被使用以及預(yù)期的行為。
- 簡(jiǎn)化重構(gòu):具有良好單元測(cè)試覆蓋的代碼庫(kù)可以更加自信地重構(gòu),因?yàn)闇y(cè)試可以快速發(fā)現(xiàn)由于改動(dòng)引入的任何問(wèn)題。
- 提早發(fā)現(xiàn)錯(cuò)誤:?jiǎn)卧獪y(cè)試有助于在開(kāi)發(fā)過(guò)程的早期發(fā)現(xiàn)問(wèn)題,這時(shí)修復(fù)錯(cuò)誤的成本比在生產(chǎn)環(huán)境中要低得多。
- 自動(dòng)化測(cè)試:?jiǎn)卧獪y(cè)試可以被自動(dòng)化運(yùn)行,它們是持續(xù)集成/持續(xù)部署(CI/CD)流程的重要組成部分。
- 減少調(diào)試時(shí)間:當(dāng)出現(xiàn)問(wèn)題時(shí),單元測(cè)試可以幫助快速定位錯(cuò)誤。
集成測(cè)試的好處
- 驗(yàn)證組件交互:通過(guò)集成測(cè)試可以確保不同系統(tǒng)組件(如數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)層、API等)能夠正確地協(xié)同工作。
- 發(fā)現(xiàn)接口問(wèn)題:它可以捕捉到單元測(cè)試可能遺漏的接口不匹配、數(shù)據(jù)格式錯(cuò)誤或通信問(wèn)題。
- 檢測(cè)系統(tǒng)級(jí)問(wèn)題:集成測(cè)試幫助識(shí)別配置錯(cuò)誤、環(huán)境問(wèn)題、服務(wù)依賴問(wèn)題等系統(tǒng)級(jí)別的問(wèn)題。
- 真實(shí)的使用場(chǎng)景:集成測(cè)試更接近用戶的實(shí)際使用場(chǎng)景,有助于保證用戶體驗(yàn)的質(zhì)量。
- 減少手動(dòng)測(cè)試:自動(dòng)化的集成測(cè)試可以減少對(duì)手動(dòng)測(cè)試的依賴,節(jié)約時(shí)間和成本。
- 提高信心:通過(guò)在接近生產(chǎn)的環(huán)境中運(yùn)行集成測(cè)試,團(tuán)隊(duì)可以對(duì)代碼發(fā)布到生產(chǎn)環(huán)境的可靠性更有信心。
- 端到端流程驗(yàn)證:集成測(cè)試有助于驗(yàn)證應(yīng)用程序的端到端工作流程和業(yè)務(wù)邏輯。
總體而言,單元測(cè)試和集成測(cè)試提供了一個(gè)強(qiáng)大的安全網(wǎng),可以在整個(gè)軟件開(kāi)發(fā)生命周期中確保軟件質(zhì)量。它們有助于開(kāi)發(fā)團(tuán)隊(duì)及時(shí)發(fā)現(xiàn)和解決問(wèn)題,提高開(kāi)發(fā)效率,減少后期維護(hù)的負(fù)擔(dān),并最終提供更穩(wěn)定、更可靠的軟件產(chǎn)品。
編寫(xiě)測(cè)試的最佳實(shí)踐
編寫(xiě)單元測(cè)試和集成測(cè)試時(shí)遵循一些最佳實(shí)踐可以提高測(cè)試的可維護(hù)性、有效性和效率。以下是一些最佳實(shí)踐和建議:
單元測(cè)試最佳實(shí)踐
- 遵循FIRST原則:測(cè)試應(yīng)該是Fast(快速的)、Independent(獨(dú)立的)、Repeatable(可重復(fù)的)、Self-validating(自我驗(yàn)證的)和Timely(及時(shí)的)。
- 測(cè)試單一功能:每個(gè)測(cè)試應(yīng)該集中驗(yàn)證單一功能點(diǎn)或行為。
- 模擬依賴:使用模擬(Mocking)和樁(Stubbing)技術(shù)來(lái)隔離被測(cè)試的組件,確保測(cè)試的獨(dú)立性和確定性。
- 描述性的測(cè)試名稱:給測(cè)試方法起一個(gè)描述性的名稱,說(shuō)明它們驗(yàn)證的行為。
- 避免測(cè)試私有方法:專注于測(cè)試公共接口。私有方法的行為應(yīng)該通過(guò)公共方法的測(cè)試來(lái)驗(yàn)證。
- 不要過(guò)度模擬:只模擬外部依賴和無(wú)法控制的部分,否則可能會(huì)隱藏真實(shí)的集成問(wèn)題。
- 測(cè)試覆蓋重要路徑:確保測(cè)試覆蓋代碼的所有重要執(zhí)行路徑,包括邊界條件和異常情況。
- 保持測(cè)試簡(jiǎn)單:測(cè)試代碼應(yīng)該簡(jiǎn)單直接,避免復(fù)雜的邏輯和控制流。
集成測(cè)試最佳實(shí)踐
- 選擇適當(dāng)?shù)牧6?/strong>:集成測(cè)試不需要覆蓋每個(gè)組件間的所有交互,而是應(yīng)該集中在關(guān)鍵的集成點(diǎn)。
- 使用真實(shí)環(huán)境:盡可能地使用與生產(chǎn)環(huán)境類似的配置和依賴,包括數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)和服務(wù)。
- 準(zhǔn)備測(cè)試數(shù)據(jù):為集成測(cè)試準(zhǔn)備適當(dāng)?shù)臏y(cè)試數(shù)據(jù),并確保它們?cè)跍y(cè)試開(kāi)始前正確地設(shè)置,并在測(cè)試結(jié)束后進(jìn)行清理。
- 避免跨服務(wù)邊界:在需要時(shí)模擬外部服務(wù)或使用契約測(cè)試,以避免由于外部服務(wù)不穩(wěn)定導(dǎo)致的測(cè)試失敗。
- 并行化和分離:盡可能并行化測(cè)試以提高執(zhí)行速度,并將測(cè)試分離到不同的模塊或管道階段中。
- 測(cè)試失敗時(shí)的診斷信息:確保測(cè)試失敗時(shí)提供足夠的診斷信息,方便快速定位問(wèn)題。
- 定期維護(hù)和更新:隨著系統(tǒng)演變,集成測(cè)試也應(yīng)該進(jìn)行定期的維護(hù)和更新。
- 遵循CI/CD的實(shí)踐:集成測(cè)試應(yīng)該集成到持續(xù)集成/持續(xù)部署流程中,確保代碼變更后自動(dòng)執(zhí)行。
通用建議
- 編寫(xiě)可測(cè)試的代碼:良好的設(shè)計(jì)通常會(huì)導(dǎo)致更容易測(cè)試的代碼,考慮可測(cè)試性可以幫助優(yōu)化設(shè)計(jì)。
- 定期運(yùn)行測(cè)試:自動(dòng)化測(cè)試應(yīng)該頻繁運(yùn)行,最好是每次代碼提交時(shí)都執(zhí)行。
- 使用專業(yè)的測(cè)試工具:利用專業(yè)的單元測(cè)試(如JUnit、TestNG)和集成測(cè)試工具(如Spring Test、TestContainers)。
- 測(cè)試和代碼一起演變:隨著產(chǎn)品需求和代碼的變化,測(cè)試也應(yīng)該相應(yīng)地更新和維護(hù)。
- 優(yōu)先考慮測(cè)試的可讀性:清晰的測(cè)試代碼有助于其他開(kāi)發(fā)者理解測(cè)試的意圖,并在需要時(shí)進(jìn)行修改。
- 復(fù)用和抽象測(cè)試代碼:測(cè)試中的常用模式可以抽象成工具方法或測(cè)試輔助類,以便復(fù)用。
以下是寫(xiě)出容易被單元測(cè)試代碼的建議:
- 遵循單一職責(zé)原則(SRP)。盡量將類或方法設(shè)計(jì)成只做一件事,這樣可以減少對(duì)其他類或方法的依賴,方便進(jìn)行單元測(cè)試。
- 采用依賴注入(DI)。使用依賴注入框架或手動(dòng)注入依賴,可以使被測(cè)試類的依賴更容易模擬和替換,從而方便單元測(cè)試。
- 避免靜態(tài)方法和全局變量。靜態(tài)方法和全局變量會(huì)增加代碼的耦合性,并且在單元測(cè)試中難以控制和替換,應(yīng)該盡量避免使用。
- 編寫(xiě)可測(cè)試的代碼。在編寫(xiě)代碼時(shí),要考慮測(cè)試的可行性和可擴(kuò)展性。例如,盡量避免使用隨機(jī)數(shù)、不可控制的時(shí)間戳等。
- 使用斷言(Assertion)。在單元測(cè)試中,使用斷言來(lái)驗(yàn)證代碼的正確性,可以大大提高測(cè)試的可靠性和可維護(hù)性。
- 使用Mock對(duì)象。在單元測(cè)試中,使用Mock對(duì)象可以模擬依賴的行為,更加方便和高效地進(jìn)行測(cè)試。
- 編寫(xiě)可讀性高的代碼。編寫(xiě)可讀性高的代碼可以方便其他人或自己進(jìn)行單元測(cè)試和維護(hù)。
總之,編寫(xiě)容易被單元測(cè)試的代碼需要注重代碼的可測(cè)試性、可讀性和可擴(kuò)展性,遵循良好的設(shè)計(jì)原則和編碼規(guī)范,使用合適的工具和技術(shù)。遵循這些最佳實(shí)踐和建議可以幫助你編寫(xiě)出更高效、更可靠的單元測(cè)試和集成測(cè)試,進(jìn)而提高整體的軟件質(zhì)量。
實(shí)踐篇
靜態(tài)方法如何寫(xiě)單測(cè)
在Mockito的早期版本中,模擬靜態(tài)方法是不支持的,但從Mockito 3.4.0開(kāi)始,通過(guò)使用mockito-inline模塊,可以對(duì)靜態(tài)方法進(jìn)行模擬。假設(shè)有一個(gè)靜態(tài)方法需要被模擬,下面是如何進(jìn)行操作的示例:
首先,確保你在項(xiàng)目中添加了正確的Mockito依賴。如果你使用Maven,你需要添加mockito-core和mockito-inline依賴:
<dependencies>
<!-- 其他依賴 -->
<!-- Mockito的核心庫(kù) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.4.0</version> <!-- 或者更高版本 -->
<scope>test</scope>
</dependency>
<!-- 支持模擬靜態(tài)方法的庫(kù) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.4.0</version> <!-- 或者更高版本 -->
<scope>test</scope>
</dependency>
</dependencies>
然后,使用Mockito的try資源塊來(lái)創(chuàng)建一個(gè)模擬的靜態(tài)方法調(diào)用。下面是一個(gè)如何對(duì)靜態(tài)方法進(jìn)行模擬的例子:
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*;
class SomeClass {
public static String staticMethod() {
return "實(shí)際的靜態(tài)方法調(diào)用";
}
}
public class SomeClassTest {
@Test
public void testStaticMethodMocking() {
// 開(kāi)始模擬靜態(tài)方法
try (MockedStatic<SomeClass> mockedStatic = Mockito.mockStatic(SomeClass.class)) {
// 指定靜態(tài)方法的期望行為
mockedStatic.when(SomeClass::staticMethod).thenReturn("模擬的靜態(tài)方法調(diào)用");
// 調(diào)用靜態(tài)方法并驗(yàn)證模擬行為是否生效
String result = SomeClass.staticMethod();
assertEquals("模擬的靜態(tài)方法調(diào)用", result);
// 驗(yàn)證靜態(tài)方法是否被調(diào)用
mockedStatic.verify(SomeClass::staticMethod);
}
// 在資源塊結(jié)束后,靜態(tài)方法的模擬將會(huì)自動(dòng)失效
}
}
在這個(gè)例子中,我們使用Mockito的mockStatic方法來(lái)模擬SomeClass類的靜態(tài)方法staticMethod。在try資源塊中,我們?cè)O(shè)置了期望行為,然后調(diào)用了靜態(tài)方法并驗(yàn)證了結(jié)果。當(dāng)try資源塊結(jié)束后,靜態(tài)方法的模擬自動(dòng)失效,恢復(fù)原始行為。
請(qǐng)注意,模擬靜態(tài)方法時(shí),應(yīng)該只在必要時(shí)使用,因?yàn)檫@可能會(huì)隱藏代碼中的設(shè)計(jì)問(wèn)題。盡量通過(guò)重構(gòu)來(lái)避免對(duì)靜態(tài)方法的依賴,使代碼更容易測(cè)試。
抽象方法如何寫(xiě)單測(cè)
在Mockito中,對(duì)抽象方法進(jìn)行單元測(cè)試通常涉及創(chuàng)建一個(gè)抽象類的具體子類或模擬實(shí)例。以下是如何使用Mockito對(duì)抽象方法進(jìn)行單元測(cè)試的基本步驟:
1. 直接創(chuàng)建匿名類
如果你的抽象類只有少數(shù)幾個(gè)方法需要被模擬,你可以創(chuàng)建一個(gè)匿名子類,并在其中實(shí)現(xiàn)這些方法:
@Test
public void testAbstractMethod() {
// 創(chuàng)建抽象類的匿名實(shí)現(xiàn)
AbstractClass testInstance = new AbstractClass() {
@Override
public String abstractMethod() {
return "mocked response";
}
};
// 使用testInstance進(jìn)行測(cè)試
assertEquals("mocked response", testInstance.abstractMethod());
}
在這個(gè)簡(jiǎn)單的例子中,我們重寫(xiě)了abstractMethod并返回了一個(gè)已經(jīng)模擬的響應(yīng)字符串。
2. 使用Mockito模擬
Mockito允許你直接模擬抽象類的具體實(shí)例,并為其抽象方法指定行為,你可以使用mock()方法創(chuàng)建一個(gè)模擬并使用when()來(lái)指定期望的行為。
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public abstract class AbstractClass {
public abstract String abstractMethod();
}
public class AbstractClassTest {
@Test
public void testAbstractMethodWithMockito() {
// 使用Mockito創(chuàng)建AbstractClass的模擬實(shí)例
AbstractClass mockAbstractClass = mock(AbstractClass.class);
// 配置模擬行為:當(dāng)調(diào)用abstractMethod時(shí)返回"mocked response"
when(mockAbstractClass.abstractMethod()).thenReturn("mocked response");
// 測(cè)試模擬的方法
assertEquals("mocked response", mockAbstractClass.abstractMethod());
}
}
這種方式不需要實(shí)際創(chuàng)建一個(gè)子類實(shí)例。Mockito允許你模擬抽象方法,并定義方法被調(diào)用時(shí)的行為。
3. 使用Mockito的spy
如果你想要對(duì)抽象類的實(shí)例進(jìn)行部分模擬(模擬一些方法,而其他方法則保持原有行為),你可以使用Mockito的spy方法。但是,這通常需要?jiǎng)?chuàng)建抽象類的一個(gè)具體子類實(shí)例。
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public abstract class AbstractClass {
public abstract String abstractMethod();
public String concreteMethod() {
return "concrete response";
}
}
public class AbstractClassTest {
@Test
public void testAbstractMethodWithSpy() {
// 創(chuàng)建AbstractClass的匿名實(shí)現(xiàn),并創(chuàng)建一個(gè)spy
AbstractClass spyAbstractClass = spy(new AbstractClass() {
@Override
public String abstractMethod() {
return "actual implementation";
}
});
// 修改spy,使得abstractMethod返回"mocked response"
doReturn("mocked response").when(spyAbstractClass).abstractMethod();
// 測(cè)試模擬的方法
assertEquals("mocked response", spyAbstractClass.abstractMethod());
// 測(cè)試未被模擬的具體方法
assertEquals("concrete response", spyAbstractClass.concreteMethod());
}
}
在這個(gè)例子中,我們創(chuàng)建了AbstractClass的一個(gè)匿名實(shí)現(xiàn),并對(duì)它進(jìn)行了部分模擬(spy)。然后我們修改了abstractMethod的行為,而concreteMethod保持原有實(shí)現(xiàn)。
以上三種方法中,使用Mockito模擬抽象類是最常見(jiàn)和通用的方法,它不需要額外的類定義,也不需要實(shí)際實(shí)現(xiàn)抽象方法。但在某些情況下,如果你需要測(cè)試抽象類的方法實(shí)現(xiàn),創(chuàng)建一個(gè)匿名子類或使用spy方法可能是更合適的選擇。
異常如何寫(xiě)單測(cè)
在單元測(cè)試中測(cè)試異常通常涉及到兩個(gè)方面:
- 確保代碼在特定條件下拋出預(yù)期的異常
- 驗(yàn)證異常處理邏輯是否正確
以下是在Java中測(cè)試異常的幾種方法:
方法1:try catch手動(dòng)構(gòu)造異常并捕獲
@Test
public void testDivide() {
try {
// 構(gòu)造輸入,并調(diào)用被測(cè)方法
var output = testFunction(input);
fail("no exception");
} catch (Exception e) {
assertTrue(expectedException);
assertTrue(e.getMessage().contains("some message in exception"));
}
}
@Test
public void testDivide() {
try {
int i = 1/0;
fail("Expected an ArithmeticException to be thrown");
} catch (ArithmeticException ae) {
assertTrue(true);
}
}
由于構(gòu)造的單測(cè)目的就是為了測(cè)試拋出異常的正確性,所以沒(méi)有拋出異常需要認(rèn)為測(cè)試不通過(guò),標(biāo)識(shí)為fail。
方法2:使用@Test的expected屬性捕獲異常
JUnit 4 中你可以使用@Test
注解的 expected 屬性來(lái)指定預(yù)期拋出的異常類型。
import org.junit.Test;
public class ExceptionTest {
@Test(expected = IllegalArgumentException.class)
public void whenExceptionThrown_thenExpectationSatisfied() {
MyClass myClass = new MyClass();
myClass.methodThatShouldThrowException();
}
}
使用@Test注解的expected屬性來(lái)指定期望拋出的異常類型為 IllegalArgumentException
。當(dāng)測(cè)試的方法拋出IllegalArgumentException
異常時(shí),測(cè)試將會(huì)通過(guò)。如果沒(méi)有拋出異常或拋出了不同類型的異常,測(cè)試將會(huì)失敗。方法2的不足是無(wú)法判斷異常中e.getMessage()的具體信息內(nèi)容。
當(dāng)你使用Mockito框架進(jìn)行模擬測(cè)試時(shí),也可以輕松地測(cè)試拋出異常的情況,配置mock對(duì)象拋出異常:
import org.junit.Test;
import org.mockito.Mockito;
public class ExceptionTest {
@Test(expected = IOException.class)
public void whenConfiguredMockException_thenThrow() throws IOException {
MyCollaborator collaborator = Mockito.mock(MyCollaborator.class);
Mockito.when(collaborator.doSomething()).thenThrow(new IOException());
collaborator.doSomething(); // 這將拋出IOException異常
}
}
在這個(gè)例子中,MyCollaborator是一個(gè)被模擬的協(xié)作類,我們配置了它的doSomething方法在調(diào)用時(shí)拋出IOException。然后我們嘗試調(diào)用這個(gè)方法,并驗(yàn)證了是否拋出了異常。
方法3:使用 @Rule 和 ExpectedException 類捕獲異常
public class ExceptionTest {
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void testDivide() {
exception.expect(ArithmeticException.class);
exception.expectMessage("cannot divide 0");
int i = 1/0;
}
}
聲明了一個(gè)ExpectedException對(duì)象exception,并使用@Rule注解將它聲明為測(cè)試規(guī)則。在測(cè)試方法中,利用exception.expect方法指定期望拋出的異常類型,利用exception.expectMessage方法指定期望拋出異常中包含的信息。
方法4: 使用assertThrows方法
JUnit 5提供了更靈活的異常測(cè)試機(jī)制,assertThrows
是JUnit 5中用于捕獲和驗(yàn)證異常的方法。它可以驗(yàn)證異常的類型,并允許對(duì)異常對(duì)象進(jìn)行進(jìn)一步的斷言。
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ExceptionTest {
@Test
public void whenDerivedExceptionThrown_thenAssertionSucceeds() {
MyClass myClass = new MyClass();
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
myClass.methodThatShouldThrowException();
});
// 可選的額外斷言,比如檢查異常消息
assertEquals("Expected message", exception.getMessage());
}
}
assertThrows方法會(huì)返回捕獲到的異常,這樣你就可以對(duì)異常對(duì)象進(jìn)行更詳細(xì)的斷言。
通過(guò)這些方法,你可以確保你的單元測(cè)試可以有效地驗(yàn)證異常情況,無(wú)論是確保方法在給定的條件下拋出正確的異常,還是驗(yàn)證異常處理邏輯的正確性,良好的異常測(cè)試覆蓋可以顯著提高代碼的健壯性和質(zhì)量。
使用Mock對(duì)象
在軟件測(cè)試中為什么需要 mock 對(duì)象?mock 對(duì)象是用來(lái)模擬真實(shí)對(duì)象行為的假對(duì)象,mock 對(duì)象可以幫助我們?cè)趩卧獪y(cè)試和集成測(cè)試中隔離測(cè)試的組件,確保測(cè)試的準(zhǔn)確性和獨(dú)立性。在不同類型的測(cè)試中使用 mock 對(duì)象的原理相似,但具體應(yīng)用可能會(huì)有所不同。
以下是使用 mock 對(duì)象的一些具體原因:
- 隔離測(cè)試:Mock 對(duì)象可以幫助將被測(cè)試的單元與其依賴項(xiàng)隔離開(kāi)來(lái),這樣可以確保單元測(cè)試只關(guān)注于被測(cè)試單元的功能,而不受外部系統(tǒng)變化或不穩(wěn)定性的影響。
- 控制測(cè)試環(huán)境:使用 mock 對(duì)象可以讓測(cè)試者完全控制測(cè)試環(huán)境。這意味著可以精確地模擬特定的條件和情況,例如異常情況、邊界情況或罕見(jiàn)事件。
- 減少測(cè)試成本:與真實(shí)的依賴項(xiàng)(如數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)服務(wù)等)交互可能需要額外的資源和設(shè)置。Mock 對(duì)象可以避免這些成本,因?yàn)樗鼈冊(cè)趦?nèi)存中運(yùn)行并可以快速配置。
- 提高測(cè)試速度:訪問(wèn)實(shí)際的外部資源、服務(wù)或數(shù)據(jù)庫(kù)通常需要顯著的時(shí)間。Mock 對(duì)象通常在內(nèi)存中執(zhí)行,不需要網(wǎng)絡(luò)調(diào)用或磁盤(pán) I/O,這可以顯著提高測(cè)試的執(zhí)行速度。
- 簡(jiǎn)化測(cè)試:有些外部系統(tǒng)可能極其復(fù)雜,要在測(cè)試中設(shè)置和管理它們可能非常困難。Mock 對(duì)象允許模擬這些復(fù)雜系統(tǒng)的行為,而無(wú)需實(shí)際與它們交互。
- 可預(yù)測(cè)的行為:真實(shí)系統(tǒng)可能會(huì)因?yàn)槎喾N原因而表現(xiàn)出不穩(wěn)定的行為,而 mock 對(duì)象可以提供一致的、可預(yù)測(cè)的響應(yīng),幫助編寫(xiě)穩(wěn)定的測(cè)試。
- 測(cè)試無(wú)法訪問(wèn)的代碼路徑:有些代碼路徑可能很難通過(guò)真實(shí)的輸入和依賴來(lái)測(cè)試,例如錯(cuò)誤處理代碼或特定的異常情況。Mock 對(duì)象可以輕松地模擬這些情況,確保這些代碼路徑得到有效的測(cè)試。
- 并發(fā)測(cè)試:在多線程環(huán)境中,真實(shí)的依賴可能會(huì)因?yàn)楦?jìng)爭(zhēng)條件而導(dǎo)致不確定的結(jié)果。Mock 對(duì)象可以用來(lái)創(chuàng)建一個(gè)更可控的環(huán)境來(lái)測(cè)試并發(fā)代碼。
- 避免對(duì)外部系統(tǒng)的影響:直接在生產(chǎn)級(jí)的服務(wù)或數(shù)據(jù)庫(kù)上進(jìn)行測(cè)試可能會(huì)導(dǎo)致數(shù)據(jù)污染或其他問(wèn)題。Mock 對(duì)象消除了這種風(fēng)險(xiǎn),因?yàn)樗鼈兣c真實(shí)的系統(tǒng)完全隔離。
- 法律或安全限制:某些情況下,對(duì)真實(shí)的數(shù)據(jù)或系統(tǒng)進(jìn)行測(cè)試可能受到法律或安全限制。Mock 對(duì)象可以模擬敏感數(shù)據(jù),而不會(huì)有泄露真實(shí)數(shù)據(jù)的風(fēng)險(xiǎn)。
使用 mock 對(duì)象是一個(gè)在軟件開(kāi)發(fā)過(guò)程中廣泛采用的最佳實(shí)踐,尤其是當(dāng)采用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)或行為驅(qū)動(dòng)開(kāi)發(fā)(BDD)方法時(shí)。然而,mock 對(duì)象應(yīng)該謹(jǐn)慎使用,因?yàn)樗鼈兛赡茈[藏真實(shí)環(huán)境中的問(wèn)題,因此在完成單元測(cè)試和集成測(cè)試后,仍然需要在真實(shí)環(huán)境中進(jìn)行系統(tǒng)測(cè)試和驗(yàn)收測(cè)試。
接下來(lái)我將介紹一下在單元測(cè)試和集成測(cè)試中如何使用 mock 對(duì)象。
單元測(cè)試中使用 Mock 對(duì)象
單元測(cè)試通常聚焦于測(cè)試系統(tǒng)中的一個(gè)單一組件,如一個(gè)類或者方法。在這個(gè)層面上,你可能會(huì)使用 mock 對(duì)象來(lái)模擬該組件依賴的其他組件的行為。這樣做可以確保你的測(cè)試僅關(guān)注于當(dāng)前組件的行為,并且不會(huì)受到外部依賴的影響。在單元測(cè)試中使用 mock 對(duì)象的步驟如下:
- 識(shí)別依賴:首先明確當(dāng)前測(cè)試單元依賴了哪些外部組件或服務(wù)。
- 創(chuàng)建 Mocks:使用 mock 框架(如 Mockito、Moq、JMock 等)創(chuàng)建這些依賴的 mock 版本。
- 配置 Mock 行為:設(shè)定 mock 對(duì)象的預(yù)期行為,包括確定當(dāng)調(diào)用特定方法時(shí)它們應(yīng)該返回的值或者拋出的異常。
- 注入 Mocks:將 mock 對(duì)象注入到測(cè)試單元中,替代真實(shí)的依賴。
- 執(zhí)行測(cè)試:運(yùn)行你的測(cè)試,此時(shí)測(cè)試單元將與 mock 對(duì)象交互而非真實(shí)的依賴。
- 驗(yàn)證 Mocks:最后,驗(yàn)證 mock 對(duì)象是否如預(yù)期那樣被調(diào)用了(例如,檢查是否調(diào)用了特定的方法或方法被調(diào)用的次數(shù))。
Mockito的基本使用
Mockito是一個(gè)流行的Java單元測(cè)試框架,它允許你創(chuàng)建和配置模擬對(duì)象,用于隔離需要測(cè)試的代碼。以下是一些基本的Mockito使用方法。
首先,確保添加Mockito依賴到你的項(xiàng)目中。如果你使用Maven,可以在pom.xml中添加如下依賴:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.11.2</version>
<scope>test</scope>
</dependency>
以下是一些基本的Mockito使用方法:
1. 創(chuàng)建模擬對(duì)象
在使用Mockito創(chuàng)建模擬對(duì)象時(shí),有幾種常用的方法:
1、使用mock()方法直接創(chuàng)建
// 創(chuàng)建一個(gè)模擬的List對(duì)象
List mockedList = mock(List.class);
2、使用注解@Mock
在測(cè)試類中,你可以聲明一個(gè)帶有@Mock
注解的字段。為了初始化這些注解,你需要在測(cè)試初始化時(shí)調(diào)用MockitoAnnotations.initMocks(this)
,或者使用MockitoJUnitRunner
運(yùn)行測(cè)試類。
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.junit.Before;
import org.junit.Test;
public class ExampleTest {
@Mock
private List mockedList;
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testMethod() {
// 使用模擬的mockedList對(duì)象進(jìn)行測(cè)試
}
}
如果你使用?JUnit 4,可以用@RunWith(MockitoJUnitRunner.class)
代替MockitoAnnotations.initMocks(this)
。
import static org.mockito.Mockito.*;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.junit.runner.RunWith;
import org.junit.Test;
@RunWith(MockitoJUnitRunner.class)
public class ExampleTest {
@Mock
private MyCollaborator collaborator;
@Test
public void testMethod() {
// 配置模擬對(duì)象
when(collaborator.someMethod()).thenReturn("expected response");
// 調(diào)用被測(cè)試的方法
MyClass myClass = new MyClass(collaborator);
myClass.performAction();
// 驗(yàn)證方法是否被調(diào)用
verify(collaborator).someMethod();
}
}
在這個(gè)例子中,我們不需要顯式調(diào)用MockitoAnnotations.initMocks(this)
,因?yàn)?code>MockitoJUnitRunner會(huì)幫我們完成模擬對(duì)象的初始化。@RunWith(MockitoJUnitRunner.class)
注解告訴JUnit使用Mockito提供的測(cè)試運(yùn)行器MockitoJUnitRunner
來(lái)運(yùn)行測(cè)試。這個(gè)運(yùn)行器提供了一些有用的功能,可以簡(jiǎn)化Mockito在測(cè)試中的使用,并確保更好的測(cè)試實(shí)踐。
以下是使用MockitoJUnitRunner
的一些好處:
-
自動(dòng)初始化模擬對(duì)象:在測(cè)試類中使用@Mock注解創(chuàng)建模擬對(duì)象時(shí),不需要顯式調(diào)用
MockitoAnnotations.initMocks(this)
方法來(lái)初始化這些模擬對(duì)象。MockitoJUnitRunner
會(huì)負(fù)責(zé)在每個(gè)測(cè)試方法執(zhí)行前自動(dòng)初始化所有@Mock
注解的字段。 - 檢查未使用的存根:運(yùn)行器會(huì)在測(cè)試結(jié)束后檢查所有的存根(stub)是否被使用過(guò)。如果有任何未使用的存根,它會(huì)告知你,這通常是一個(gè)壞味道,因?yàn)槟憧赡軇?chuàng)建了一些不必要的測(cè)試設(shè)置。
-
簡(jiǎn)化測(cè)試代碼:使用
MockitoJUnitRunner
可以減少編寫(xiě)初始化代碼的需要,讓測(cè)試代碼更加簡(jiǎn)潔。 -
驗(yàn)證框架的正確使用:運(yùn)行器會(huì)對(duì)一些Mockito的錯(cuò)誤使用進(jìn)行檢查,比如當(dāng)一個(gè)不合法的參數(shù)傳遞給
when()
方法時(shí),它可能會(huì)拋出有用的錯(cuò)誤信息。
如果你正在使用JUnit 5,那么你不需要 @RunWith
注解,因?yàn)?JUnit 5 有一個(gè)內(nèi)建的擴(kuò)展模型。在JUnit 5中,你可以使用@ExtendWith(MockitoExtension.class)
注解來(lái)達(dá)到類似的效果。
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Test;
@ExtendWith(MockitoExtension.class)
public class ExampleTest {
@Mock
private List mockedList;
@Test
public void testMethod() {
// 使用模擬的mockedList對(duì)象進(jìn)行測(cè)試
}
}
2. 指定模擬對(duì)象的行為
// 配置模擬對(duì)象返回特定的值
when(mockObject.myMethod("some input")).thenReturn("expected output");
3. 驗(yàn)證測(cè)試結(jié)果
驗(yàn)證方法調(diào)用:
// 驗(yàn)證myMethod是否被調(diào)用了一次
verify(mockObject).myMethod("some input");
// 驗(yàn)證myMethod是否從未被調(diào)用
verify(mockObject, never()).myMethod("some input");
// 驗(yàn)證myMethod是否至少被調(diào)用了一次
verify(mockObject, atLeastOnce()).myMethod("some input");
// 驗(yàn)證myMethod是否被調(diào)用了指定次數(shù)
verify(mockObject, times(2)).myMethod("some input");
驗(yàn)證返回值:
// then
Assert.assertNotNull(result);
Assert.assertEquals(result.getResponseCode(), 200);
// 其他常用的斷言函數(shù)
Assert.assertTrue(...);
Assert.assertFalse(...);
Assert.assertSame(...);
Assert.assertEquals(...);
Assert.assertArrayEquals(...);
4. 模擬拋出異常
// 配置模擬對(duì)象在調(diào)用myMethod時(shí)拋出異常
when(mockObject.myMethod("some input")).thenThrow(new RuntimeException());
5. 參數(shù)匹配
Mockito提供了參數(shù)匹配器,允許靈活地指定輸入?yún)?shù),這些匹配器可以是any(), eq(), anyInt()等。
// 使用anyString()匹配器來(lái)匹配任何String類型的輸入
when(mockObject.myMethod(anyString())).thenReturn("expected output");
// 使用eq()匹配器來(lái)匹配特定的值
when(mockObject.myMethod(eq("specific input"))).thenReturn("expected output");
6. 模擬void方法
對(duì)于沒(méi)有返回值的方法(void方法),你可以使用doNothing()、doThrow()、doAnswer()來(lái)進(jìn)行模擬。
// 配置void方法什么都不做
doNothing().when(mockObject).myVoidMethod("some input");
// 配置void方法拋出異常
doThrow(new RuntimeException()).when(mockObject).myVoidMethod("some input");
7. 連續(xù)調(diào)用
thenReturn()方法和thenThrow()方法可以鏈?zhǔn)秸{(diào)用,以設(shè)置連續(xù)調(diào)用的行為。
// 第一次調(diào)用返回值"first call",第二次調(diào)用返回值"second call" when(mockObject.myMethod("input")).thenReturn("first call").thenReturn("second call");
8. 模擬真實(shí)調(diào)用(部分模擬)
有時(shí),你可能想調(diào)用真實(shí)的方法實(shí)現(xiàn),而不是返回模擬的結(jié)果。這可以通過(guò)spy()來(lái)實(shí)現(xiàn)。使用spy時(shí),除非你顯式指定了模擬的行為,否則調(diào)用對(duì)象的方法都會(huì)執(zhí)行真實(shí)的邏輯。
// 創(chuàng)建一個(gè)“間諜”對(duì)象
MyClass spyObject = spy(new MyClass());
// 配置間諜對(duì)象的特定方法調(diào)用真實(shí)方法
doCallRealMethod().when(spyObject).myMethod("some input");
集成測(cè)試中使用 Mock 對(duì)象
集成測(cè)試通常涉及到多個(gè)組件的相互作用,目的是驗(yàn)證它們能夠協(xié)同工作。在這個(gè)層面上,mock 對(duì)象可以用來(lái)模擬外部系統(tǒng)或服務(wù),例如數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)服務(wù)或消息隊(duì)列。這樣做可以提供一個(gè)可控的環(huán)境來(lái)測(cè)試組件的集成。在集成測(cè)試中使用 mock 對(duì)象的步驟如下:
- 定義集成點(diǎn):確認(rèn)哪些外部系統(tǒng)或服務(wù)需要被集成,并且可能需要模擬。
- 創(chuàng)建 Mocks 或 Stubs:有時(shí)在集成測(cè)試中,可能會(huì)更多地使用 stubs(提供固定響應(yīng)的簡(jiǎn)單實(shí)現(xiàn)),而不是 mocks。使用合適的工具創(chuàng)建這些外部依賴的 mock 或 stub。
- 配置 Mock 行為或 Stubs:設(shè)置 mock 或 stub 對(duì)象的預(yù)期行為,以模擬外部系統(tǒng)的響應(yīng)。
- 集成 Mocks 或 Stubs:將 mock 或 stub 對(duì)象集成到你的測(cè)試環(huán)境中,以替代實(shí)際的外部依賴。
- 執(zhí)行集成測(cè)試:運(yùn)行集成測(cè)試,確保組件能夠與 mock 或 stub 對(duì)象合理交互。
- 驗(yàn)證結(jié)果:檢查系統(tǒng)的最終狀態(tài)或返回值,確保它們符合預(yù)期。
在使用 mock 對(duì)象時(shí),重要的是要理解它們并不是替代完整的集成測(cè)試或系統(tǒng)測(cè)試,而是作為測(cè)試策略中的一部分。Mock 對(duì)象能夠幫助我們?cè)诓皇芡獠凯h(huán)境影響的情況下測(cè)試代碼,但它們不能完全模擬真實(shí)世界的復(fù)雜性。因此,在測(cè)試周期的后期,還需要執(zhí)行含有真實(shí)依賴的測(cè)試,以確保系統(tǒng)在真實(shí)環(huán)境下的表現(xiàn)。
Spring Boot中的Mock對(duì)象
Spring Boot 包含一個(gè) @MockBean
注解,可用于在 ApplicationContext
中為 bean 定義 Mockito 模擬,可以使用注解來(lái)添加新 bean 或替換單個(gè)現(xiàn)有 bean ,@MockBean
可以直接用于測(cè)試類、測(cè)試中的字段或 @Configuration 類和字段。
在Spring Boot的測(cè)試中,當(dāng)你使用@SpringBootTest
注解時(shí),它會(huì)加載完整的應(yīng)用程序上下文并自動(dòng)啟用 Mock 的功能,如果在你的測(cè)試類中沒(méi)有使用 @SpringBootTest
,則必須手動(dòng)添加 @TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class })
示例如下:
import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener;
import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
@ContextConfiguration(classes = MyConfig.class)
@TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class })
class MyTests {
// ...
}
@MockBean
不能用于模擬在應(yīng)用程序上下文刷新期間執(zhí)行的 bean 的行為,因?yàn)樵趫?zhí)行測(cè)試時(shí)應(yīng)用程序上下文刷新已經(jīng)完成,現(xiàn)在配置模擬行為為時(shí)已晚。在這種情況下,我們建議使用 @Bean 方法來(lái)創(chuàng)建和配置模擬。
1. 使用Mockito模擬Bean
你可以使用@MockBean
注解來(lái)添加一個(gè)模擬到Spring應(yīng)用程序上下文中。這個(gè)Mock會(huì)替換任何現(xiàn)有的同類型的Bean,因此當(dāng)你的服務(wù)嘗試使用該Bean時(shí),它會(huì)使用你的Mock版本,而不是實(shí)際的實(shí)例。
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit4.SpringRunner;
import org.junit.runner.RunWith;
import org.junit.Test;
import org.mockito.Mockito;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ExternalServiceTest {
@MockBean
private ExternalService externalService; // 需要模擬的外部服務(wù)
@Autowired
private ServiceUnderTest serviceUnderTest; // 測(cè)試的目標(biāo)服務(wù)
@Test
public void testServiceMethod() {
// 設(shè)置模擬行為
Mockito.when(externalService.callExternalService()).thenReturn("Mock Response");
// 調(diào)用服務(wù)方法,它會(huì)使用模擬的外部服務(wù)
serviceUnderTest.serviceMethod();
// 驗(yàn)證外部服務(wù)是否被調(diào)用
Mockito.verify(externalService).callExternalService();
}
}
在這個(gè)例子中,ExternalService是我們想要模擬的外部服務(wù),而ServiceUnderTest是我們正在測(cè)試的服務(wù)。
然而,由于@MockBean
是基于對(duì) Bean 的完整聲明周期進(jìn)行 Mock,為了保證不同測(cè)試用例之間被 Mock 的 Bean 不會(huì)互相干擾,使用了不同 @MockBean
注解的測(cè)試用例之間不再?gòu)?fù)用 Spring 上下文,從而導(dǎo)致整個(gè)集成測(cè)試執(zhí)行期間會(huì)啟動(dòng)多次 Spring 上下文,這會(huì)帶來(lái)一些負(fù)面問(wèn)題:
- 整個(gè)集成測(cè)試的完整執(zhí)行時(shí)間變長(zhǎng);
- 一旦 Spring 上下文執(zhí)行過(guò)程中存在一些 JVM 級(jí)別的不可重入邏輯(例如通過(guò) static 變量實(shí)現(xiàn)不可重入邏輯),多次啟動(dòng)的 Spring 上下文將加載失敗,導(dǎo)致測(cè)試用例執(zhí)行失??;
如何解決這個(gè)問(wèn)題,可以參考 InjectorMockTestExecutionListener.java。它的原理就是:
-
在測(cè)試類開(kāi)始執(zhí)行前,先解析相關(guān)注解確定需要生成哪些 Mock/Spy 以及對(duì)應(yīng)的注入目標(biāo)(可能是 Bean 或者 SOFA 服務(wù))。
-
在測(cè)試方法執(zhí)行前,會(huì)將目標(biāo)中的相應(yīng)字段替換成 Mock/Spy,并執(zhí)行測(cè)試方法。
-
在測(cè)試類執(zhí)行完畢后,會(huì)將注入的 Mock/Spy 重置回原來(lái)的值,保證 Spring 上下文不被污染,因此 Spring Test 可以直接復(fù)用緩存的上下文。
spy 和 mock 的區(qū)別
在Mockito框架中,mock和spy是用于創(chuàng)建測(cè)試的兩種不同的方法,它們?cè)趩卧獪y(cè)試中有著不同的應(yīng)用場(chǎng)景。
Mock
使用mock方法創(chuàng)建的是一個(gè)完全的模擬對(duì)象,這種模擬對(duì)象沒(méi)有任何與原始類相關(guān)的行為,每個(gè)非void方法的默認(rèn)行為都是返回相應(yīng)類型的默認(rèn)值(比如0、false、null等),而void方法則不執(zhí)行任何操作。你需要為這個(gè)模擬對(duì)象手動(dòng)設(shè)置所有希望在測(cè)試中調(diào)用的方法的期望行為。
使用mock的場(chǎng)景是你想完全控制一個(gè)類的行為,通常是因?yàn)檫@個(gè)類很復(fù)雜,或者它的行為依賴于外部系統(tǒng),如數(shù)據(jù)庫(kù)或網(wǎng)絡(luò)服務(wù)。
List mockedList = mock(List.class);
when(mockedList.size()).thenReturn(100);
在上面的代碼中,mockedList對(duì)象是一個(gè)完全的模擬對(duì)象,其size()方法的行為被指定為返回100。
Spy
使用spy方法創(chuàng)建的是一個(gè)部分模擬的對(duì)象,這種對(duì)象的默認(rèn)行為是調(diào)用實(shí)際的方法,但你可以覆蓋某些方法的行為來(lái)滿足測(cè)試需求。它基于一個(gè)已經(jīng)存在的實(shí)例,可以讓你在保持大部分原有行為的基礎(chǔ)上,只修改其中一部分方法。
Spy通常用于那些不方便或不需要完全模擬的場(chǎng)景。比如,當(dāng)你想測(cè)試一個(gè)類的某個(gè)功能,而這個(gè)功能依賴于類中其他已經(jīng)被良好測(cè)試和驗(yàn)證的方法時(shí)。
List list = new ArrayList();
List spyList = spy(list);
doReturn(100).when(spyList).size(); // 正確的使用方式
// when(spyList.size()).thenReturn(100); // 錯(cuò)誤的使用方式,size()會(huì)被調(diào)用
在上面的代碼中,spyList是基于list的一個(gè)spy對(duì)象,它的大部分行為都和list一樣,但是size()方法的行為被修改為返回100。
使用Mockito中的Spy時(shí)要注意
- 你應(yīng)該盡可能避免使用Spy,因?yàn)樗鼈円肓苏鎸?shí)對(duì)象的狀態(tài),這可能會(huì)使測(cè)試變得復(fù)雜和脆弱。
- 在使用Spy時(shí),覆蓋方法行為時(shí)必須使用
doReturn()/doThrow()/doAnswer()
等語(yǔ)法,而不是when()/thenReturn()/thenThrow()/thenAnswer()
等語(yǔ)法,因?yàn)楹笳邥?huì)首先調(diào)用一次真實(shí)方法,然后再設(shè)置存根。
doReturn(100).when(spyList).size(); // 正確的使用方式
// when(spyList.size()).thenReturn(100); // 錯(cuò)誤的使用方式,size()會(huì)被調(diào)用
綜上所述,mock主要用于完全模擬對(duì)象,而spy用于在需要時(shí)只覆蓋部分方法的部分模擬對(duì)象。在單元測(cè)試中,通常推薦使用mock,因?yàn)檫@可以保持測(cè)試的獨(dú)立性和可預(yù)測(cè)性。Spy則在需要對(duì)現(xiàn)有實(shí)例進(jìn)行微調(diào)時(shí)使用。
問(wèn)題記錄
@MockBean mock的 bean 為 null
如果你在使用@MockBean進(jìn)行單元測(cè)試,但是發(fā)現(xiàn)mock的bean為null,這通常意味著Spring的測(cè)試上下文沒(méi)有正確設(shè)置或者@MockBean沒(méi)有被正確應(yīng)用。下面是一些可能導(dǎo)致這種情況的原因以及如何解決它們:
1. 確保包含Spring Boot測(cè)試依賴
首先,確認(rèn)你的項(xiàng)目中已經(jīng)包含了Spring Boot測(cè)試相關(guān)的依賴。
對(duì)于Maven,應(yīng)該包括以下依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
對(duì)于Gradle,應(yīng)該添加以下依賴:
testImplementation 'org.springframework.boot:spring-boot-starter-test'
2. 使用正確的測(cè)試注解
確保你使用了正確的測(cè)試注解,如@SpringBootTest、@DataJpaTest、@WebMvcTest等,這取決于你的測(cè)試類型。
例如:
@RunWith(SpringRunner.class)
@SpringBootTest
public class YourTest {
// ...
}
對(duì)于JUnit 5,使用以下注解:
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class YourTest {
// ...
}
3. 在測(cè)試類中使用@MockBean
確保@MockBean注解是被添加在測(cè)試類中,而不是在測(cè)試方法或其他地方。
@SpringBootTest
public class YourTest {
@MockBean
private YourService yourService;
// ...
}
4. 確保使用了Spring的測(cè)試運(yùn)行器
當(dāng)使用JUnit 4時(shí),確保你的測(cè)試類使用了@RunWith(SpringRunner.class)
或@RunWith(SpringJUnit4ClassRunner.class)
。
5. 正確初始化Mockito
如果你不使用@SpringBootTest
,而是用@ExtendWith(MockitoExtension.class)
來(lái)進(jìn)行普通的單元測(cè)試,那么你不能使用@MockBean,而應(yīng)該使用@Mock和@InjectMocks。
6. 避免循環(huán)依賴
如果你的測(cè)試中出現(xiàn)了循環(huán)依賴,它可能會(huì)導(dǎo)致@MockBean
無(wú)法正確工作。檢查你的應(yīng)用配置和Bean之間的依賴關(guān)系,確保沒(méi)有循環(huán)依賴。
7. 清理緩存的測(cè)試上下文
有時(shí)候,緩存的測(cè)試上下文可能會(huì)產(chǎn)生問(wèn)題。嘗試在IDE中清除構(gòu)建并重新運(yùn)行測(cè)試,或者在命令行中使用Maven或Gradle的清理命令。
8. 檢查測(cè)試配置文件
如果你的項(xiàng)目中有多個(gè)測(cè)試配置文件,確認(rèn)沒(méi)有其他配置覆蓋了你的MockBean。
如果以上步驟都無(wú)法解決問(wèn)題,還可以嘗試查看測(cè)試日志輸出和Spring的調(diào)試日志(通過(guò)設(shè)置logging.level.org.springframework=DEBUG)來(lái)獲取更多關(guān)于Bean初始化過(guò)程的信息。如果問(wèn)題仍然存在,可能需要更詳細(xì)地查看你的測(cè)試代碼和配置,檢查是否有其他配置或代碼影響了Spring的正常工作。
9. 檢查是否指定了 TestExecutionListeners
如果沒(méi)有使用 @SpringBootTest
,則需要手動(dòng)開(kāi)啟Mockito的 Listener,執(zhí)行依賴注入和reset操作,否則 @MockBean
注解的字段為 null。
import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener;
import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
@ContextConfiguration(classes = MyConfig.class)
@TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class })
class MyTests {
// ...
}
參考文檔
7 Popular Unit Test Naming Conventions
Power Use of Value Objects in DDD
Spring Boot @MockBean Example
Spring boot Mocking and Spying Beans
最后: 下方這份完整的軟件測(cè)試視頻教程已經(jīng)整理上傳完成,需要的朋友們可以自行領(lǐng)取【保證100%免費(fèi)】
軟件測(cè)試面試文檔
我們學(xué)習(xí)必然是為了找到高薪的工作,下面這些面試題是來(lái)自阿里、騰訊、字節(jié)等一線互聯(lián)網(wǎng)大廠最新的面試資料,并且有字節(jié)大佬給出了權(quán)威的解答,刷完這一套面試資料相信大家都能找到滿意的工作。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-761559.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-761559.html
這些都在我的軟件測(cè)試學(xué)習(xí)交流群里:902061117 自取
到了這里,關(guān)于java項(xiàng)目如何寫(xiě)單元測(cè)試的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!