[翻譯]使用Spring Boot進行單元測試

原文地址:https://reflectoring.io/unit-...html

編寫好的單元測試能夠被當作一個很難掌握的藝術。但好消息是支持單元測試的機制很容易學習。java

本文給你提供在Spring Boot 應用程序中編寫好的單元測試的機制,而且深刻技術細節。git

咱們將帶你學習如何以可測試的方式建立Spring Bean實例,而後討論如何使用MockitoAssertJ,這兩個包在Spring Boot中都爲了測試默認引用了。github

本文只討論單元測試。至於集成測試,測試web層和測試持久層將會在接下來的系列文章中進行討論。web

代碼示例

本文附帶的代碼示例地址:spring-boot-testing算法

使用 Spring Boot 進行測試系列文章

這個教程是一個系列:spring

  1. 使用 Spring Boot 進行單元測試(本文)
  2. 使用 Spring Boot 和 @WebMvcTest 測試SpringMVC controller層
  3. 使用 Spring Boot 和 @DataJpaTest 測試JPA持久層查詢
  4. 經過 @SpringBootTest 進行集成測試

若是你喜歡看視頻教程,能夠看看Philip的課程:測試Spring Boot應用程序課程數據庫

依賴項

本文中,爲了進行單元測試,咱們會使用JUnit Jupiter(Junit 5)MockitoAssertJ。此外,咱們會引用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')
}

MockitoAssertJ會在spring-boot-test依賴中自動引用,可是咱們須要本身引用Lombok框架

不要在單元測試中使用Spring

若是你之前使用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來模擬依賴項

如今事實上的標準模擬庫是 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() {
    // ...
  }

}

使用AssertJ建立可讀斷言

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附帶MockitoAssertJ做爲測試庫。讓咱們利用這些測試庫來建立富有表現力的單元測試!

相關文章
相關標籤/搜索