Spring Boot單元和整合測試概述 | rieckpil

banq發表於2021-03-17

單元和整合測試是您作為開發人員日常生活不可或缺的一部分。特別是對於Spring Boot而言,新手為他們的應用程式編寫有意義的測試是一個障礙:
  • 從哪裡開始我的測試工作?
  • Spring Boot如何幫助我編寫高效的測試?
  • 我應該使用哪些庫?

透過此部落格,您將獲得有關單元引導和整合測試如何與Spring Boot一起工作的概述。最重要的是,您將學習Spring首先要關注的功能和庫。本文充當聚合器,在許多地方,您都可以找到其他文章和指南的連結,這些文章和指南對這些概念進行了更詳細的說明。
 

使用Spring Boot進行單元測試
單元測試為您的測試策略奠定了基礎。您使用Spring Initializr引導的每個Spring Boot專案都具有編寫單元測試的堅實基礎。幾乎沒有什麼可設定的,因為Spring Boot Starter Test包含所有必要的構建基塊。
除了包含和管理Spring Test的版本外,此Spring Boot Starter包括並管理以下庫的版本:

  • JUnit 4/5
  • Mockito
  • 斷言庫,如AssertJ,Hamcrest,JsonPath等。

大多數時候,您的單元測試不需要任何特定的Spring Boot或Spring Test功能,因為它們僅依賴JUnit和Mockito。
使用單元測試,您可以單獨測試*Service類,例如模擬Mock要測試的類的每個協作者:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
 
import java.math.BigDecimal;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
 
@ExtendWith(MockitoExtension.class) // register the Mockito extension
public class PricingServiceTest {
 
  @Mock // // Instruct Mockito to mock this object
  private ProductVerifier mockedProductVerifier;
 
  @Test
  public void shouldReturnCheapPriceWhenProductIsInStockOfCompetitor() {
    when(mockedProductVerifier.isCurrentlyInStockOfCompetitor("AirPods"))
      .thenReturn(true); //Specify what boolean value to return
 
    PricingService cut = new PricingService(mockedProductVerifier);
 
    assertEquals(new BigDecimal("99.99"), cut.calculatePrice("AirPods"));
  }
}


從上面import的測試類的部分可以看到,Spring根本沒有import進來。因此,您可以應用對任何其他Java應用程式進行單元測試的技術和知識。
因此,重要的是要學習JUnit 4/5和Mockito的基礎知識,以充分利用您的單元測試。
對於應用程式的某些部分,單元測試不會帶來很多好處。持久層或測試HTTP客戶端就是一個很好的例子。測試應用程式的這些部分,最終將幾乎複製您的實現,因為您必須模擬與其他類的大量互動。
這裡更好的方法是使用切片的Spring Context,您可以使用Spring Boot測試註釋輕鬆地自動配置它。
 

使用切片的Spring上下文進行測試
在傳統的單元測試之上,您可以使用Spring Boot編寫針對應用程式特定部分(切片)的測試。SpringTestContext框架和Spring Boot一起將為Spring Context量身定製具有足夠用於特定測試的元件的Spring Context。
這些測試的目的是在不啟動整個應用程式的情況下單獨測試應用程式的特定部分。這樣既可以縮短測試執行時間,又可以減少對大量測試設定的需求。
如何命名此類測試?我認為,它們在單元測試或整合測試類別中均不會下降100%。一些開發人員將它們稱為單元測試,因為它們例如獨立測試一個控制器。其他開發人員將它們歸類為整合測試,因為涉及到Spring支援。無論您如何命名,至少在團隊中都要確保有一致的理解。
Spring Boot提供了大量的註解來測試您的應用程式的不同部分隔離:@JsonTest,@WebMvcTest,@DataMongoTest,@JdbcTest,等。
它們全部自動配置切片的Spring,TestContext並且僅包含與測試應用程式的特定部分相關的Spring Bean。我在整篇文章中都專門介紹了這些註釋中最常見的註釋,並解釋了它們的用法。
兩個最重要的註釋(考慮首先學習它們)是:


還有一些註釋可用於您應用程式的更多細分部分:

您始終可以透過以下方式顯式地匯入元件@Import或定義其他Spring Bean來豐富測試的自動配置上下文@TestConfiguration:

@WebMvcTest(PublicController.class)
class PublicControllerTest {
 
  @Autowired
  private MockMvc mockMvc;
 
  @Autowired
  private MeterRegistry meterRegistry;
 
  @MockBean
  private UserService userService;
 
  @TestConfiguration
  static class TestConfig {
 
    @Bean
    public MeterRegistry meterRegistry() {
      return new SimpleMeterRegistry();
    }
 
  } 
}

 

JUnit 4與JUnit 5陷阱
在回答Stack Overflow上的問題時,我經常遇到的一個大陷阱是同一測試中JUnit 4和JUnit 5(更具體地講,JUnit Jupiter)的混合。在同一測試類中使用不同JUnit版本的API會導致意外的輸出和失敗。
重要的是要注意匯入import,尤其是@Test註釋:

// JUnit 4
import org.junit.Test;
 
// JUnit Jupiter (part of JUnit 5)
import org.junit.jupiter.api.Test;

對於JUnit 4的其他指標有:@RunWith,@Rule,@ClassRule,@Before,@BeforeClass,@After,@AfterClass。
為了避免意外混用不同的JUnit版本,從專案中排除它們有助於始終選擇正確的匯入:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
    <exclusion>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
  </exclusions>
</dependency>

除了Spring Boot Starter Test之外,其他測試依賴項還可能包括舊版本的JUnit:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>${testcontainers.version}</version>
  <exclusions>
    <exclusion>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </exclusion>
  </exclusions>
</dependency>


為了避免將來包含任何(偶然的)JUnit 4依賴關係,可以使用Maven Enforcer外掛並將其定義為禁止的依賴關係。一旦有人包含一個新的測試依賴關係,該依賴關係會暫時拉動JUnit 4,這將使構建失敗。
請注意,從Spring Boot 2.4.0開始,Spring Boot Starter Test依賴項vintage-engine預設不再包含。
 

使用Spring Boot進行整合測試
使用整合測試,通常可以組合測試應用程式的多個元件。在大多數情況下,您將為此使用@SpringBootTest註釋,並使用或從外部訪問您的應用程式。
@SpringBootTest將為您的測試填充整個應用程式上下文。使用時,瞭解它的webEnvironment屬性很重要。如果不指定此屬性,則此類測試將不會啟動嵌入式Servlet容器(例如Tomcat),而是使用模擬的Servlet環境。因此,您的應用程式將無法在本地埠訪問。
您可以透過指定DEFINE_PORT或來覆蓋此行為RANDOM_PORT:

// or DEFINED_PORT
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)


對於啟動嵌入式Servlet容器的整合測試,您可以注入應用程式的埠並使用TestRestTemplateWebTestClient或從外部訪問它:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ApplicationTests {
 
  @LocalServerPort
  private Integer port;
 
  @Autowired
  private TestRestTemplate testRestTemplate;
 
  @Test
  void accessApplication() {
    System.out.println(port);
  }
}

由於SpringTestContext框架將填充整個應用程式上下文,因此您必須確儲存在所有依賴的基礎結構元件(例如,資料庫,訊息傳遞佇列等)。
這就是Testcontainer發揮作用的地方。測試容器將為您的測試管理任何Docker容器的生命週期:

@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationIT {
 
  @Container
  public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
    .withPassword("inmemory")
    .withUsername("inmemory");
 
  @DynamicPropertySource
  static void postgresqlProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
    registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
    registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
  }
 
  @Test
  public void contextLoads() {
  }
 
}

一旦您的應用程式與其他系統通訊,您就需要一個解決方案來模擬HTTP通訊。這是很常見的情況,例如在應用程式啟動時從遠端REST API或OAuth2訪問令牌中獲取資料。藉助WireMock,您可以存根並準備HTTP響應以模擬遠端系統的存在。
此外,SpringTestContext框架具有一項巧妙的功能,可以快取和重用以及已經啟動的上下文。這可以幫助減少構建時間並大大改善您的反饋週期。
 

使用Spring Boot進行端到端測試
端到端(E2E)測試的目的是從使用者的角度驗證系統。這包括針對主要使用者旅程的測試(例如,下訂單或建立新客戶)。與整合測試相比,此類測試通常涉及使用者介面(如果有的話)。
您還可以在繼續進行生產部署之前,針對dev或staging環境上的應用程式的已部署版本執行E2E測試。
對於使用伺服器端渲染(例如Thymeleaf)或自包含系統方法(由Spring Boot後端提供前端)的應用程式,您可以使用@SpringBootTest這些測試。
一旦需要與瀏覽器進行互動,Selenium通常是預設選擇。如果您已經與Selenium合作了一段時間,您可能會發現自己一遍又一遍地實現相同的幫助器功能。為了獲得更好的開發人員體驗並減少編寫涉及瀏覽器互動的測試時的頭痛,請考慮使用Selenide。Selenide是Selenium低階API之上的抽象,用於編寫穩定而簡潔的瀏覽器測試。
以下測試展示瞭如何使用Selenide訪問和測試Spring Boot應用程式的公共頁面:

@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class BookStoreTestcontainersWT {
 
  @LocalServerPort
  private Integer port;
 
  @Test
  public void shouldDisplayBook() {
 
    Configuration.timeout = 2000;
    Configuration.baseUrl = "http://localhost:" + port;
 
    open("/book-store");
 
    $(By.id("all-books")).shouldNot(Condition.exist);
    $(By.id("fetch-books")).click();
    $(By.id("all-books")).shouldBe(Condition.visible);
  }
}

對於您需要啟動E2E測試的基礎架構元件,Testcontainers再次扮演著重要的角色。如果您必須啟動多個Docker容器,則TestcontainersDocker Compose模組會派上用場:

public static DockerComposeContainer<?> environment =
  new DockerComposeContainer<>(new File("docker-compose.yml"))
    .withExposedService("database_1", 5432, Wait.forListeningPort())
    .withExposedService("keycloak_1", 8080, Wait.forHttp("/auth").forStatusCode(200)
      .withStartupTimeout(Duration.ofSeconds(30)))
    .withExposedService("sqs_1", 9324, Wait.forListeningPort());


概括
Spring Boot為單元測試和整合測試提供了出色的支援。由於每個Spring Boot專案都包括Spring Boot Starter Test,因此它使測試成為一等公民。該入門程式為您提供了具有基本測試庫的基本測試工具箱。
最重要的是,Spring Boot測試註釋使對應用程式不同部分的編寫測試變得輕而易舉。您將獲得TestContext僅包含相關Spring Bean的量身定製的Spring。
要熟悉Spring Boot專案的單元和整合測試,請考慮以下步驟:


相關文章