本文翻譯自: https://reflectoring.io/unit-...原文做者:Tom Hombergshtml
譯文原地址:https://weyunx.com/2019/02/04...java
寫好單元測試是一門技術活,不過好在咱們如今有不少框架來幫助咱們學習。git
本文就爲您介紹這些框架,同時詳細介紹編寫優秀的 Sping Boot 單元測試所必需的技術細節,github
咱們將瞭解如何以可測試的方式建立 Spring bean,而後討論 Mockito 和 AssertJ 的使用,這兩個庫在默認狀況下都集成在 Spring Boot 裏。web
須要注意的是本文只討論單元測試,組裝測試、web 層測試和持久層測試會在後面的文章裏討論。spring
在本文中,咱們將使用 JUnit Jupiter (JUnit 5), Mockito, and AssertJ,同時還會引入 Lombok 來省去一些繁複的工做。數據庫
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')
spring-boot-starter-test
默認引入了 Mockito and AssertJ,對於 Lombok 則須要咱們本身手工引入。springboot
看一下下面的「單元」測試,是用來測試 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 項目。maven
可是,一個好的單元測試應該是毫秒級的,不然這會影響「test / code / test」的工做方式,這也就是測試驅動開發的思想 (TDD)。即便咱們不作 TDD,在編寫測試上花了太多時間也會影響咱們的開發思路。
其實,上面的測試方法實際執行只花費了幾毫秒,剩下的 4.5 秒所有花費在了 @SpringBootRun
上,由於 Spring Boot 須要啓動整個 Spring Boot 應用。
也就是說,咱們啓動整個應用,耗費了大量資源,僅僅是去爲了測試一個方法,當咱們的應用將來愈來愈大的時候,那將耗費更久的時間去啓動。
因此,爲何不要用 Spring Boot 來作單元測試呢?接下來,本文會討論如何不用 Spring Boot 來進行單元測試。
一般,咱們能夠有以下方法來讓咱們的 Spring beans 更容易進行測試。
首先咱們先看一個錯誤的例子:
@Service public class RegisterUseCase { @Autowired private UserRepository userRepository; public User registerUser(User user) { return userRepository.save(user); } }
然而這個類仍是必須經過 Spring 才能執行,由於咱們沒法繞過 UserRepository
這個實例。就像前面提到的,咱們必須換一種方法,不使用 @Autowired
來注入 UserRepository
。
知識點:不要注入
咱們看一下不使用 @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
實例。在單元測試中,咱們能夠像這樣來構建一個實例。
Spring 會自動的使用構造器來實例化一個 RegisterUseCase
對象。須要注意的是,在 Spring 5 以前,咱們須要@Autowired
註解來讓構造器生效。
一樣須要注意的是 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); } }
如今咱們的測試類就很簡潔,沒有冗餘繁複的代碼:
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(); } }
不過咱們還有一點遺漏,就是如何去模擬 UserRepository
實例,由於咱們不想去真正的去執行,由於它可能須要去鏈接數據庫。
現行的標準模擬庫是 Mockito,它提供了至少兩種方式來模擬 UserRepository
。
第一種方法就是直接使用 Mockito:
private UserRepository userRepository = Mockito.mock(UserRepository.class);
這個建立一個對象,看起來和 UserRepository
同樣。默認的狀況下,這個類什麼也不會作,若是調用有返回值的方法,也只會返回 null。
咱們的測試如今會是失敗,在 assertThat(savedUser.getRegistrationDate()).isNotNull()
這兒報 NullPointerException
空指針異常,由於 userRepository.save(user)
只會返回 null
。
因此,咱們須要告訴 Mockito,當 userRepository.save()
被調用的時候須要有返回值,因此咱們使用靜態的 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 具備一整套的測試方案,能夠用來模擬、匹配參數以及識別方法的調用,更多資料能夠參考這裏。
@Mock
此外還能夠用 @Mock
註解來模擬對象,它須要和 MockitoExtension
組合使用。
@ExtendWith(MockitoExtension.class) class RegisterUseCaseTest { @Mock private UserRepository userRepository; private RegisterUseCase registerUseCase; @BeforeEach void initUseCase() { registerUseCase = new RegisterUseCase(userRepository); } @Test void savedUserHasRegistrationDate() { // ... } }
@Mock
註解會指定字段將被注入到 mock 對象,@MockitoExtension
會告訴 Mockito 去掃描 @Mock
註解,由於 JUnit 不會自動去執行。
這其實和直接手工執行 Mockito.mock()
的結果同樣,只是使用習慣的區別。不過使用 MockitoExtension
咱們的測試就能夠綁定到測試框架裏。
須要說明的是咱們能夠在 registerUseCase
字段上使用 @InjectMocks
註解來替代手工構造一個 RegisterUseCase
對象,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();
一般,咱們能夠作小改動就可讓代碼變得更容易理解,因此咱們新建一個自定義的斷言對象:
public class UserAssert extends AbstractAssert<UserAssert, User> { public UserAssert(User user) { super(user, UserAssert.class); } public static UserAssert assertThat(User actual) { return new UserAssert(actual); } public UserAssert hasRegistrationDate() { isNotNull(); if (actual.getRegistrationDate() == null) { failWithMessage("Expected user to have a registration date, but it was null"); } return this; } }
這樣,咱們調用 UserAssert
類的 assertThat
方法,而不是直接從 Assertj 庫裏調用。
建立自定義的斷言看起來須要不少的工做量,但其實也就是幾分鐘的事。我相信這幾分鐘的工做,絕對是值得的,即便是讓代碼看起來更直白容易理解。測試代碼咱們只會寫一次,而後其餘人(包括我在之後)都只是去讀這段代碼,而後是反反覆覆的去修改這段代碼,直到產品消亡。
若是還有疑問,能夠參考 Assertions Generator。
咱們可能有種種的理由在 Spring 裏進行測試,可是對於一個普通的單元測試,能夠這麼作,可是沒有必要。隨着之後應用愈來愈龐大,啓動時間愈來愈長,可能還會帶來問題。因此,咱們在寫單元測試的時候,應該以一種更簡單的方式去構建 Sprnig bean。
Spring Boot Test Starter 附帶了 Mockito 和 AssertJ 做爲測試依賴庫,因此儘量的使用這些測試庫來作更好的單元測試吧。
全部的代碼能夠在這裏找到。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎斧正。