原文地址:https://www.baeldung.com/spring-boot-testing
1 概覽
在這個教程中,我們會帶你看看如果使用 Spring Boot 中的框架編寫測試用例。內容會覆蓋單元測試,也會有在執行測試用例前會啓動 Spring 上下文的集成測試。如果你是使用 Spring Boot 的新手,查看鏈接:Spring Boot 介紹。
擴展閱讀:探索 Spring Boot TestRestTemplate、Spring Boot @RestClientTest快速導航、在Spring Beans中注入 Mockito Mocks
2 項目啓動
我們要使用的應用程序是一個api,這個api會提供一些關於Employee表的基本操作(增刪改查)。這是一個典型的分層框架——API調用從controller層到service層,最後到持久層。
3 Maven 依賴
首先增加測試依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-test
包是包含測試所需要的大部分元素的主要依賴項。H2
數據庫是一個內存數據庫。它不需要我們配置和啓動一個真正的數據庫,因此在測試場景下方便了開發人員。
3.1 JUnit4
Spring Boot 2.4 中,JUnit 5’s vintage engine 包已經從spring-boot-starter-test
中被移除了。如果我們想用 JUnit4
寫測試用例,我們需要添加下述依賴項。
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
4 通過 @SpringBootTest 進行集成測試
就像標題所說,集成測試焦點是整合應用程序的不同層(controller層、service層以及持久層)。這也意味着沒有 mocking 參與其中。
理想情況下,我們應該把單元測試和集成測試分開,並且不應該和單元測試一起運行。我們可以通過使用不同的配置文件來實現這個分離。爲什麼要這麼做呢?因爲一般集成測試比較消耗時間並且有可能需要真正的數據庫(不是內存數據庫)來執行。
然而在本文中,我們不關注這個,我們關注的是,使用內存數據庫H2持久化存儲。
集成測試需要啓動一個容器來執行測試用例。因此需要一些額外的設置——這些在 Spring Boot 中都很容易。
@RunWith(SpringRunner.class)
@SpringBootTest(
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)。
我們可以@SpringBootTest
註解的webEnvironment
屬性來配置運行時環境;我們可以在這裏使用WebEnvironment.MOCK
,這樣整個容器會以模擬servlet 環境來運行。
然後,@TestPropertySource
註解幫助我們配置在測試用例中使用的配置文件地址。需要注意的是,這個註解配置的配置文件會覆蓋存在的application.properties
配置文件。
application-integrationtest.properties
該配置文件包含持久層存儲的配置細節:
spring.datasource.url = jdbc:h2:mem:test
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect
如果我們想使用MySQL來進行集成測試,我們可以修改上述配置文件(application-integrationtest.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層測試用例中,沒有東西是模擬的,並且是執行端到端場景。
5 通過@TestConfiguration
進行測試配置
在前文中我們看到,增加了註解@SpringBootTest
的類會啓動整個應用上下文,這也意味着我們可以通過@Autowire
注入任何通過component
掃描的類到我們的測試類中:
@RunWith(SpringRunner.class)
@SpringBootTest
public class EmployeeServiceImplIntegrationTest {
@Autowired
private EmployeeService employeeService;
// class code ...
}
然而,我們也許想要避免啓動整個應用程序,而只是啓動一個特殊的測試配置。我們可以通過@TestConfiguration
註解實現它。使用這個註解的方式有兩種。一種方式是,我們可以在內部類的地方使用該註解來注入我們想要通過@Autowire
注入的類。
@RunWith(SpringRunner.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
註解的配置類會被componet
掃描排除在外,因此我們需要在所有我們想要使用@Autowired
的測試類中清晰的導入該類。我們可以通過@Import
註解來實現:
@RunWith(SpringRunner.class)
@Import(EmployeeServiceImplTestContextConfiguration.class)
public class EmployeeServiceImplIntegrationTest {
@Autowired
private EmployeeService employeeService;
// remaining class code
}
6 通過 @MockBean 模擬
Service 層代碼是依賴於持久層代碼的:
@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 提供的 Mocking 支持來做到==。
讓我們瞟一眼測試類的框架先:
@RunWith(SpringRunner.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類,我們需要有個一已經創建好並且可以通過 @Bean
可獲得的Service類實例,這樣我們纔可以通過@Autowired
在測試類中注入該Service類。我們可以通過@TestConfiguration
註解來實現。
這裏另一個有趣的事情是使用@MockBean
。它會創建一個EmployeeRepository
模擬類,它可以被用來替換真正的EmployeeRepository
.
@Before
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);
}
這是持久層代碼。現在讓我們繼續往下編寫測試代碼。首先,我們創建測試類的基本框架:
@RunWith(SpringRunner.class)
@DataJpaTest
public class EmployeeRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private EmployeeRepository employeeRepository;
// write test cases here
}
@RunWith(SpringRunner.class)
註解提供一個Spring Boot Test 特性和JUnit中間的一個橋樑。當我們需要在JUnit測試類中使用Spring Boot 測試的特性的時候,這個註解就有用了。
@DataJpaTest
註解提供了持久層測試類的一些標準設置:
-
配置H2數據庫,一個內存數據庫 -
設置Hibernate,SPring Data,和DataSource -
執行@EntityScan -
打開SQL日誌記錄
爲了繼續數據庫操作,我們需要在數據庫中添加一些記錄。爲了設置這些數據,我們可以使用TestEntityManager
。
Spring Boot TestEntityManager
是標準JPA EntityManager
的替代方案,標準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
記錄,然後就通過命名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層:
@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeRestController.class)
public class EmployeeRestControllerIntegrationTest {
@Autowired
private MockMvc mvc;
@MockBean
private EmployeeService service;
// write test cases here
}
要測試Controller層,我們可以使用在大部分情況下,@WebMvcTest
只會啓動單個Controller類。我們可以和@MockBean
註解一起使用來提供任何需要依賴的模擬實現。。它將爲我們的單元測試自動配置Spring MVC基礎結構。
在大部分情況下,@WebMvcTest
只會啓動單個Controller類。我們可以和@MockBean
註解一起使用來提供任何需要依賴的模擬實現。
@WebMvcTest
會自動配置MockMvc
,它提供了一種強力的方式來簡化測試MVC controller層的方式,而不需要啓動一個完整的 HTTP 服務器。
測試類如下:
@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())));
}
get()方法調用可以被其他與HTTP相對應的方法替換,如put()、 post()等。請注意,我們還在請求中設置內容類型。MockMvc是很靈活的,我們可以用它創建任何請求。
9 自動配置測試
Spring Boot的自動配置註釋的一個驚人特性是,它有助於加載完整應用程序的某些部分和代碼庫的特定測試層。
除了上述提供的註解,這裏還有一些被廣泛使用的註解列表:
-
@WebFluxTest:我們可以使用 @WebFluxTest
註解來測試Spring WebFlux控制器。它經常與@MockBean
一起使用,爲所需的依賴項提供模擬實現。 -
@JdbcTest:我們可以使用 @JdbcTest
註釋來測試JPA應用程序,但它只用於只需要數據源的測試。該註釋配置一個內存內嵌入式數據庫和一個JdbcTemplate
。 -
@JooqTest -
@DataMongoTest
...
你可以讀到關於這些註解的更多文章,並繼續優化集成測試,優化Spring集成測試。
10 結論
在本文中,我們深入探討了在Spring Boot中進行測試,並展示了怎麼更有效的編寫測試用例。
所有本文的源碼都可以在這裏找到,github。源碼包含很多其他示例和不同的測試用例。
其他閱讀:【Guide to Testing With the Spring Boot Starter Test】https://rieckpil.de/guide-to-testing-with-spring-boot-starter-test/
本文分享自微信公衆號 - 哥妞(gh_d18ec82f19ea)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。