1. 概述
在本教程中,我們將探討使用 Spring Boot 框架編寫測試。我們將涵蓋可以獨立運行的單元測試,以及在執行測試之前啓動 Spring 容器的集成測試。
如果您是 Spring Boot 的新手,請查看我們的 Spring Boot 介紹。
2. 項目設置
本文檔中我們將使用的應用程序是一個 API,它提供對 Employee 資源的某些基本操作。這是一種典型的分層架構——API 調用從 Controller 到 Service 再到 Persistence 層進行處理。
3. Maven 依賴
讓我們首先添加我們的測試依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>spring-boot-starter-test 是主要依賴項,包含用於我們測試的大部分所需元素。
H2 DB 是我們的內存數據庫。它消除了為測試目的配置和啓動實際數據庫的需求。
4. 使用 @SpringBootTest 進行集成測試
正如其名稱所示,集成測試側重於應用程序的不同層之間的集成。 這也意味着沒有涉及任何模擬(mocking)。
理想情況下,我們應該將集成測試與單元測試分開,並且不應與單元測試一起運行。 我們可以通過使用不同的配置文件來僅運行集成測試來實現這一點。 這樣做的一些原因可能是集成測試耗時較長,並且可能需要實際的數據庫來執行。
然而,在本文中,我們將不關注這些內容,而是使用內存中的 H2 持久性存儲。
集成測試需要啓動一個容器來執行測試用例。 因此,需要進行一些額外的設置——所有這些都非常簡單,在 Spring Boot 中:
@ExtendWith(SpringExtension.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.MOCK,
classes = Application.class)
@AutoConfigureMockMvc
@TestPropertySource(
locations = "classpath:application-integrationtest.properties")
public class EmployeeRestControllerIntegrationTest {
@Autowired
private MockMvc mvc;
@Autowired
private EmployeeRepository repository;
// write test cases here
}使用@SpringBootTest 註解時,當我們需要啓動整個容器時非常有用。該註解通過創建 ApplicationContext 來實現,該 ApplicationContext 將在我們的測試中使用。
我們可以使用 @SpringBootTest 的 webEnvironment 屬性來配置我們的運行時環境;這裏我們使用 WebEnvironment.MOCK,以便容器在 mock servlet 環境中運行。
接下來,@TestPropertySource 註解有助於配置特定於測試的屬性文件的位置。請注意,使用 @TestPropertySource 加載的屬性文件將覆蓋現有的 application.properties 文件。
文件 application-integrationtest.properties 包含配置持久化存儲的詳細信息:
spring.datasource.url = jdbc:h2:mem:test
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect如果我們想在 MySQL 上運行我們的集成測試,可以更改上述 properties 文件中的值。
集成測試的用例可能與 Controller 層單元測試類似:
@Test
public void givenEmployees_whenGetEmployees_thenStatus200()
throws Exception {
createTestEmployee("bob");
mvc.perform(get("/api/employees")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content()
.contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].name", is("bob")));
}與 Controller 層單元測試的不同之處在於,這裏不會進行任何 Mock 操作,並且將執行端到端場景。
5. 使用測試配置與<em @TestConfiguration>
正如我們在上一部分所見,帶有<em @TestConfiguration>註解的測試將啓動完整的應用程序上下文,這意味着我們可以將通過組件掃描拾取的任何 Bean 自動注入到測試中:
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class EmployeeServiceImplIntegrationTest {
@Autowired
private EmployeeService employeeService;
// class code ...
}
然而,我們可能希望避免使用真實的應用程序上下文進行引導,而是使用特殊的測試配置。我們可以通過使用 @TestConfiguration 註解來實現這一點。使用該註解有兩... 種方式。
@ExtendWith(SpringExtension.class)
public class EmployeeServiceImplIntegrationTest {
@TestConfiguration
static class EmployeeServiceImplTestContextConfiguration {
@Bean
public EmployeeService employeeService() {
return new EmployeeService() {
// implement methods
};
}
}
@Autowired
private EmployeeService employeeService;
}或者,我們還可以創建一個單獨的測試配置類:
@TestConfiguration
public class EmployeeServiceImplTestContextConfiguration {
@Bean
public EmployeeService employeeService() {
return new EmployeeService() {
// implement methods
};
}
}帶有 @TestConfiguration 註解的配置類在組件掃描中將被排除在外。因此,在需要使用 @Autowire 注入它們的所有測試中,必須顯式導入它們。可以使用 @Import 註解來實現這一點:
@ExtendWith(SpringExtension.class)
@Import(EmployeeServiceImplTestContextConfiguration.class)
public class EmployeeServiceImplIntegrationTest {
@Autowired
private EmployeeService employeeService;
// remaining class code
}6. 使用 @MockBean 進行偽裝
我們的 Service 層代碼依賴於我們的 Repository:
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Override
public Employee getEmployeeByName(String name) {
return employeeRepository.findByName(name);
}
}然而,為了測試 Service 層,我們無需瞭解或關心持久化層是如何實現的。理想情況下,我們應該能夠在不連接完整的持久化層的情況下編寫和測試 Service 層的代碼。
要實現這一點,我們可以利用 Spring Boot Test 提供的 Mock 支持。
讓我們先看一下測試類骨架:
@ExtendWith(SpringExtension.class)
public class EmployeeServiceImplIntegrationTest {
@TestConfiguration
static class EmployeeServiceImplTestContextConfiguration {
@Bean
public EmployeeService employeeService() {
return new EmployeeServiceImpl();
}
}
@Autowired
private EmployeeService employeeService;
@MockBean
private EmployeeRepository employeeRepository;
// write test cases here
}要檢查 Service 類,我們需要創建一個 Service 類的實例,並將其作為 @Bean 註冊,以便在測試類中通過 @Autowire 注入。我們可以使用 @TestConfiguration 註解來實現這種配置。
此外,這裏還使用了 @MockBean。它會創建一個 EmployeeRepository 的 Mock,從而可以繞過對實際的 EmployeeRepository 的調用。
@BeforeEach
public void setUp() {
Employee alex = new Employee("alex");
Mockito.when(employeeRepository.findByName(alex.getName()))
.thenReturn(alex);
}配置完成後,測試用例變得更簡單了:
@Test
public void whenValidName_thenEmployeeShouldBeFound() {
String name = "alex";
Employee found = employeeService.getEmployeeByName(name);
assertThat(found.getName())
.isEqualTo(name);
}7. 使用 @DataJpaTest 進行集成測試
我們將使用一個名為 Employee 的實體,其屬性包括 id 和 name:
@Entity
@Table(name = "person")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Size(min = 3, max = 20)
private String name;
// standard getters and setters, constructors
}以下是我們使用 Spring Data JPA 的倉庫:
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
public Employee findByName(String name);
}以下是翻譯後的內容:
這就是持久層代碼的全部內容。現在我們來編寫測試類。
首先,讓我們創建測試類的基本框架:
@ExtendWith(SpringExtension.class)
@DataJpaTest
public class EmployeeRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private EmployeeRepository employeeRepository;
// write test cases here
}@ExtendWith(SpringExtension.class) 提供了一個橋樑,連接了 Spring Boot 測試功能和 JUnit。 在 JUnit 測試中使用任何 Spring Boot 測試功能時,此註解將是必需的。
@DataJpaTest 提供了一些標準設置,用於測試持久層:
- 配置 H2,一個內存數據庫
- 設置 Hibernate、Spring Data 和 DataSource
- 執行 @EntityScan
- 啓用 SQL 日誌
為了執行數據庫操作,我們需要數據庫中已經存在的一些記錄。為了設置這些數據,我們可以使用 TestEntityManager。
Spring Boot 的 TestEntityManager 是標準 JPA EntityManager 的替代方案,它提供了常用的用於編寫測試的方法。
EmployeeRepository 是我們將要測試的組件。
現在讓我們編寫第一個測試用例:
@Test
public void whenFindByName_thenReturnEmployee() {
// given
Employee alex = new Employee("alex");
entityManager.persist(alex);
entityManager.flush();
// when
Employee found = employeeRepository.findByName(alex.getName());
// then
assertThat(found.getName())
.isEqualTo(alex.getName());
}在上述測試中,我們使用 TestEntityManager 將 Employee 插入到數據庫中,並通過查找ByName API 讀取它。
“assertThat(…)” 部分來自 Assertj 庫,該庫與 Spring Boot 捆綁使用。
8. 使用 @WebMvcTest 進行單元測試
我們的 Controller 依賴於 Service 層;為了簡化,我們只包含一個方法:
@RestController
@RequestMapping("/api")
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/employees")
public List<Employee> getAllEmployees() {
return employeeService.getAllEmployees();
}
}由於我們僅關注 Controller 代碼,因此在單元測試中,我們可以對 Service 層代碼進行模擬:
@ExtendWith(SpringExtension.class)
@WebMvcTest(EmployeeRestController.class)
public class EmployeeRestControllerIntegrationTest {
@Autowired
private MockMvc mvc;
@MockBean
private EmployeeService service;
// write test cases here
}要測試 Controllers,我們可以使用 @WebMvcTest。它將自動配置 Spring MVC 基礎設施,用於我們的單元測試。
在大多數情況下,@WebMvcTest 僅限於為單個控制器提供啓動。 此外,我們可以將其與 @MockBean 結合使用,以提供任何必需依賴項的 Mock 實現。
@WebMvcTest 還自動配置了 MockMvc,這提供了一種簡單的方法,可以在不啓動完整 HTTP 服務器的情況下輕鬆測試 MVC 控制器。
説了這麼多,我們來編寫測試用例:
@Test
public void givenEmployees_whenGetEmployees_thenReturnJsonArray()
throws Exception {
Employee alex = new Employee("alex");
List<Employee> allEmployees = Arrays.asList(alex);
given(service.getAllEmployees()).willReturn(allEmployees);
mvc.perform(get("/api/employees")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].name", is(alex.getName())));
}我們可以用其他對應 HTTP 動詞的方法替換 get(…) 方法調用,例如 put(), post() 等。請注意,我們還設置了請求的內容類型。
MockMvc 具有靈活性,我們可以使用它創建任何請求。
9. 自動配置的測試
Spring Boot 的自動配置註解之一的功能是幫助加載應用程序的完整部分以及代碼庫的特定測試層。
除了上述註解之外,以下是一些常用的註解列表:
| Annotation | Description |
|---|---|
| @WebFluxTest | 用於測試 Spring WebFlux 控制器。通常與 @MockBean 結合使用,以提供所需依賴項的模擬實現。 |
| @JdbcTest | 用於測試僅需要 DataSource 的 JPA 應用程序。配置一個嵌入式內存數據庫和一個 JdbcTemplate。 |
| @JooqTest | 用於測試 jOOQ 相關組件。配置一個 DSLContext。 |
| @DataMongoTest | 用於測試 MongoDB 應用程序。配置一個嵌入式內存 MongoDB(如果驅動程序可用)、一個 MongoTemplate,掃描 @Document 類,並配置 Spring Data MongoDB 存儲庫。 |
| @DataRedisTest | 簡化對 Redis 應用程序的測試。默認掃描 @RedisHash 類,並配置 Spring Data Redis 存儲庫。 |
| @DataLdapTest | 配置一個嵌入式 LDAP(如果可用)、一個 LdapTemplate,默認掃描 @Entry 類,並配置 Spring Data LDAP 存儲庫。 |
| @RestClientTest | 用於測試 REST 客户端。自動配置諸如 Jackson、Gson 和 Jsonb 之類的依賴項;配置 RestTemplateBuilder;並默認添加對 MockRestServiceServer 的支持。 |
| @JsonTest | 僅初始化 Spring 應用程序上下文,以測試 JSON 序列化所需的 Bean。 |
我們可以在關於這些註解以及如何進一步優化 Spring 集成測試的文章中瞭解更多信息。
10. 結論
在本文中,我們深入探討了 Spring Boot 中的測試支持,並展示瞭如何高效地編寫單元測試。
如果您想繼續學習關於測試的知識,我們有與集成測試、優化 Spring 集成測試以及 JUnit 5 中的單元測試相關的單獨文章。