Spring Boot test
1. Spring Boot testoverview
Spring Boot providing了全面 testsupport, including单元test, 集成test, API testetc. many 种testclass型.
1.1 testclass型
- 单元test - test单个component, such asserviceclass, toolclassetc.
- 集成test - test many 个component之间 交互, such asservice and datalibrary 集成
- API test - test REST API 端点
- 端 to 端test - test完整 业务流程
1.2 testtool栈
- JUnit 5 - testframework
- Mockito - mockframework
- Spring Boot Test - Spring Boot testsupport
- Testcontainers - containerizationtest
- RestAssured - REST API test
2. 单元testBasics
2.1 添加依赖
<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>
2.2 writing simple 单元test
package com.example.demo.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorServiceTest {
private CalculatorService calculatorService = new CalculatorService();
@Test
void testAdd() {
int result = calculatorService.add(2, 3);
assertEquals(5, result, "2 + 3 should equal 5");
}
@Test
void testSubtract() {
int result = calculatorService.subtract(5, 3);
assertEquals(2, result, "5 - 3 should equal 2");
}
@Test
void testMultiply() {
int result = calculatorService.multiply(2, 3);
assertEquals(6, result, "2 * 3 should equal 6");
}
@Test
void testDivide() {
int result = calculatorService.divide(6, 3);
assertEquals(2, result, "6 / 3 should equal 2");
}
@Test
void testDivideByZero() {
assertThrows(ArithmeticException.class, () -> {
calculatorService.divide(6, 0);
}, "Division by zero should throw ArithmeticException");
}
}
2.3 using Mockito formocktest
package com.example.demo.service;
import com.example.demo.repository.UserRepository;
import com.example.demo.entity.User;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void testGetUserById() {
// 准备testdata
User mockUser = new User();
mockUser.setId(1L);
mockUser.setUsername("admin");
mockUser.setEmail("admin@example.com");
// configurationmockbehavior
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// 执行test
User result = userService.getUserById(1L);
// verification结果
assertNotNull(result);
assertEquals(1L, result.getId());
assertEquals("admin", result.getUsername());
// verificationmethod调用
verify(userRepository, times(1)).findById(1L);
}
@Test
void testSaveUser() {
// 准备testdata
User userToSave = new User();
userToSave.setUsername("newuser");
userToSave.setEmail("newuser@example.com");
User savedUser = new User();
savedUser.setId(1L);
savedUser.setUsername("newuser");
savedUser.setEmail("newuser@example.com");
// configurationmockbehavior
when(userRepository.save(userToSave)).thenReturn(savedUser);
// 执行test
User result = userService.saveUser(userToSave);
// verification结果
assertNotNull(result);
assertEquals(1L, result.getId());
// verificationmethod调用
verify(userRepository, times(1)).save(userToSave);
}
@Test
void testDeleteUser() {
// 执行test
userService.deleteUser(1L);
// verificationmethod调用
verify(userRepository, times(1)).deleteById(1L);
}
}
3. Spring Boot 集成test
集成test用于test many 个component之间 交互, 通常需要启动 Spring on under 文.
3.1 basic集成test
package com.example.demo;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserRepositoryIntegrationTest {
@Autowired
private UserRepository userRepository;
@Test
void testSaveAndFindUser() {
// creationuser
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setPassword("password");
// 保存user
User savedUser = userRepository.save(user);
// verification保存成功
assertNotNull(savedUser.getId());
assertEquals("testuser", savedUser.getUsername());
// finduser
User foundUser = userRepository.findById(savedUser.getId()).orElse(null);
// verificationfind结果
assertNotNull(foundUser);
assertEquals(savedUser.getId(), foundUser.getId());
assertEquals("testuser", foundUser.getUsername());
}
@Test
void testFindByUsername() {
// creationuser
User user = new User();
user.setUsername("uniqueuser");
user.setEmail("unique@example.com");
user.setPassword("password");
userRepository.save(user);
// 按user名find
User foundUser = userRepository.findByUsername("uniqueuser");
// verification结果
assertNotNull(foundUser);
assertEquals("uniqueuser", foundUser.getUsername());
}
}
3.2 using @DataJpaTest test JPA component
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.jdbc.AutoconfigurationTestDatabase;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
@AutoconfigurationTestDatabase(replace = AutoconfigurationTestDatabase.Replace.NONE)
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntitymanagementr entitymanagementr;
@Test
void testFindByEmail() {
// using TestEntitymanagementr 插入testdata
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setPassword("password");
entitymanagementr.persist(user);
entitymanagementr.flush();
// 执行query
User foundUser = userRepository.findByEmail("test@example.com");
// verification结果
assertNotNull(foundUser);
assertEquals("testuser", foundUser.getUsername());
}
@Test
void testExistsByUsername() {
// 插入testdata
User user = new User();
user.setUsername("existinguser");
user.setEmail("existing@example.com");
user.setPassword("password");
entitymanagementr.persist(user);
entitymanagementr.flush();
// test存 in user名
boolean exists = userRepository.existsByUsername("existinguser");
assertTrue(exists);
// test不存 in user名
boolean notExists = userRepository.existsByUsername("nonexistentuser");
assertFalse(notExists);
}
}
4. REST API test
Spring Boot providing了 many 种方式来test REST API.
4.1 using MockMvc test API
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestbuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void testGetUserById() throws Exception {
// 准备testdata
User mockUser = new User();
mockUser.setId(1L);
mockUser.setUsername("admin");
mockUser.setEmail("admin@example.com");
// configurationmockbehavior
when(userService.getUserById(1L)).thenReturn(mockUser);
// 执行test
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").value("admin"))
.andExpect(jsonPath("$.email").value("admin@example.com"));
}
@Test
void testCreateUser() throws Exception {
// 准备testdata
User userToCreate = new User();
userToCreate.setUsername("newuser");
userToCreate.setEmail("new@example.com");
userToCreate.setPassword("password");
User createdUser = new User();
createdUser.setId(1L);
createdUser.setUsername("newuser");
createdUser.setEmail("new@example.com");
// configurationmockbehavior
when(userService.saveUser(any(User.class))).thenReturn(createdUser);
// 执行test
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userToCreate)))
.andExpect(status().isCreated())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").value("newuser"));
}
@Test
void testUpdateUser() throws Exception {
// 准备testdata
User updatedUser = new User();
updatedUser.setId(1L);
updatedUser.setUsername("updateduser");
updatedUser.setEmail("updated@example.com");
// configurationmockbehavior
when(userService.updateUser(eq(1L), any(User.class))).thenReturn(updatedUser);
// 执行test
mockMvc.perform(put("/api/users/1")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updatedUser)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("updateduser"));
}
@Test
void testDeleteUser() throws Exception {
// 执行test
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isNoContent());
// verificationmethod调用
verify(userService, times(1)).deleteUser(1L);
}
}
4.2 using TestRestTemplate test API
package com.example.demo;
import com.example.demo.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
private String getBaseUrl() {
return "http://localhost:" + port + "/api/users";
}
@Test
void testGetAllUsers() {
ResponseEntity<User[]> response = restTemplate.getForEntity(getBaseUrl(), User[].class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
@Test
void testCreateAndGetUser() {
// creationuser
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setPassword("password");
ResponseEntity<User> createResponse = restTemplate.postForEntity(getBaseUrl(), user, User.class);
assertEquals(HttpStatus.CREATED, createResponse.getStatusCode());
User createdUser = createResponse.getBody();
assertNotNull(createdUser);
assertNotNull(createdUser.getId());
// 获取user
ResponseEntity<User> getResponse = restTemplate.getForEntity(getBaseUrl() + "/" + createdUser.getId(), User.class);
assertEquals(HttpStatus.OK, getResponse.getStatusCode());
User retrievedUser = getResponse.getBody();
assertNotNull(retrievedUser);
assertEquals(createdUser.getId(), retrievedUser.getId());
assertEquals(createdUser.getUsername(), retrievedUser.getUsername());
}
@Test
void testUpdateUser() {
// 先creationuser
User user = new User();
user.setUsername("updatableuser");
user.setEmail("update@example.com");
user.setPassword("password");
User createdUser = restTemplate.postForEntity(getBaseUrl(), user, User.class).getBody();
assertNotNull(createdUser);
// updateuser
createdUser.setUsername("updateduser");
createdUser.setEmail("updated@example.com");
HttpHeaders headers = new HttpHeaders();
HttpEntity<User> requestEntity = new HttpEntity<>(createdUser, headers);
ResponseEntity<User> updateResponse = restTemplate.exchange(
getBaseUrl() + "/" + createdUser.getId(),
HttpMethod.PUT,
requestEntity,
User.class
);
assertEquals(HttpStatus.OK, updateResponse.getStatusCode());
User updatedUser = updateResponse.getBody();
assertNotNull(updatedUser);
assertEquals("updateduser", updatedUser.getUsername());
}
}
5. testbest practices
5.1 test命名规范
- testclass名: 被testclass名 + Test
- testmethod名: test + 被testmethod名 + test场景
- usingdescribes性 method名, such as testSaveUserWithValidData()
5.2 teststructure
@Test
void testMethodName() {
// Arrange - 准备testdata and environment
// Act - 执行被testmethod
// Assert - verification结果
// Verify - verification交互 (such as果using了mock)
}
5.3 test覆盖率
using JaCoCo 生成testcoverage report:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
runtest并生成报告:
mvn test
5.4 parameter化test
package com.example.demo.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorServiceTest {
private CalculatorService calculatorService = new CalculatorService();
@ParameterizedTest
@CsvSource({
"2, 3, 5",
"-1, 1, 0",
"0, 0, 0",
"100, 200, 300"
})
void testAdd(int a, int b, int expected) {
int result = calculatorService.add(a, b);
assertEquals(expected, result, a + " + " + b + " should equal " + expected);
}
@ParameterizedTest
@CsvSource({
"5, 3, 2",
"0, 0, 0",
"10, 5, 5",
"-5, -3, -2"
})
void testSubtract(int a, int b, int expected) {
int result = calculatorService.subtract(a, b);
assertEquals(expected, result, a + " - " + b + " should equal " + expected);
}
}
6. advancedtesttechniques
6.1 using Testcontainers forcontainerizationtest
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
package com.example.demo;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.junit.jupiter.api.Assertions.*;
@Testcontainers
@SpringBootTest
class UserRepositoryContainerTest {
@Container
@ServiceConnection
static MySQLContainer<?> mysqlContainer = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpassword");
@Autowired
private UserRepository userRepository;
@Test
void testSaveAndFindUser() {
// creationuser
User user = new User();
user.setUsername("containertest");
user.setEmail("container@example.com");
user.setPassword("password");
// 保存user
User savedUser = userRepository.save(user);
// verification保存成功
assertNotNull(savedUser.getId());
// finduser
User foundUser = userRepository.findById(savedUser.getId()).orElse(null);
// verificationfind结果
assertNotNull(foundUser);
assertEquals(savedUser.getUsername(), foundUser.getUsername());
}
}
6.2 testexceptionprocessing
package com.example.demo.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceExceptionTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void testGetUserByIdNotFound() {
// configurationmockbehavior
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// 执行test并verificationexception
UserNotFoundException exception = assertThrows(UserNotFoundException.class, () -> {
userService.getUserById(999L);
});
assertEquals("User not found with id: 999", exception.getMessage());
}
@Test
void testSaveUserWithExistingUsername() {
// 准备testdata
User user = new User();
user.setUsername("existinguser");
user.setEmail("new@example.com");
// configurationmockbehavior
when(userRepository.existsByUsername("existinguser")).thenReturn(true);
// 执行test并verificationexception
UserAlreadyExistsException exception = assertThrows(UserAlreadyExistsException.class, () -> {
userService.saveUser(user);
});
assertEquals("Username 'existinguser' already exists", exception.getMessage());
}
}
6.3 testasynchronousmethod
package com.example.demo.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.concurrent.ListenableFuture;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class AsyncServiceTest {
@Autowired
private AsyncService asyncService;
@Test
void testAsyncMethod() throws ExecutionException, InterruptedException {
// 执行asynchronousmethod
ListenableFuture<String> future = asyncService.performAsyncTask("test");
// 获取结果 (会阻塞直 to completion)
String result = future.get();
// verification结果
assertEquals("completed: test", result);
}
@Test
void testCompletableFuture() throws Exception {
// 执行asynchronousmethod
CompletableFuture<String> future = asyncService.performCompletableFutureTask("test");
// using CompletableFuture API
String result = future.thenApply(s -> s.toUpperCase())
.get();
// verification结果
assertEquals("COMPLETED: TEST", result);
}
}
7. runtest
7.1 using Maven runtest
# run所 has test
mvn test
# runspecifictestclass
mvn test -Dtest=UserServiceTest
# runspecifictestmethod
mvn test -Dtest=UserServiceTest#testGetUserById
# runtest并生成coverage report
mvn test jacoco:report
# 跳过test
mvn package -DskipTests
# 只编译test, 不run
mvn test-compile
7.2 using Gradle runtest
# run所 has test
gradle test
# runspecifictestclass
gradle test --tests UserServiceTest
# runspecifictestmethod
gradle test --tests UserServiceTest.testGetUserById
# 生成coverage report
gradle jacocoTestReport
# 跳过test
gradle build -x test
7.3 in IDE inruntest
- IntelliJ IDEA - right 键点击testclass or method, 选择 "Run"
- Eclipse - right 键点击testclass or method, 选择 "Run As" > "JUnit Test"
- VS Code - installation Java Test Runner scale, 点击test旁edge run按钮
8. testbest practicessummarized
- writing清晰, 简洁 test用例
- test应该 is 独立 , 不依赖于othertest
- usingdescribes性 testmethod名
- 覆盖正面 and 负面场景
- usingmockobject隔离被testcomponent
- 保持testcode readable 性
- 定期runtest
- usingcontinuous integrationtool自动runtest
- 追求合理 test覆盖率, 而不 is 100%
- test应该 fast 速run