<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
本文給你提供在Spring Boot 應用程式中編寫好的單元測試的機制,並且深入技術細節。
我們將帶你學習如何以可測試的方式建立Spring Bean範例,然後討論如何使用Mockito
和AssertJ
,這兩個包在Spring Boot中都為了測試預設參照了。
本文只討論單元測試。至於整合測試,測試web層和測試持久層將會在接下來的系列文章中進行討論。
這個教學是一個系列:
如果你喜歡看視訊教學,可以看看Philip
的課程:測試Spring Boot應用程式課程
本文中,為了進行單元測試,我們會使用JUnit Jupiter(Junit 5)
,Mockito
和AssertJ
。此外,我們會參照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
會在spring-boot-test
依賴中自動參照,但是我們需要自己參照Lombok
。
如果你以前使用Spring
或者Spring Boot
寫過單元測試,你可能會說我們不要在寫單元測試的時候用Spring
。但是為什麼呢?
考慮下面的單元測試類,這個類測試了RegisterUseCase
類的單個方法:
@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(); } }
這個測試類在我的電腦上需要大概4.5秒來執行一個空的Spring專案。
但是一個好的單元測試僅僅需要幾毫秒。否則就會阻礙TDD(測試驅動開發)流程,這個流程倡導“測試/開發/測試”。
但是就算我們不使用TDD,等待一個單元測試太久也會破壞我們的注意力。
執行上述的測試方法事實上僅需要幾毫秒。剩下的4.5秒是因為@SpringBootTest
告訴了 Spring Boot
要啟動整個Spring Boot 應用程式上下文。
所以我們啟動整個應用程式僅僅是因為要把RegisterUseCase
範例注入到我們的測試類中。啟動整個應用程式可能耗時更久,假設應用程式更大、Spring
需要載入更多的範例到應用程式上下文中。
所以,這就是為什麼不要在單元測試中使用Spring
。坦白說,大部分編寫單元測試的教學都沒有使用Spring Boot
。
然後,為了讓Spring
範例有更好的測試性,有幾件事是我們可以做的。
讓我們以一個反例開始。考慮下述類:
@Service public class RegisterUseCase { @Autowired private UserRepository userRepository; public User registerUser(User user) { return userRepository.save(user); } }
這個類如果沒有Spring
沒法進行單元測試,因為它沒有提供方法傳遞UserRepository
範例。因此我們只能用文章之前討論的方式-讓Spring建立UserRepository
範例,並通過@Autowired
註解注入進去。
這裡的教訓是:不要用屬性注入。
實際上,我們根本不需要使用@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); } }
這個版本通過提供一個允許傳入UserRepository
範例引數的建構函式來允許建構函式注入。在這個單元測試中,我們現在可以建立這樣一個範例(或者我們之後要討論的Mock範例)並通過建構函式注入了。
當建立生成應用上下文的時候,Spring會自動使用這個建構函式來初始化RegisterUseCase
物件。注意,在Spring 5 之前,我們需要在建構函式上增加@Autowired
註解,以便讓Spring找到這個建構函式。
還要注意的是,現在UserRepository
屬性是final
修飾的。這很重要,因為這樣的話,應用程式生命週期時間內這個屬性內容不會再變化。此外,它還可以幫我們避免變成錯誤,因為如果我們忘記初始化該屬性的話,編譯器就報錯。
通過使用Lombok
的@RequiredArgsConstructor
註解,我們可以讓建構函式自動生成:
@Service @RequiredArgsConstructor public class RegisterUseCase { private final UserRepository userRepository; public User registerUser(User user) { user.setRegistrationDate(LocalDateTime.now()); return userRepository.save(user); } }
現在,我們有一個非常簡潔的類,沒有樣板程式碼,可以在普通的 java 測試用例中很容易被範例化:
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(); } }
還有部分確實,就是如何模擬測試類所依賴的UserReposity
範例,我們不想依賴真實的類,因為這個類需要一個資料庫連線。
現在事實上的標準模擬庫是 Mockito
。它提供至少兩種方式來建立一個模擬UserRepository
範例,來填補前述程式碼的空白。
Mockito
來模擬依賴第一種方式是使用Mockito程式設計:
private UserRepository userRepository = Mockito.mock(UserRepository.class);
這會從外界建立一個看起來像UserRepository
的物件。預設情況下,方法被呼叫時不會做任何事情,如果方法有返回值,會返回null
。
因為userRepository.save(user)
返回null,現在我們的測試程式碼assertThat(savedUser.getRegistrationDate()).isNotNull()
會報空指標異常(NullPointerException)。
所以我們需要告訴Mockito
,當userRepository.save(user)
呼叫的時候返回一些東西。我們可以用靜態的when
方法實現:
@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(); }
這會讓userRepository.save()
返回和傳入物件相同的物件。
Mockito
為了模擬物件、匹配引數以及驗證方法呼叫,提供了非常多的特性。想看更多,檔案
Mockito
的@Mock
註解模擬物件建立一個模擬物件的第二種方式是使用Mockito
的@Mock
註解結合 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
不會自動實現,MockitoExtension
則告訴Mockito
來評估這些@Mock
註解。
這個結果和呼叫Mockito.mock()
方法一樣,憑個人品味選擇即可。但是請注意,通過使用 MockitoExtension
,我們的測試用例被繫結到測試框架。
我們可以在RegisterUseCase
屬性上使用@InjectMocks
註解來注入範例,而不是手動通過建構函式構造。Mockito
會使用特定的演演算法來幫助我們建立相應範例物件:
@ExtendWith(MockitoExtension.class) class RegisterUseCaseTest { @Mock private UserRepository userRepository; @InjectMocks private RegisterUseCase registerUseCase; @Test void savedUserHasRegistrationDate() { // ... } }
Spring Boot
測試包自動附帶的另一個庫是AssertJ
。我們在上面的程式碼中已經用到它進行斷言:
assertThat(savedUser.getRegistrationDate()).isNotNull();
然而,有沒有可能讓斷言可讀性更強呢?像這樣,例子:
assertThat(savedUser).hasRegistrationDate();
有很多測試用例,只需要像這樣進行很小的改動就能大大提高可理解性。所以,讓我們在test/sources中建立我們自定義的斷言吧:
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; } }
現在,如果我們不是從AssertJ
庫直接匯入,而是從我們自定義斷言類UserAssert
引入assertThat
方法的話,我們就可以使用新的、更可讀的斷言。
建立一個這樣自定義的斷言類看起來很費時間,但是其實幾分鐘就完成了。我相信,將這些時間投入到建立可讀性強的測試程式碼中是值得的,即使之後它的可讀性只有一點點提高。我們編寫測試程式碼就一次,但是之後,很多其他人(包括未來的我)在軟體生命週期中,需要閱讀、理解然後操作這些程式碼很多次。
如果你還是覺得很費事,可以看看斷言生成器
儘管在測試中啟動Spring應用程式也有些理由,但是對於一般的單元測試,它不必要。有時甚至有害,因為更長的週轉時間。換言之,我們應該使用更容易支援編寫普通單元測試的方式構建Spring範例。
Spring Boot Test Starter
附帶Mockito
和AssertJ
作為測試庫。讓我們利用這些測試庫來建立富有表現力的單元測試!
到此這篇關於使用Spring Boot進行單元測試詳情的文章就介紹到這了,更多相關Spring Boot單元測試內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45