Spring Boot单元测试与MockMvc引言测试是保证软件质量的关键环节Spring Boot提供了强大的测试支持使得编写单元测试和集成测试变得更加便捷。MockMvc是Spring MVC测试的核心工具可以模拟HTTP请求并验证控制器行为无需启动真实的Web服务器。本文将详细介绍Spring Boot测试框架、MockMvc的使用方法以及最佳实践。一、测试依赖配置1.1 Maven依赖dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scopetest/scope /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-test/artifactId scopetest/scope /dependency /dependencies1.2 测试配置# test/resources/application-test.yml spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1 driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: create-drop show-sql: true cache: type: simple二、MockMvc基本用法2.1 WebMvcTest切片测试WebMvcTest(UserController.class) Import(TestSecurityConfig.class) class UserControllerTest { Autowired private MockMvc mockMvc; Autowired private ObjectMapper objectMapper; MockBean private UserService userService; Test WithMockUser(roles USER) void shouldGetUser() throws Exception { // Given User user createTestUser(); when(userService.findById(1L)).thenReturn(user); // When Then mockMvc.perform(get(/api/users/1) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath($.id).value(1)) .andExpect(jsonPath($.username).value(testuser)) .andExpect(jsonPath($.email).value(testexample.com)); } Test WithMockUser(roles USER) void shouldReturn404WhenUserNotFound() throws Exception { // Given when(userService.findById(999L)).thenReturn(null); // When Then mockMvc.perform(get(/api/users/999)) .andExpect(status().isNotFound()); } }2.2 POST请求测试Test WithMockUser(roles ADMIN) void shouldCreateUser() throws Exception { // Given CreateUserRequest request CreateUserRequest.builder() .username(newuser) .email(newexample.com) .password(Password123!) .build(); User createdUser createTestUser(); when(userService.createUser(any(CreateUserRequest.class))) .thenReturn(createdUser); // When Then mockMvc.perform(post(/api/users) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(header().exists(Location)) .andExpect(jsonPath($.id).exists()); }2.3 请求参数验证Test WithMockUser(roles ADMIN) void shouldReturn400WhenInvalidRequest() throws Exception { // Given - 缺少必需字段 MapString, Object invalidRequest new HashMap(); invalidRequest.put(username, ); // When Then mockMvc.perform(post(/api/users) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(invalidRequest))) .andExpect(status().isBadRequest()) .andExpect(jsonPath($.errors).isArray()); }三、测试结果验证3.1 JSON Path验证Test void shouldVerifyJsonResponse() throws Exception { mockMvc.perform(get(/api/users/1)) .andExpect(status().isOk()) .andExpect(jsonPath($.id).value(1)) .andExpect(jsonPath($.username).value(testuser)) .andExpect(jsonPath($.email).value(testexample.com)) .andExpect(jsonPath($.profile.age).value(25)) .andExpect(jsonPath($.roles[*]).isArray()) .andExpect(jsonPath($.roles, hasSize(2))) .andExpect(jsonPath($.roles, hasItem(ADMIN))) .andExpect(jsonPath($.profile.address.city).exists()); }3.2 多条件验证Test void shouldVerifyMultipleConditions() throws Exception { mockMvc.perform(get(/api/users)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(cookie().exists(sessionId)) .andExpect(cookie().httpOnly(sessionId, true)) .andExpect(header().string(X-Custom-Header, value)) .andExpect(model().attribute(users, hasSize(10))) .andExpect(view().name(userList)) .andExpect(forwardedUrl(/WEB-INF/views/userList.jsp)); }3.3 异常处理验证Test void shouldHandleValidationException() throws Exception { mockMvc.perform(post(/api/users) .contentType(MediaType.APPLICATION_JSON) .content({\username\:\\})) .andExpect(status().isBadRequest()) .andExpect(result - { Exception resolvedException result.getResolvedException(); assertTrue(resolvedException instanceof MethodArgumentNotValidException); }); }四、安全测试4.1 认证测试Test void shouldAllowAuthenticatedAccess() throws Exception { mockMvc.perform(get(/api/users/1) .with(user(testuser).roles(USER))) .andExpect(status().isOk()); } Test void shouldDenyAnonymousAccess() throws Exception { mockMvc.perform(get(/api/users/1)) .andExpect(status().isUnauthorized()); } Test void shouldDenyUnauthorizedRoleAccess() throws Exception { mockMvc.perform(delete(/api/users/1) .with(user(testuser).roles(USER))) // 需要ADMIN .andExpect(status().isForbidden()); }4.2 CSRF测试Test void shouldProtectAgainstCSRF() throws Exception { // Without CSRF token mockMvc.perform(post(/api/users) .contentType(MediaType.APPLICATION_JSON) .content({})) .andExpect(status().isForbidden()); // With CSRF token mockMvc.perform(post(/api/users) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content({})) .andExpect(status().isCreated()); }五、服务层测试5.1 Service单元测试ExtendWith(MockitoExtension.class) class UserServiceTest { Mock private UserRepository userRepository; Mock private PasswordEncoder passwordEncoder; InjectMocks private UserService userService; Test void shouldCreateUserSuccessfully() { // Given CreateUserRequest request CreateUserRequest.builder() .username(testuser) .email(testexample.com) .password(password) .build(); when(userRepository.existsByUsername(testuser)) .thenReturn(false); when(passwordEncoder.encode(password)) .thenReturn(encodedPassword); when(userRepository.save(any(User.class))) .thenAnswer(inv - { User user inv.getArgument(0); user.setId(1L); return user; }); // When User result userService.createUser(request); // Then assertThat(result.getId()).isEqualTo(1L); assertThat(result.getUsername()).isEqualTo(testuser); verify(userRepository).save(any(User.class)); } Test void shouldThrowExceptionWhenUsernameExists() { // Given when(userRepository.existsByUsername(existing)) .thenReturn(true); // When Then assertThatThrownBy(() - { CreateUserRequest request CreateUserRequest.builder() .username(existing) .build(); userService.createUser(request); }) .isInstanceOf(UsernameExistsException.class); } }5.2 Repository测试DataJpaTest AutoConfigureTestDatabase(replace AutoConfigureTestDatabase.Replace.NONE) class UserRepositoryTest { Autowired private UserRepository userRepository; Test void shouldFindByUsername() { // Given User user createAndSaveUser(testuser); // When OptionalUser found userRepository .findByUsername(testuser); // Then assertThat(found).isPresent(); assertThat(found.get().getUsername()) .isEqualTo(testuser); } Test void shouldFindByEmail() { // Given User user createAndSaveUser(test, testexample.com); // When OptionalUser found userRepository .findByEmail(testexample.com); // Then assertThat(found).isPresent(); } }六、集成测试6.1 SpringBootTestSpringBootTest AutoConfigureMockMvc ActiveProfiles(test) class UserIntegrationTest { Autowired private MockMvc mockMvc; Autowired private UserRepository userRepository; Autowired private ObjectMapper objectMapper; AfterEach void cleanup() { userRepository.deleteAll(); } Test void shouldCreateAndRetrieveUser() throws Exception { // Create CreateUserRequest request CreateUserRequest.builder() .username(integration) .email(integrationtest.com) .password(password) .build(); mockMvc.perform(post(/api/users) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()); // Retrieve mockMvc.perform(get(/api/users) .param(username, integration)) .andExpect(status().isOk()) .andExpect(jsonPath($.content[0].username) .value(integration)); } }6.2 数据库事务SpringBootTest Transactional class TransactionalIntegrationTest { Autowired private MockMvc mockMvc; Autowired private UserRepository userRepository; Test void shouldRollbackOnError() throws Exception { int countBefore userRepository.count(); mockMvc.perform(post(/api/users) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content({\username\:\\})) .andExpect(status().isBadRequest()); // 数据应回滚 int countAfter userRepository.count(); assertThat(countAfter).isEqualTo(countBefore); } }七、测试数据准备7.1 TestEntityManagerDataJpaTest class UserRepositoryWithEntityManagerTest { Autowired private TestEntityManager entityManager; Autowired private UserRepository userRepository; Test void shouldFindByEmailAfterPersist() { // Given User user new User(); user.setUsername(testuser); user.setEmail(testexample.com); entityManager.persist(user); entityManager.flush(); // When User found userRepository.findByEmail(testexample.com); // Then assertThat(found).isNotNull(); assertThat(found.getUsername()).isEqualTo(testuser); } }7.2 SQL脚本Test Sql({/sql/cleanup.sql, /sql/test-data.sql}) Sql(scripts /sql/cleanup.sql, executionPhase Sql.ExecutionPhase.AFTER_TEST_METHOD) void shouldWorkWithTestData() { // 使用测试数据 }八、最佳实践8.1 测试组织// 按功能分组 class UserService_CreateUser_Tests { // 创建用户的各种测试 } class UserService_UpdateUser_Tests { // 更新用户的各种测试 } // 使用内类组织 Nested class UserServiceTests { Nested class CreateUserTests { // ... } Nested class UpdateUserTests { // ... } }8.2 测试命名// 好的命名: 方法_场景_预期结果 Test void shouldReturnUser_WhenUserExists() { } Test void shouldThrowException_WhenUsernameAlreadyExists() { } Test void shouldReturnEmptyList_WhenNoUsersFound() { }总结Spring Boot提供了完整的测试支持通过MockMvc可以方便地测试Web层MockBean和InjectMocks简化了依赖MockDataJpaTest等切片测试提高了测试效率。良好的测试实践包括清晰的测试命名、完整的断言覆盖、适当的数据准备和清理。