使用Spring?Boot進(jìn)行單元測試詳情
前言
本文給你提供在Spring Boot 應(yīng)用程序中編寫好的單元測試的機(jī)制,并且深入技術(shù)細(xì)節(jié)。
我們將帶你學(xué)習(xí)如何以可測試的方式創(chuàng)建Spring Bean實(shí)例,然后討論如何使用Mockito
和AssertJ
,這兩個(gè)包在Spring Boot中都為了測試默認(rèn)引用了。
本文只討論單元測試。至于集成測試,測試web層和測試持久層將會(huì)在接下來的系列文章中進(jìn)行討論。
使用 Spring Boot 進(jìn)行測試系列文章
這個(gè)教程是一個(gè)系列:
- 使用 Spring Boot 進(jìn)行單元測試(本文)
- 使用 Spring Boot 和 @WebMvcTest 測試SpringMVC controller層
- 使用 Spring Boot 和 @DataJpaTest 測試JPA持久層查詢
- 通過 @SpringBootTest 進(jìn)行集成測試
如果你喜歡看視頻教程,可以看看Philip
的課程:測試Spring Boot應(yīng)用程序課程
依賴項(xiàng)
本文中,為了進(jìn)行單元測試,我們會(huì)使用JUnit Jupiter(Junit 5)
,Mockito
和AssertJ
。此外,我們會(huì)引用Lombok
來減少一些模板代碼:
dependencies{ compileOnly('org.projectlombok:lombok') testCompile('org.springframework.boot:spring-boot-starter-test') testCompile 'org.junit.jupiter:junit-jupiter-engine:5.2.0' testCompile('org.mockito:mockito-junit-jupiter:2.23.0') }
Mockito
和AssertJ
會(huì)在spring-boot-test
依賴中自動(dòng)引用,但是我們需要自己引用Lombok
。
不要在單元測試中使用Spring
如果你以前使用Spring
或者Spring Boot
寫過單元測試,你可能會(huì)說我們不要在寫單元測試的時(shí)候用Spring
。但是為什么呢?
考慮下面的單元測試類,這個(gè)類測試了RegisterUseCase
類的單個(gè)方法:
@ExtendWith(SpringExtension.class) @SpringBootTest class RegisterUseCaseTest { @Autowired private RegisterUseCase registerUseCase; @Test void savedUserHasRegistrationDate() { User user = new User("zaphod", "zaphod@mail.com"); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); } }
這個(gè)測試類在我的電腦上需要大概4.5秒來執(zhí)行一個(gè)空的Spring項(xiàng)目。
但是一個(gè)好的單元測試僅僅需要幾毫秒。否則就會(huì)阻礙TDD(測試驅(qū)動(dòng)開發(fā))流程,這個(gè)流程倡導(dǎo)“測試/開發(fā)/測試”。
但是就算我們不使用TDD,等待一個(gè)單元測試太久也會(huì)破壞我們的注意力。
執(zhí)行上述的測試方法事實(shí)上僅需要幾毫秒。剩下的4.5秒是因?yàn)?code>@SpringBootTest告訴了 Spring Boot
要啟動(dòng)整個(gè)Spring Boot 應(yīng)用程序上下文。
所以我們啟動(dòng)整個(gè)應(yīng)用程序僅僅是因?yàn)橐?code>RegisterUseCase實(shí)例注入到我們的測試類中。啟動(dòng)整個(gè)應(yīng)用程序可能耗時(shí)更久,假設(shè)應(yīng)用程序更大、Spring
需要加載更多的實(shí)例到應(yīng)用程序上下文中。
所以,這就是為什么不要在單元測試中使用Spring
。坦白說,大部分編寫單元測試的教程都沒有使用Spring Boot
。
創(chuàng)建一個(gè)可測試的類實(shí)例
然后,為了讓Spring
實(shí)例有更好的測試性,有幾件事是我們可以做的。
屬性注入是不好的
讓我們以一個(gè)反例開始。考慮下述類:
@Service public class RegisterUseCase { @Autowired private UserRepository userRepository; public User registerUser(User user) { return userRepository.save(user); } }
這個(gè)類如果沒有Spring
沒法進(jìn)行單元測試,因?yàn)樗鼪]有提供方法傳遞UserRepository
實(shí)例。因此我們只能用文章之前討論的方式-讓Spring創(chuàng)建UserRepository
實(shí)例,并通過@Autowired
注解注入進(jìn)去。
這里的教訓(xùn)是:不要用屬性注入。
提供一個(gè)構(gòu)造函數(shù)
實(shí)際上,我們根本不需要使用@Autowired
注解:
@Service public class RegisterUseCase { private final UserRepository userRepository; public RegisterUseCase(UserRepository userRepository) { this.userRepository = userRepository; } public User registerUser(User user) { return userRepository.save(user); } }
這個(gè)版本通過提供一個(gè)允許傳入UserRepository
實(shí)例參數(shù)的構(gòu)造函數(shù)來允許構(gòu)造函數(shù)注入。在這個(gè)單元測試中,我們現(xiàn)在可以創(chuàng)建這樣一個(gè)實(shí)例(或者我們之后要討論的Mock實(shí)例)并通過構(gòu)造函數(shù)注入了。
當(dāng)創(chuàng)建生成應(yīng)用上下文的時(shí)候,Spring會(huì)自動(dòng)使用這個(gè)構(gòu)造函數(shù)來初始化RegisterUseCase
對象。注意,在Spring 5 之前,我們需要在構(gòu)造函數(shù)上增加@Autowired
注解,以便讓Spring找到這個(gè)構(gòu)造函數(shù)。
還要注意的是,現(xiàn)在UserRepository
屬性是final
修飾的。這很重要,因?yàn)檫@樣的話,應(yīng)用程序生命周期時(shí)間內(nèi)這個(gè)屬性內(nèi)容不會(huì)再變化。此外,它還可以幫我們避免變成錯(cuò)誤,因?yàn)槿绻覀兺洺跏蓟搶傩缘脑挘幾g器就報(bào)錯(cuò)。
減少模板代碼
通過使用Lombok
的@RequiredArgsConstructor
注解,我們可以讓構(gòu)造函數(shù)自動(dòng)生成:
@Service @RequiredArgsConstructor public class RegisterUseCase { private final UserRepository userRepository; public User registerUser(User user) { user.setRegistrationDate(LocalDateTime.now()); return userRepository.save(user); } }
現(xiàn)在,我們有一個(gè)非常簡潔的類,沒有樣板代碼,可以在普通的 java 測試用例中很容易被實(shí)例化:
class RegisterUseCaseTest { private UserRepository userRepository = ...; private RegisterUseCase registerUseCase; @BeforeEach void initUseCase() { registerUseCase = new RegisterUseCase(userRepository); } @Test void savedUserHasRegistrationDate() { User user = new User("zaphod", "zaphod@mail.com"); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); } }
還有部分確實(shí),就是如何模擬測試類所依賴的UserReposity
實(shí)例,我們不想依賴真實(shí)的類,因?yàn)檫@個(gè)類需要一個(gè)數(shù)據(jù)庫連接。
使用Mockito來模擬依賴項(xiàng)
現(xiàn)在事實(shí)上的標(biāo)準(zhǔn)模擬庫是 Mockito
。它提供至少兩種方式來創(chuàng)建一個(gè)模擬UserRepository
實(shí)例,來填補(bǔ)前述代碼的空白。
使用普通Mockito
來模擬依賴
第一種方式是使用Mockito編程:
private UserRepository userRepository = Mockito.mock(UserRepository.class);
這會(huì)從外界創(chuàng)建一個(gè)看起來像UserRepository
的對象。默認(rèn)情況下,方法被調(diào)用時(shí)不會(huì)做任何事情,如果方法有返回值,會(huì)返回null
。
因?yàn)?code>userRepository.save(user)返回null,現(xiàn)在我們的測試代碼assertThat(savedUser.getRegistrationDate()).isNotNull()
會(huì)報(bào)空指針異常(NullPointerException)。
所以我們需要告訴Mockito
,當(dāng)userRepository.save(user)
調(diào)用的時(shí)候返回一些東西。我們可以用靜態(tài)的when
方法實(shí)現(xiàn):
@Test void savedUserHasRegistrationDate() { User user = new User("zaphod", "zaphod@mail.com"); when(userRepository.save(any(User.class))).then(returnsFirstArg()); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); }
這會(huì)讓userRepository.save()
返回和傳入對象相同的對象。
Mockito
為了模擬對象、匹配參數(shù)以及驗(yàn)證方法調(diào)用,提供了非常多的特性。想看更多,文檔
通過Mockito
的@Mock
注解模擬對象
創(chuàng)建一個(gè)模擬對象的第二種方式是使用Mockito
的@Mock
注解結(jié)合 JUnit Jupiter的MockitoExtension
一起使用:
@ExtendWith(MockitoExtension.class) class RegisterUseCaseTest { @Mock private UserRepository userRepository; private RegisterUseCase registerUseCase; @BeforeEach void initUseCase() { registerUseCase = new RegisterUseCase(userRepository); } @Test void savedUserHasRegistrationDate() { // ... } }
@Mock
注解指明那些屬性需要Mockito
注入模擬對象。由于JUnit
不會(huì)自動(dòng)實(shí)現(xiàn),MockitoExtension
則告訴Mockito
來評估這些@Mock
注解。
這個(gè)結(jié)果和調(diào)用Mockito.mock()
方法一樣,憑個(gè)人品味選擇即可。但是請注意,通過使用 MockitoExtension
,我們的測試用例被綁定到測試框架。
我們可以在RegisterUseCase
屬性上使用@InjectMocks
注解來注入實(shí)例,而不是手動(dòng)通過構(gòu)造函數(shù)構(gòu)造。Mockito
會(huì)使用特定的算法來幫助我們創(chuàng)建相應(yīng)實(shí)例對象:
@ExtendWith(MockitoExtension.class) class RegisterUseCaseTest { @Mock private UserRepository userRepository; @InjectMocks private RegisterUseCase registerUseCase; @Test void savedUserHasRegistrationDate() { // ... } }
使用AssertJ創(chuàng)建可讀斷言
Spring Boot
測試包自動(dòng)附帶的另一個(gè)庫是AssertJ
。我們在上面的代碼中已經(jīng)用到它進(jìn)行斷言:
assertThat(savedUser.getRegistrationDate()).isNotNull();
然而,有沒有可能讓斷言可讀性更強(qiáng)呢?像這樣,例子:
assertThat(savedUser).hasRegistrationDate();
有很多測試用例,只需要像這樣進(jìn)行很小的改動(dòng)就能大大提高可理解性。所以,讓我們在test/sources中創(chuàng)建我們自定義的斷言吧:
class UserAssert extends AbstractAssert<UserAssert, User> { UserAssert(User user) { super(user, UserAssert.class); } static UserAssert assertThat(User actual) { return new UserAssert(actual); } UserAssert hasRegistrationDate() { isNotNull(); if (actual.getRegistrationDate() == null) { failWithMessage( "Expected user to have a registration date, but it was null" ); } return this; } }
現(xiàn)在,如果我們不是從AssertJ
庫直接導(dǎo)入,而是從我們自定義斷言類UserAssert
引入assertThat
方法的話,我們就可以使用新的、更可讀的斷言。
創(chuàng)建一個(gè)這樣自定義的斷言類看起來很費(fèi)時(shí)間,但是其實(shí)幾分鐘就完成了。我相信,將這些時(shí)間投入到創(chuàng)建可讀性強(qiáng)的測試代碼中是值得的,即使之后它的可讀性只有一點(diǎn)點(diǎn)提高。我們編寫測試代碼就一次,但是之后,很多其他人(包括未來的我)在軟件生命周期中,需要閱讀、理解然后操作這些代碼很多次。
如果你還是覺得很費(fèi)事,可以看看斷言生成器
結(jié)論
盡管在測試中啟動(dòng)Spring應(yīng)用程序也有些理由,但是對于一般的單元測試,它不必要。有時(shí)甚至有害,因?yàn)楦L的周轉(zhuǎn)時(shí)間。換言之,我們應(yīng)該使用更容易支持編寫普通單元測試的方式構(gòu)建Spring實(shí)例。
Spring Boot Test Starter
附帶Mockito
和AssertJ
作為測試庫。讓我們利用這些測試庫來創(chuàng)建富有表現(xiàn)力的單元測試!
到此這篇關(guān)于使用Spring Boot進(jìn)行單元測試詳情的文章就介紹到這了,更多相關(guān)Spring Boot單元測試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之在線蛋糕銷售商城的實(shí)現(xiàn)
這是一個(gè)使用了java+JSP+Springboot+maven+mysql+ThymeLeaf+FTP開發(fā)的在線蛋糕銷售商城,是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有線上蛋糕商城該有的所有功能,感興趣的朋友快來看看吧2022-01-01IDEA設(shè)置JVM運(yùn)行參數(shù)的方法步驟
這篇文章主要介紹了IDEA設(shè)置JVM運(yùn)行參數(shù)的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08關(guān)于JDK源碼中的@author unascribed注釋閑談
這篇文章主要介紹了關(guān)于JDK源碼中的@author unascribed注釋閑談,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08JAVA的LIST接口的REMOVE重載方法調(diào)用原理解析
這篇文章主要介紹了JAVA的LIST接口的REMOVE重載方法調(diào)用原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10