Skip to content

Spring Boot 测试完全指南:从入门到精通 🚀

引言:为什么测试如此重要?

在现代软件开发中,测试不仅仅是验证代码是否正确运行,更是确保应用程序在各种场景下都能稳定、可靠地工作。Spring Boot 作为 Java 生态系统中最受欢迎的框架之一,提供了强大而灵活的测试支持,让开发者能够轻松编写各种类型的测试。

NOTE

Spring Boot 应用本质上就是一个 Spring ApplicationContext,因此测试 Spring Boot 应用与测试普通 Spring 应用没有本质区别。但 Spring Boot 提供了许多便利的测试注解和工具,让测试变得更加简单高效。

核心概念:@SpringBootTest 注解

什么是 @SpringBootTest?

@SpringBootTest 是 Spring Boot 提供的核心测试注解,它可以替代标准的 @ContextConfiguration 注解。这个注解的魔力在于它通过 SpringApplication 来创建测试中使用的 ApplicationContext,这意味着你的测试环境会更接近真实的运行环境。

Web 环境配置

@SpringBootTest 提供了四种不同的 Web 环境配置:

kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class MockEnvironmentTest {
    // 提供模拟的 Web 环境,不启动真实服务器
    // 适合快速的单元测试
}
kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RandomPortTest {
    // 启动真实的嵌入式服务器,使用随机端口
    // 适合集成测试
    
    @LocalServerPort
    private var port: Int = 0
}
kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class DefinedPortTest {
    // 使用配置文件中定义的端口或默认的 8080 端口
}
kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class NoWebEnvironmentTest {
    // 不提供任何 Web 环境
    // 适合测试非 Web 组件
}

TIP

对于大多数 Web 应用测试,推荐使用 RANDOM_PORT,因为它既提供了真实的服务器环境,又避免了端口冲突问题。

测试配置的自动发现

Spring Boot 的一个强大特性是能够自动发现测试配置。当你不显式指定配置类时,Spring Boot 会从测试类所在的包开始向上搜索,直到找到带有 @SpringBootApplication@SpringBootConfiguration 注解的类。

kotlin
// 假设你的项目结构如下:
// com.example.myapp.MyApplication (带有 @SpringBootApplication)
// com.example.myapp.service.UserService
// com.example.myapp.service.UserServiceTest

@SpringBootTest
class UserServiceTest {
    // Spring Boot 会自动找到 MyApplication 作为配置类
    @Autowired
    private lateinit var userService: UserService
    
    @Test
    fun `should create user successfully`() {
        // 测试逻辑
    }
}

使用 Main 方法进行测试

有时候,你的 main 方法可能包含一些特殊的配置逻辑:

kotlin
@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args) {
        setBannerMode(Banner.Mode.OFF) 
        setAdditionalProfiles("test-profile") 
    }
}

如果你希望测试也使用这些配置,可以使用 useMainMethod 属性:

kotlin
@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)
class MainMethodTest {
    @Test
    fun `should use main method configuration`() {
        // 这个测试会调用 main 方法来创建 ApplicationContext
        // 因此会应用 main 方法中的所有配置
    }
}

Mock 环境测试:快速而高效

Mock 环境测试是最常用的测试方式,它提供了模拟的 Web 环境,无需启动真实的服务器,因此执行速度很快。

使用 MockMvc

kotlin
@SpringBootTest
@AutoConfigureMockMvc
class MockMvcTest {
    
    @Autowired
    private lateinit var mockMvc: MockMvc
    
    @Test
    fun `should return hello world`() {
        mockMvc.perform(get("/hello"))
            .andExpect(status().isOk)
            .andExpect(content().string("Hello World"))
    }
}

使用 MockMvcTester (推荐)

如果你的项目中有 AssertJ,Spring Boot 还提供了更现代的 MockMvcTester

kotlin
@SpringBootTest
@AutoConfigureMockMvc
class MockMvcTesterTest {
    
    @Autowired
    private lateinit var mvc: MockMvcTester
    
    @Test
    fun `should return hello world with better syntax`() {
        assertThat(mvc.get().uri("/hello"))
            .hasStatusOk()
            .hasBodyTextEqualTo("Hello World")
    }
}

使用 WebTestClient

对于响应式应用,可以使用 WebTestClient

kotlin
@SpringBootTest
@AutoConfigureWebTestClient
class WebTestClientTest {
    
    @Autowired
    private lateinit var webClient: WebTestClient
    
    @Test
    fun `should handle reactive endpoint`() {
        webClient
            .get().uri("/reactive-hello")
            .exchange()
            .expectStatus().isOk
            .expectBody<String>().isEqualTo("Hello Reactive World")
    }
}

真实服务器测试:完整的集成测试

当你需要测试完整的 HTTP 请求-响应周期时,可以启动真实的服务器:

kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTest {
    
    @Autowired
    private lateinit var webClient: WebTestClient
    
    @Test
    fun `should handle real HTTP request`() {
        webClient
            .get().uri("/api/users")
            .exchange()
            .expectStatus().isOk
            .expectBodyList<User>()
            .hasSize(3)
    }
}

如果你不想使用 WebFlux,也可以使用传统的 TestRestTemplate

kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RestTemplateTest {
    
    @Autowired
    private lateinit var restTemplate: TestRestTemplate
    
    @Test
    fun `should get users via REST template`() {
        val response = restTemplate.getForObject("/api/users", Array<User>::class.java)
        assertThat(response).hasSize(3)
    }
}

切片测试:专注特定层级

Spring Boot 提供了多种切片测试注解,让你可以只测试应用的特定部分,从而提高测试速度和专注度。

Web 层测试 (@WebMvcTest)

kotlin
@WebMvcTest(UserController::class)
class UserControllerTest {
    
    @Autowired
    private lateinit var mvc: MockMvcTester
    
    @MockitoBean
    private lateinit var userService: UserService
    
    @Test
    fun `should get user by id`() {
        // 模拟服务层行为
        given(userService.findById(1L))
            .willReturn(User(1L, "John", "[email protected]"))
        
        // 测试控制器
        assertThat(mvc.get().uri("/users/1"))
            .hasStatusOk()
            .hasJsonPath("$.name", "John")
            .hasJsonPath("$.email", "[email protected]")
    }
}

IMPORTANT

@WebMvcTest 只会加载 Web 层相关的组件,如 @Controller@ControllerAdvice 等,不会加载 @Service@Repository 组件。这就是为什么我们需要使用 @MockitoBean 来模拟服务层依赖。

数据访问层测试 (@DataJpaTest)

kotlin
@DataJpaTest
class UserRepositoryTest {
    
    @Autowired
    private lateinit var testEntityManager: TestEntityManager
    
    @Autowired
    private lateinit var userRepository: UserRepository
    
    @Test
    fun `should find user by email`() {
        // 准备测试数据
        val user = User(name = "John", email = "[email protected]")
        testEntityManager.persistAndFlush(user)
        
        // 执行查询
        val found = userRepository.findByEmail("[email protected]")
        
        // 验证结果
        assertThat(found).isNotNull
        assertThat(found?.name).isEqualTo("John")
    }
}

JSON 序列化测试 (@JsonTest)

kotlin
@JsonTest
class UserJsonTest {
    
    @Autowired
    private lateinit var json: JacksonTester<User>
    
    @Test
    fun `should serialize user to JSON`() {
        val user = User(1L, "John", "[email protected]")
        
        assertThat(json.write(user))
            .hasJsonPathStringValue("$.name")
            .hasJsonPathStringValue("$.email")
            .extractingJsonPathStringValue("$.name")
            .isEqualTo("John")
    }
    
    @Test
    fun `should deserialize JSON to user`() {
        val content = """{"id":1,"name":"John","email":"[email protected]"}"""
        
        assertThat(json.parse(content))
            .isEqualTo(User(1L, "John", "[email protected]"))
    }
}

测试配置管理

使用 @TestConfiguration

当你需要为测试提供特殊配置时,可以使用 @TestConfiguration

kotlin
@TestConfiguration
class TestConfig {
    
    @Bean
    @Primary
    fun testUserService(): UserService {
        return mockk<UserService>() // 使用 MockK 创建模拟对象
    }
    
    @Bean
    fun testDataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build()
    }
}

@SpringBootTest
@Import(TestConfig::class)
class UserServiceIntegrationTest {
    // 测试逻辑
}

排除测试配置

如果你有一些只用于特定测试的配置类,可以使用 @TestConfiguration 并确保它们不会被组件扫描意外加载:

kotlin
@TestConfiguration
class SpecialTestConfig {
    // 这个配置只会在显式导入时才会被加载
}

高级测试技巧

使用应用参数

kotlin
@SpringBootTest(args = ["--app.test=true", "--debug"])
class ApplicationArgumentsTest {
    
    @Test
    fun `should parse application arguments`(@Autowired args: ApplicationArguments) {
        assertThat(args.optionNames).contains("app.test")
        assertThat(args.getOptionValues("app.test")).containsOnly("true")
    }
}

JMX 测试

kotlin
@SpringBootTest(properties = ["spring.jmx.enabled=true"])
@DirtiesContext
class JmxTest {
    
    @Autowired
    private lateinit var mBeanServer: MBeanServer
    
    @Test
    fun `should access MBean server`() {
        assertThat(mBeanServer.domains).contains("java.lang")
    }
}

自定义 WebTestClient

kotlin
@TestConfiguration
class WebTestClientConfig {
    
    @Bean
    fun webTestClientCustomizer(): WebTestClientBuilderCustomizer {
        return WebTestClientBuilderCustomizer { builder ->
            builder
                .defaultHeader("X-Test-Header", "test-value")
                .responseTimeout(Duration.ofSeconds(10))
        }
    }
}

最佳实践和建议

1. 选择合适的测试类型

测试金字塔原则

  • 单元测试 (70%): 使用 @WebMvcTest@DataJpaTest 等切片测试
  • 集成测试 (20%): 使用 @SpringBootTest 测试多个组件的交互
  • 端到端测试 (10%): 使用真实服务器测试完整的用户场景

2. 合理使用 Mock

kotlin
@SpringBootTest
class OrderServiceTest {
    
    @Autowired
    private lateinit var orderService: OrderService
    
    @MockitoBean
    private lateinit var paymentService: PaymentService
    
    @MockitoBean
    private lateinit var inventoryService: InventoryService
    
    @Test
    fun `should create order successfully`() {
        // 模拟外部依赖
        given(paymentService.processPayment(any())).willReturn(true)
        given(inventoryService.reserveItems(any())).willReturn(true)
        
        // 测试核心逻辑
        val order = orderService.createOrder(OrderRequest("item1", 2))
        
        assertThat(order.status).isEqualTo(OrderStatus.CONFIRMED)
    }
}

3. 测试数据管理

kotlin
@DataJpaTest
@Sql("/test-data.sql") 
class UserRepositoryTest {
    
    @Autowired
    private lateinit var userRepository: UserRepository
    
    @Test
    @Transactional
    @Rollback
    fun `should find active users`() {
        val activeUsers = userRepository.findByStatus(UserStatus.ACTIVE)
        assertThat(activeUsers).hasSize(3)
    }
}

4. 环境隔离

kotlin
@SpringBootTest
@ActiveProfiles("test") 
@TestPropertySource(properties = [
    "spring.datasource.url=jdbc:h2:mem:testdb",
    "logging.level.org.springframework.web=DEBUG"
])
class IntegrationTest {
    // 测试逻辑
}

常见问题和解决方案

问题 1: 测试运行缓慢

WARNING

如果你的测试运行很慢,可能是因为每个测试都在重新创建 ApplicationContext。

解决方案: 确保测试使用相同的配置,这样 Spring 会复用 ApplicationContext:

kotlin
// 好的做法:这些测试会共享同一个 ApplicationContext
@SpringBootTest
class UserServiceTest { /* ... */ }

@SpringBootTest
class OrderServiceTest { /* ... */ }

// 不好的做法:每个测试都有不同的配置
@SpringBootTest(properties = ["debug=true"])
class UserServiceTest { /* ... */ }

@SpringBootTest(properties = ["debug=false"])
class OrderServiceTest { /* ... */ }

问题 2: 测试之间相互影响

CAUTION

当测试修改了共享状态(如数据库)时,可能会影响其他测试。

解决方案: 使用 @DirtiesContext 或确保测试的事务会回滚:

kotlin
@SpringBootTest
@Transactional
class UserServiceTest {
    
    @Test
    @Rollback
    fun `should create user`() {
        // 这个测试的数据库修改会在测试结束后回滚
    }
    
    @Test
    @DirtiesContext
    fun `should handle special case`() {
        // 这个测试会强制重新创建 ApplicationContext
    }
}

问题 3: 无法注入依赖

解决方案: 检查组件扫描配置和测试切片的限制:

kotlin
@WebMvcTest(UserController::class)
@Import(UserService::class) 
class UserControllerTest {
    // 如果 UserService 不在 @WebMvcTest 的扫描范围内,需要显式导入
}

总结

Spring Boot 的测试框架提供了强大而灵活的测试能力,从快速的单元测试到完整的集成测试,都有相应的解决方案。关键是要:

  1. 选择合适的测试策略 - 根据测试目标选择合适的注解和配置
  2. 保持测试的独立性 - 避免测试之间的相互依赖
  3. 合理使用 Mock - 在需要时隔离外部依赖
  4. 关注测试性能 - 通过复用 ApplicationContext 和选择合适的测试切片来提高效率

通过掌握这些测试技巧,你可以构建出既快速又可靠的测试套件,为你的 Spring Boot 应用提供坚实的质量保障。

NOTE

记住,好的测试不仅能发现 bug,更重要的是能够在重构时提供信心,确保代码的行为符合预期。投入时间编写高质量的测试,长期来看会大大提高开发效率。