使用 Spring Boot 和 @SpringBootTest 進行測試

信碼由韁發表於2021-11-05

【注】本文譯自: Testing with Spring Boot and @SpringBootTest - Reflectoring

使用@SpringBootTest 註解,Spring Boot 提供了一種方便的方法來啟動要在測試中使用的應用程式上下文。在本教程中,我們將討論何時使用 @SpringBootTest 以及何時更好地使用其他工具進行測試。我們還將研究自定義應用程式上下文的不同方法以及如何減少測試執行時間。

 程式碼示例

本文附有 GitHub 上的工作程式碼示例。

“使用 Spring Boot 進行測試”系列

本教程是系列的一部分:

  1. 使用 Spring Boot 進行單元測試
  2. 使用 Spring Boot 和 @WebMvcTest 測試 MVC Web Controller
  3. 使用 Spring Boot 和 @DataJpaTest 測試 JPA 查詢
  4. 使用 Spring Boot 和 @SpringBootTest 進行測試

整合測試與單元測試

在開始使用 Spring Boot 進行整合測試之前,讓我們定義整合測試與單元測試的區別。
單元測試涵蓋單個“單元”,其中一個單元通常是單個類,但也可以是組合測試的一組內聚類。
整合測試可以是以下任何一項:

  • 涵蓋多個“單元”的測試。它測試兩個或多個內聚類叢集之間的互動。
  • 覆蓋多個層的測試。這實際上是第一種情況的特化,例如可能涵蓋業務服務和持久層之間的互動。
  • 涵蓋整個應用程式路徑的測試。在這些測試中,我們嚮應用程式傳送請求並檢查它是否正確響應並根據我們的預期更改了資料庫狀態。

Spring Boot 提供了 @SpringBootTest 註解,我們可以使用它來建立一個應用程式上下文,其中包含我們對上述所有測試型別所需的所有物件。但是請注意,過度使用 @SpringBootTest 可能會導致測試套件執行時間非常長
因此,對於涵蓋多個單元的簡單測試,我們應該建立簡單的測試,與單元測試非常相似,在單元測試中,我們手動建立測試所需的物件圖並模擬其餘部分。這樣,Spring 不會在每次測試開始時啟動整個應用程式上下文。

測試切片

我們可以將我們的 Spring Boot 應用程式作為一個整體來測試、一個單元一個單元地測試、也可以一層一層地測試。使用 Spring Boot 的測試切片註解,我們可以分別測試每一層。
在我們詳細研究 @SpringBootTest 註解之前,讓我們探索一下測試切片註解,以檢查 @SpringBootTest 是否真的是您想要的。
@SpringBootTest 註解載入完整的 Spring 應用程式上下文。相比之下,測試切片註釋僅載入測試特定層所需的 bean。正因為如此,我們可以避免不必要的模擬和副作用。

@WebMvcTest

我們的 Web 控制器承擔許多職責,例如偵聽 HTTP 請求、驗證輸入、呼叫業務邏輯、序列化輸出以及將異常轉換為正確的響應。我們應該編寫測試來驗證所有這些功能。
@WebMvcTest 測試切片註釋將使用剛好足夠的元件和配置來設定我們的應用程式上下文,以測試我們的 Web 控制器層。例如,它將設定我們的@Controller@ControllerAdvice、一個 MockMvc bean 和其他一些自動配置
要閱讀有關 @WebMvcTest 的更多資訊並瞭解我們如何驗證每個職責,請閱讀我關於使用 Spring Boot 和 @WebMvcTest 測試 MVC Web 控制器的文章

@WebFluxTest

@WebFluxTest 用於測試 WebFlux 控制器。 @WebFluxTest 的工作方式類似於 @WebMvcTest 註釋,不同之處在於它不是 Web MVC 元件和配置,而是啟動 WebFlux 元件和配置。其中一個 bean 是 WebTestClient,我們可以使用它來測試我們的 WebFlux 端點。

@DataJpaTest

就像 @WebMvcTest 允許我們測試我們的 web 層一樣,@DataJpaTest 用於測試持久層。
它配置我們的實體、儲存庫並設定嵌入式資料庫。現在,這一切都很好,但是,測試我們的持久層意味著什麼? 我們究竟在測試什麼? 如果查詢,那麼什麼樣的查詢?要找出所有這些問題的答案,請閱讀我關於使用 Spring Boot 和 @DataJpaTest 測試 JPA 查詢的文章

@DataJdbcTest

Spring Data JDBC 是 Spring Data 系列的另一個成員。 如果我們正在使用這個專案並且想要測試持久層,那麼我們可以使用 @DataJdbcTest 註解 。@DataJdbcTest 會自動為我們配置在我們的專案中定義的嵌入式測試資料庫和 JDBC 儲存庫。
另一個類似的專案是 Spring JDBC,它為我們提供了 JdbcTemplate 物件來執行直接查詢。@JdbcTest 註解自動配置測試我們的 JDBC 查詢所需的 DataSource 物件。依賴
本文中的程式碼示例只需要依賴 Spring Boot 的 test starter 和 JUnit Jupiter:

dependencies {
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
}

使用 @SpringBootTest 建立 ApplicationContext

@SpringBootTest 在預設情況下開始在測試類的當前包中搜尋,然後在包結構中向上搜尋,尋找用 @SpringBootConfiguration 註解的類,然後從中讀取配置以建立應用程式上下文。這個類通常是我們的主要應用程式類,因為 @SpringBootApplication 註解包括 @SpringBootConfiguration 註解。然後,它會建立一個與在生產環境中啟動的應用程式上下文非常相似的應用程式上下文。
我們可以通過許多不同的方式自定義此應用程式上下文,如下一節所述。
因為我們有一個完整的應用程式上下文,包括 web 控制器、Spring 資料儲存庫和資料來源,@SpringBootTest 對於貫穿應用程式所有層的整合測試非常方便:

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private ObjectMapper objectMapper;

  @Autowired
  private UserRepository userRepository;

  @Test
  void registrationWorksThroughAllLayers() throws Exception {
    UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");

    mockMvc.perform(post("/forums/{forumId}/register", 42L)
            .contentType("application/json")
            .param("sendWelcomeMail", "true")
            .content(objectMapper.writeValueAsString(user)))
            .andExpect(status().isOk());

    UserEntity userEntity = userRepository.findByName("Zaphod");
    assertThat(userEntity.getEmail()).isEqualTo("zaphod@galaxy.net");
  }
}
@ExtendWith
本教程中的程式碼示例使用 @ExtendWith 註解告訴 JUnit 5 啟用 Spring 支援。從 Spring Boot 2.1 開始,我們不再需要載入 SpringExtension,因為它作為元註釋包含在 Spring Boot 測試註釋中,例如 @DataJpaTest@WebMvcTest@SpringBootTest

在這裡,我們另外使用 @AutoConfigureMockMvc 將 MockMvc 例項新增到應用程式上下文中。
我們使用這個 MockMvc 物件向我們的應用程式執行 POST 請求並驗證它是否按預期響應。
然後,我們使用應用程式上下文中的 UserRepository 來驗證請求是否導致資料庫狀態發生預期的變化。

自定義應用程式上下文

我們可以有很多種方法來自定義 @SpringBootTest 建立的應用程式上下文。讓我們看看我們有哪些選擇。

自定義應用上下文時的注意事項
應用程式上下文的每個自定義都是使其與在生產設定中啟動的“真實”應用程式上下文不同的另一件事。因此,為了使我們的測試儘可能接近生產,我們應該只定制讓測試執行真正需要的東西!

新增自動配置

在上面,我們已經看到了自動配置的作用:

@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
  ...
}

還有很多其他可用的自動配置,每個都可以將其他 bean 新增到應用程式上下文中。以下是文件中其他一些有用的內容:

  • @AutoConfigureWebTestClient:將 WebTestClient 新增到測試應用程式上下文。它允許我們測試伺服器端點。
  • @AutoConfigureTestDatabase:這允許我們針對真實資料庫而不是嵌入式資料庫執行測試。
  • @RestClientTest:當我們想要測試我們的 RestTemplate 時它會派上用場。 它自動配置所需的元件以及一個 MockRestServiceServer 物件,該物件幫助我們模擬來自 RestTemplate 呼叫的請求的響應。
  • @JsonTest:自動配置 JSON 對映器和類,例如 JacksonTesterGsonTester。使用這些我們可以驗證我們的 JSON 序列化/反序列化是否正常工作。

設定自定義配置屬性

通常,在測試中需要將一些配置屬性設定為與生產設定中的值不同的值:

@SpringBootTest(properties = "foo=bar")
class SpringBootPropertiesTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

如果屬性 foo 存在於預設設定中,它將被此測試的值 bar 覆蓋。

使用 @ActiveProfiles 外部化屬性

如果我們的許多測試需要相同的屬性集,我們可以建立一個配置檔案 application-<profile>.propertieapplication-<profile>.yml 並通過啟用某個配置檔案從該檔案載入屬性:

# application-test.yml
foo: bar
@SpringBootTest
@ActiveProfiles("test")
class SpringBootProfileTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

使用 @TestPropertySource 設定自定義屬性

另一種定製整個屬性集的方法是使用 @TestPropertySource 註釋:

# src/test/resources/foo.properties
foo=bar
@SpringBootTest
@TestPropertySource(locations = "/foo.properties")
class SpringBootPropertySourceTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

foo.properties 檔案中的所有屬性都載入到應用程式上下文中。@TestPropertySource 還可以 配置更多。

使用 @MockBean 注入模擬

如果我們只想測試應用程式的某個部分而不是從傳入請求到資料庫的整個路徑,我們可以使用 @MockBean 替換應用程式上下文中的某些 bean:

@SpringBootTest
class MockBeanTest {

  @MockBean
  private UserRepository userRepository;

  @Autowired
  private RegisterUseCase registerUseCase;

  @Test
  void testRegister(){
    // given
    User user = new User("Zaphod", "zaphod@galaxy.net");
    boolean sendWelcomeMail = true;
    given(userRepository.save(any(UserEntity.class))).willReturn(userEntity(1L));

    // when
    Long userId = registerUseCase.registerUser(user, sendWelcomeMail);

    // then
    assertThat(userId).isEqualTo(1L);
  }
 
}

在這種情況下,我們用模擬替換了 UserRepository bean。使用 Mockitogiven 方法,我們指定了此模擬的預期行為,以測試使用此儲存庫的類。
您可以在我關於模擬的文章中閱讀有關 @MockBean 註解的更多資訊。

使用 @Import 新增 Bean

如果某些 bean 未包含在預設應用程式上下文中,但我們在測試中需要它們,我們可以使用 @Import 註解匯入它們:

package other.namespace;

@Component
public class Foo {
}

@SpringBootTest
@Import(other.namespace.Foo.class)
class SpringBootImportTest {

  @Autowired
  Foo foo;

  @Test
  void test() {
    assertThat(foo).isNotNull();
  }
}

預設情況下,Spring Boot 應用程式包含它在其包和子包中找到的所有元件,因此通常只有在我們想要包含其他包中的 bean 時才需要這樣做。

使用 @TestConfiguration 覆蓋 Bean

使用 @TestConfiguration,我們不僅可以包含測試所需的其他 bean,還可以覆蓋應用程式中已經定義的 bean。在我們關於使用 @TestConfiguration 進行測試的文章中閱讀更多相關資訊。

建立自定義 @SpringBootApplication

我們甚至可以建立一個完整的自定義 Spring Boot 應用程式來啟動測試。如果這個應用程式類與真正的應用程式類在同一個包中,但是在測試源而不是生產源中,@SpringBootTest 會在實際應用程式類之前找到它,並從這個應用程式載入應用程式上下文。
或者,我們可以告訴 Spring Boot 使用哪個應用程式類來建立應用程式上下文:

@SpringBootTest(classes = CustomApplication.class)
class CustomApplicationTest {
}

但是,在執行此操作時,我們正在測試可能與生產環境完全不同的應用程式上下文,因此僅當無法在測試環境中啟動生產應用程式時,這才應該是最後的手段。但是,通常有更好的方法,例如使真實的應用程式上下文可配置以排除不會在測試環境中啟動的 bean。讓我們看一個例子。
假設我們在應用程式類上使用 @EnableScheduling 註解。每次啟動應用程式上下文時(即使在測試中),所有 @Scheduled 作業都將啟動,並且可能與我們的測試衝突。 我們通常不希望作業在測試中執行,因此我們可以建立第二個沒有 @EnabledScheduling 註釋的應用程式類,並在測試中使用它。但是,更好的解決方案是建立一個可以使用屬性切換的配置類:

@Configuration
@EnableScheduling
@ConditionalOnProperty(
        name = "io.reflectoring.scheduling.enabled",
        havingValue = "true",
        matchIfMissing = true)
public class SchedulingConfiguration {
}

我們已將 @EnableScheduling 註解從我們的應用程式類移到這個特殊的配置類。將屬性 io.reflectoring.scheduling.enabled 設定為 false 將導致此類不會作為應用程式上下文的一部分載入:

@SpringBootTest(properties = "io.reflectoring.scheduling.enabled=false")
class SchedulingTest {

  @Autowired(required = false)
  private SchedulingConfiguration schedulingConfiguration;

  @Test
  void test() {
    assertThat(schedulingConfiguration).isNull();
  }
}

我們現在已經成功地停用了測試中的預定作業。屬性 io.reflectoring.scheduling.enabled 可以通過上述任何方式指定。

為什麼我的整合測試這麼慢?

包含大量 @SpringBootTest 註釋測試的程式碼庫可能需要相當長的時間才能執行。Spring 的測試支援 足夠智慧,只建立一次應用上下文並在後續測試中重複使用,但是如果不同的測試需要不同的應用上下文,它仍然會為每個測試建立一個單獨的上下文,這需要一些時間來完成每個測試。
上面描述的所有自定義選項都會導致 Spring 建立一個新的應用程式上下文。因此,我們可能希望建立一個配置並將其用於所有測試,以便可以重用應用程式上下文。
如果您對測試花費在設定和 Spring 應用程式上下文上的時間感興趣,您可能需要檢視 JUnit Insights,它可以包含在 Gradle 或 Maven 構建中,以生成關於 JUnit 5 如何花費時間的很好的報告。

結論

@SpringBootTest 是一種為測試設定應用程式上下文的非常方便的方法,它非常接近我們將在生產中使用的上下文。有很多選項可以自定義此應用程式上下文,但應謹慎使用它們,因為我們希望我們的測試儘可能接近生產執行。
如果我們想在整個應用程式中進行測試,@SpringBootTest 會帶來最大的價值。為了僅測試應用程式的某些切片或層,我們還有其他選項可用。
本文中使用的示例程式碼可在 github 上找到。

相關文章