Spring Boot 单元测试完全指南

0 阅读3分钟

Spring Boot 单元测试完全指南

Spring Boot 提供了完整的测试支持,核心是 spring-boot-starter-test,它整合了 JUnit、Mockito、AssertJ、Hamcrest、JsonPath 等主流测试库。

一、依赖引入

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

这个依赖默认提供了以下测试框架:

  • JUnit 5:测试运行引擎
  • Mockito:Mock 框架
  • AssertJ:流式断言
  • Hamcrest:匹配器
  • JsonPath:JSON 断言
  • Spring Test:Spring 测试支持

二、测试注解

@SpringBootTest

加载完整的 Spring 应用上下文,用于集成测试。默认情况下,它会查找 @SpringBootApplication 启动类并加载所有配置。

@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;

    @Test
    public void testGetUser() {
        User user = userService.getById(1L);
        assertThat(user).isNotNull();
    }
}

@SpringBootTest 支持通过 webEnvironment 属性控制 Web 环境:

webEnvironment 值说明
MOCK默认值,加载模拟的 Servlet 环境,不启动真实 Web 服务器
RANDOM_PORT启动内嵌 Web 服务器,使用随机端口
DEFINED_PORT启动内嵌 Web 服务器,使用配置的端口
NONE不加载 Web 环境,适用于非 Web 测试

@Test

JUnit 5 的测试方法注解,标记一个方法为测试用例。

@Test
public void testAddUser() {
    // 测试逻辑
}

三、Web 层切片测试

@WebMvcTest

只加载 Web 层组件(Controller、HandlerInterceptor、WebMvcConfigurer),不加载 Service、Mapper 等业务组件。测试 Controller 层性能更好,启动更快。

@WebMvcTest(UserController.class)
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    public void testGetUser() throws Exception {
        User mockUser = new User(1L, "张三");
        when(userService.getById(1L)).thenReturn(mockUser);

        mockMvc.perform(get("/user/1"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.name").value("张三"));
    }
}

@WebMvcTest 只加载指定的 Controller,Service 层需要配合 @MockBean 模拟依赖。如果 Controller 依赖其他组件(如拦截器、参数解析器),需要额外配置或使用 @Import 导入。

MockMvc

MockMvc 是 Spring 提供的模拟 HTTP 请求的工具,无需启动真实 Web 服务器就能测试 Controller 的行为。

常用方法:

mockMvc.perform(get("/user/1"))
       .andExpect(status().isOk())
       .andExpect(jsonPath("$.name").value("张三"))
       .andExpect(jsonPath("$.age").value(18))
       .andDo(print());

四、数据访问层切片测试

@DataJpaTest

只加载 JPA 相关组件(Repository、EntityManager、DataSource),默认使用嵌入式内存数据库(H2 或 Derby),测试数据不会污染真实数据库。

@DataJpaTest
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    public void testSaveUser() {
        User user = new User("张三");
        User saved = userRepository.save(user);
        assertThat(saved.getId()).isNotNull();
    }
}

@MybatisTest

MyBatis 生态的切片测试注解,只加载 MyBatis 相关组件(Mapper、SqlSessionFactory)。同样默认使用内存数据库。

@MybatisTest
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelectById() {
        User user = userMapper.selectById(1L);
        assertThat(user).isNotNull();
    }
}

五、Mock 相关注解

@MockBean

在 Spring 容器中创建一个 Mockito Mock 对象,并替换同类型的 Bean。常用于隔离外部依赖。

@SpringBootTest
public class UserServiceTest {
    @MockBean
    private UserMapper userMapper;

    @Autowired
    private UserService userService;

    @Test
    public void testGetUser() {
        User mockUser = new User(1L, "张三");
        when(userMapper.selectById(1L)).thenReturn(mockUser);

        User result = userService.getById(1L);
        assertThat(result.getName()).isEqualTo("张三");
    }
}

@SpyBean

创建 Mockito Spy 对象,保留真实对象的部分行为,可以部分 Mock。

@SpyBean
private UserService userService;

@Test
public void testPartialMock() {
    // 真实调用会执行真实逻辑,只有 doReturn 的部分被 Mock
    doReturn("Mocked Name").when(userService).getName();
}

@MockBean 和 @SpyBean 的区别在于:@MockBean 完全模拟对象,所有方法调用都返回默认值或需要显式 stubbing;@SpyBean 保留真实对象的行为,只对显式 stubbing 的方法返回 Mock 值。

六、配置覆盖

@TestPropertySource

在测试类上指定额外的属性配置,覆盖主配置文件中的属性。

@SpringBootTest
@TestPropertySource(properties = {
    "app.name=test-app",
    "app.timeout=5000"
})
public class ConfigTest {
    @Value("${app.name}")
    private String appName;

    @Test
    public void testProperties() {
        assertThat(appName).isEqualTo("test-app");
    }
}

@TestConfiguration

在测试类中定义额外的配置,仅在测试环境中生效,不会影响主应用。

@TestConfiguration
public class TestConfig {
    @Bean
    public UserService mockUserService() {
        return mock(UserService.class);
    }
}

@SpringBootTest
@Import(TestConfig.class)
public class ServiceTest {
    @Autowired
    private UserService userService;
}

七、事务与回滚

默认情况下,@DataJpaTest 和 @MybatisTest 会在每个测试方法结束后自动回滚事务,测试数据不会持久化到数据库。

@SpringBootTest 默认不自动回滚,需要手动添加 @Transactional 和 @Rollback:

@SpringBootTest
@Transactional
@Rollback
public class UserServiceTest {
    @Test
    public void testInsertUser() {
        userService.insert(new User("张三"));
        // 测试结束后自动回滚
    }
}

八、断言工具

AssertJ

spring-boot-starter-test 默认依赖 AssertJ,提供了流式断言 API:

// 基本断言
assertThat(user.getName()).isEqualTo("张三");
assertThat(user.getAge()).isGreaterThan(18);

// 集合断言
assertThat(userList).hasSize(3);
assertThat(userList).extracting(User::getName).contains("张三", "李四");

// 异常断言
assertThatThrownBy(() -> userService.getById(null))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessageContaining("id不能为空");

JsonPath

用于断言 JSON 响应:

mockMvc.perform(get("/user/1"))
       .andExpect(jsonPath("$.id").value(1))
       .andExpect(jsonPath("$.name").value("张三"))
       .andExpect(jsonPath("$.orders.length()").value(3));

九、上下文缓存

Spring 测试框架会缓存 ApplicationContext。如果多个测试类有相同的配置,它们会复用同一个上下文,避免重复加载,加快测试执行速度。

// Test1 加载了上下文
@SpringBootTest
class Test1 { }

// Test2 复用 Test1 的上下文(配置相同)
@SpringBootTest
class Test2 { }

如果某个测试类使用了不同的配置(如 @MockBean 或 @TestPropertySource),会创建新的上下文,缓存失效。

// 上下文 A:使用 @MockBean
@SpringBootTest
class TestWithMock { }

// 上下文 B:没有 @MockBean(不同配置,不能复用)
@SpringBootTest
class TestWithoutMock { }

十、最佳实践

  • 使用切片测试(@WebMvcTest、@DataJpaTest)代替完整 @SpringBootTest,启动速度更快,隔离性更好
  • 单元测试用 @MockBean 模拟外部依赖,减少测试对环境的依赖
  • 在 @DataJpaTest 和 @MybatisTest 中,对查询逻辑进行验证,确保 SQL 正确性
  • @WebMvcTest 测试 Controller 时验证 HTTP 状态码、响应体结构、JSON 字段类型,确保接口契约正确
  • 用 @TestPropertySource 覆盖敏感配置(如第三方 API Key、数据库连接串),避免测试代码泄露敏感信息
  • 保持测试独立性,每个测试方法执行后恢复上下文状态,避免测试间依赖执行顺序
  • @BeforeEach 和 @AfterEach 用于准备测试数据和清理资源
  • 测试方法命名清晰,如 shouldReturnUserWhenIdExists 比 testGetUser 更能表达意图
  • 使用 @DisplayName 为测试方法添加中文或可读性更强的描述
  • 生产代码和测试代码保持相同包结构,方便访问包级私有成员