SpringBoot测试策略与Mock

SpringBoot测试策略与Mock

Spring Boot 简化了配置,但日志管理依然需要重视。日志配置、链路追踪、排查思路都是日常开发中会遇到的问题。本文讲实际项目中的日志管理经验。

测试依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

单元测试

#

纯单元测试(JUnit 5 + Mockito)

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

@Mock
private UserRepository userRepository;

@Mock
private EmailService emailService;

@InjectMocks
private UserService userService;

@Test
void shouldCreateUser() {
// Given
CreateUserRequest request = new CreateUserRequest("test", "test@test.com");
User user = new User(1L, "test", "test@test.com");

when(userRepository.save(any(User.class))).thenReturn(user);
doNothing().when(emailService).sendWelcomeEmail(anyString());

// When
User result = userService.create(request);

// Then
assertNotNull(result);
assertEquals("test", result.getUsername());
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("test@test.com");
}

@Test
void shouldThrowWhenUserExists() {
// Given
CreateUserRequest request = new CreateUserRequest("test", "test@test.com");
when(userRepository.existsByUsername("test")).thenReturn(true);

// When & Then
assertThrows(BusinessException.class, () -> {
userService.create(request);
});

verify(userRepository, never()).save(any());
}
}

#

常用 Mockito 方法

// 设置返回值
when(mock.method()).thenReturn(value);
when(mock.method()).thenThrow(exception);
when(mock.method()).thenAnswer(invocation -> { ... });

// 无返回值方法
doNothing().when(mock).voidMethod();
doThrow(exception).when(mock).voidMethod();

// 验证
times(n) // 调用n次
never() // 从不调用
atLeast(n) // 至少n次
atMost(n) // 最多n次
verify(mock, times(2)).method();

// 参数匹配
any() // 任何类型
anyString() // 任何字符串
anyLong() // 任何Long
any(Class.class)// 任何指定类型
eq("value") // 等于
argThat(arg -> arg.length() > 5) // 自定义匹配

// 连续返回值
when(mock.method())
.thenReturn("first")
.thenReturn("second")
.thenThrow(new RuntimeException());

集成测试

#

@SpringBootTest

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private UserRepository userRepository;

@BeforeEach
void setUp() {
userRepository.deleteAll();
}

@Test
void shouldCreateUser() throws Exception {
CreateUserRequest request = new CreateUserRequest("test", "test@test.com");

mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").isNumber());

assertEquals(1, userRepository.count());
}

@Test
void shouldGetUser() throws Exception {
User user = userRepository.save(new User("test", "test@test.com"));

mockMvc.perform(get("/users/{id}", user.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.username").value("test"));
}

@Test
void shouldReturn404WhenNotFound() throws Exception {
mockMvc.perform(get("/users/999"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(20001));
}
}

#

@MockBean

@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private PaymentService paymentService;

@Autowired
private OrderRepository orderRepository;

@Test
void shouldCreateOrder() throws Exception {
when(paymentService.charge(any())).thenReturn(true);

mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"amount\":100}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));

verify(paymentService).charge(any());
}
}

切片测试

#

@WebMvcTest(Controller 层)

@WebMvcTest(UserController.class)
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void shouldGetUser() throws Exception {
User user = new User(1L, "test", "test@test.com");
when(userService.getUser(1L)).thenReturn(user);

mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.username").value("test"));
}
}

#

@DataJpaTest(Repository 层)

@DataJpaTest
class UserRepositoryTest {

@Autowired
private TestEntityManager entityManager;

@Autowired
private UserRepository userRepository;

@Test
void shouldFindByUsername() {
User user = new User("test", "test@test.com");
entityManager.persist(user);

Optional<User> found = userRepository.findByUsername("test");

assertTrue(found.isPresent());
assertEquals("test@test.com", found.get().getEmail());
}

@Test
void shouldExistsByUsername() {
entityManager.persist(new User("test", "test@test.com"));

assertTrue(userRepository.existsByUsername("test"));
assertFalse(userRepository.existsByUsername("other"));
}
}

#

@JdbcTest(JDBC 层)

@JdbcTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class JdbcRepositoryTest {

@Autowired
private JdbcTemplate jdbcTemplate;

@Test
void shouldQueryData() {
jdbcTemplate.update("INSERT INTO users (username) VALUES (?)", "test");

String username = jdbcTemplate.queryForObject(
"SELECT username FROM users WHERE id = 1", String.class);

assertEquals("test", username);
}
}

#

@JsonTest(JSON 序列化)

@JsonTest
class UserJsonTest {

@Autowired
private JacksonTester<User> json;

@Test
void shouldSerialize() throws Exception {
User user = new User(1L, "test", "test@test.com");

assertThat(json.write(user)).isStrictlyEqualToJson("expected.json");
assertThat(json.write(user)).hasJsonPathStringValue("@.username", "test");
}

@Test
void shouldDeserialize() throws Exception {
String content = "{\"id\":1,\"username\":\"test\",\"email\":\"test@test.com\"}";

assertThat(json.parse(content)).isEqualTo(new User(1L, "test", "test@test.com"));
}
}

测试配置

#

application-test.yml

spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop

logging:
level:
root: WARN

#

@ActiveProfiles

@SpringBootTest
@ActiveProfiles("test")
class IntegrationTest {
}

#

@TestConfiguration

@TestConfiguration
public class TestConfig {

@Bean
@Primary
public PaymentService mockPaymentService() {
PaymentService mock = Mockito.mock(PaymentService.class);
when(mock.charge(any())).thenReturn(true);
return mock;
}
}

@SpringBootTest
class OrderServiceTest {

@Import(TestConfig.class)
static class Config {}
}

测试数据库

#

H2 内存数据库

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

#

TestContainers

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
@Testcontainers
@SpringBootTest
class MySQLIntegrationTest {

@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test")
.withUsername("test")
.withPassword("test");

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}

@Test
void shouldWorkWithMySQL() {
// 使用真实 MySQL 测试
}
}

测试原则

原则 说明
FIRST Fast, Independent, Repeatable, Self-validating, Timely
单一职责 每个测试只验证一个概念
独立 测试之间不依赖
可重复 任何环境都能运行
清晰命名 shouldXxxWhenYxx

总结

测试类型 注解 适用场景
单元测试 @ExtendWith(MockitoExtension.class) Service 层纯逻辑
Controller 测试 @WebMvcTest Controller 层
Repository 测试 @DataJpaTest Repository 层
集成测试 @SpringBootTest 全链路测试
JSON 测试 @JsonTest 序列化测试

好的测试是代码质量的保障,Spring Boot 提供了完善的测试支持,应该充分利用。

核心要点

  1. 日志级别设置:根据环境设置合适的级别

  2. 日志格式配置:添加 traceId 便于链路追踪

  3. 日志输出:控制台输出和文件输出的配置

  4. 日志归档:设置滚动策略和保留时间

总结

日志是排查问题的生命线,合理配置日志可以提升排查效率。在实际项目中,结合 ELK 等工具搭建日志系统,可以更好地管理和分析日志。


   转载规则


《SpringBoot测试策略与Mock》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录