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() { 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()); User result = userService.create(request); assertNotNull(result); assertEquals("test", result.getUsername()); verify(userRepository).save(any(User.class)); verify(emailService).sendWelcomeEmail("test@test.com"); } @Test void shouldThrowWhenUserExists() { CreateUserRequest request = new CreateUserRequest("test", "test@test.com"); when(userRepository.existsByUsername("test")).thenReturn(true); 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) never() atLeast(n) atMost(n) verify(mock, times(2)).method();
any() anyString() anyLong() 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() { } }
|
测试原则
| 原则 |
说明 |
| FIRST |
Fast, Independent, Repeatable, Self-validating, Timely |
| 单一职责 |
每个测试只验证一个概念 |
| 独立 |
测试之间不依赖 |
| 可重复 |
任何环境都能运行 |
| 清晰命名 |
shouldXxxWhenYxx |
总结
| 测试类型 |
注解 |
适用场景 |
| 单元测试 |
@ExtendWith(MockitoExtension.class) |
Service 层纯逻辑 |
| Controller 测试 |
@WebMvcTest |
Controller 层 |
| Repository 测试 |
@DataJpaTest |
Repository 层 |
| 集成测试 |
@SpringBootTest |
全链路测试 |
| JSON 测试 |
@JsonTest |
序列化测试 |
好的测试是代码质量的保障,Spring Boot 提供了完善的测试支持,应该充分利用。
核心要点
日志级别设置:根据环境设置合适的级别
日志格式配置:添加 traceId 便于链路追踪
日志输出:控制台输出和文件输出的配置
日志归档:设置滚动策略和保留时间
总结
日志是排查问题的生命线,合理配置日志可以提升排查效率。在实际项目中,结合 ELK 等工具搭建日志系统,可以更好地管理和分析日志。