Skip to content

Spring TestContext Framework:混合配置的艺术 🎨

引言:为什么需要混合配置?

在实际的 Spring 项目开发中,我们经常会遇到这样的场景:生产环境使用 XML 配置,但在测试时希望使用更灵活的 @Configuration 类;或者项目中既有历史遗留的 XML 配置,又有新开发的注解配置。Spring TestContext Framework 为我们提供了优雅的解决方案。

NOTE

混合配置不是为了炫技,而是为了在测试环境中更好地适应复杂的项目需求,让测试配置既能复用生产配置,又能灵活定制。

核心概念理解

什么是混合配置?

混合配置是指在同一个 ApplicationContext 中同时使用多种配置方式:

  • XML 配置文件:传统的 Spring 配置方式
  • Groovy 脚本:更简洁的 DSL 配置方式
  • 组件类:基于注解的 @Configuration

实际应用场景

场景一:生产环境 XML + 测试环境注解配置

假设我们有一个电商系统,生产环境使用 XML 配置数据库连接,但测试时希望使用内存数据库。

xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 生产环境数据源配置 -->
    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/ecommerce"/>
        <property name="username" value="root"/>
        <property name="password" value="password"/>
    </bean>
    
    <!-- 业务服务配置 -->
    <bean id="orderService" class="com.example.service.OrderService">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>
kotlin
@Configuration
@ComponentScan("com.example.service")
class TestConfiguration {
    
    /**
     * 测试环境使用内存数据库
     * 覆盖生产环境的数据源配置
     */
    @Bean
    @Primary
    fun testDataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:test-data.sql")
            .build()
    }
    
    /**
     * 测试专用的模拟服务
     */
    @Bean
    fun mockPaymentService(): PaymentService {
        return Mockito.mock(PaymentService::class.java)
    }
}

场景二:使用 @ImportResource 导入 XML 配置

kotlin
@Configuration
@ImportResource("classpath:production-config.xml") 
@TestPropertySource(properties = [
    "spring.datasource.url=jdbc:h2:mem:testdb",
    "spring.jpa.hibernate.ddl-auto=create-drop"
])
class MixedTestConfiguration {
    
    /**
     * 在注解配置中定义测试专用的 Bean
     */
    @Bean
    @Profile("test")
    fun testEmailService(): EmailService {
        return object : EmailService {
            override fun sendEmail(to: String, subject: String, body: String) {
                println("📧 模拟发送邮件: $to - $subject") 
            }
        }
    }
}

测试实战示例

完整的混合配置测试

kotlin
/**
 * 电商订单服务测试
 * 演示如何混合使用 XML 配置和注解配置
 */
@SpringBootTest
@ContextConfiguration(
    locations = ["classpath:production-config.xml"], // XML 配置
    classes = [TestConfiguration::class] // 注解配置
)
@ActiveProfiles("test")
@Transactional
class OrderServiceMixedConfigTest {
    
    @Autowired
    private lateinit var orderService: OrderService
    
    @Autowired
    private lateinit var dataSource: DataSource
    
    @Autowired
    private lateinit var emailService: EmailService
    
    @Test
    fun `应该使用测试数据源创建订单`() {
        // Given: 准备测试数据
        val order = Order(
            id = 1L,
            customerId = "CUST001",
            amount = BigDecimal("99.99"),
            status = OrderStatus.PENDING
        )
        
        // When: 创建订单
        val savedOrder = orderService.createOrder(order) 
        
        // Then: 验证结果
        assertThat(savedOrder.id).isNotNull()
        assertThat(savedOrder.status).isEqualTo(OrderStatus.CONFIRMED)
        
        // 验证使用的是测试数据源(H2 内存数据库)
        assertThat(dataSource).isInstanceOf(EmbeddedDatabase::class.java) 
    }
    
    @Test
    fun `应该使用模拟邮件服务发送通知`() {
        // Given
        val order = Order(
            customerId = "CUST002",
            amount = BigDecimal("199.99")
        )
        
        // When
        orderService.createOrderWithNotification(order)
        
        // Then: 验证邮件服务被调用(这里是模拟的)
        verify(emailService).sendEmail(
            eq("[email protected]"),
            contains("订单确认"),
            any()
        )
    }
}

使用 Groovy 脚本配置

Groovy 配置示例(点击展开)
groovy
// test-config.groovy
beans {
    // 定义测试数据源
    testDataSource(EmbeddedDatabaseBuilder) {
        setType(EmbeddedDatabaseType.H2)
        addScript("classpath:schema.sql")
        addScript("classpath:test-data.sql")
    }
    
    // 定义模拟服务
    mockNotificationService(MockNotificationService) {
        enabled = false
    }
    
    // 定义测试配置
    testProperties(Properties) {
        put("app.environment", "test")
        put("logging.level.com.example", "DEBUG")
    }
}
kotlin
@ContextConfiguration(
    locations = ["classpath:test-config.groovy"],
    classes = [MainConfiguration::class]
)
class GroovyMixedConfigTest {
    // 测试代码...
}

配置策略与最佳实践

策略选择指南

最佳实践建议

配置优先级原则

  1. 测试配置优先:使用 @Primary@Profile("test") 确保测试配置覆盖生产配置
  2. 单一入口点:选择一种配置方式作为主入口,其他配置方式作为补充
  3. 清晰的职责分离:生产配置负责核心业务,测试配置负责测试专用逻辑

常见陷阱与解决方案

配置冲突问题

当多种配置方式定义了相同的 Bean 时,可能会出现冲突。解决方案:

kotlin
@Configuration
class ConflictResolutionConfig {
    
    @Bean
    @Primary // 明确指定优先级
    @Profile("test")
    fun testDataSource(): DataSource {
        return EmbeddedDatabaseBuilder().build()
    }
    
    @Bean
    @ConditionalOnMissingBean // 条件化创建
    fun defaultDataSource(): DataSource {
        return HikariDataSource()
    }
}

框架支持情况

Spring Boot 的优势

Spring Boot 对混合配置提供了更好的支持:

kotlin
@SpringBootTest
@TestPropertySource(locations = ["classpath:test.properties"])
@Import(TestConfiguration::class) 
class SpringBootMixedConfigTest {
    
    // Spring Boot 会自动整合:
    // 1. application.yml/properties
    // 2. @TestPropertySource 指定的配置
    // 3. @Import 导入的配置类
    // 4. 自动配置类
}

传统 Spring Framework 的限制

框架限制说明

传统的 Spring Framework 在标准部署中不支持同时加载多种资源类型,但在测试环境中,通过 TestContext Framework 可以实现混合配置。

总结

混合配置是 Spring TestContext Framework 提供的强大功能,它让我们能够:

灵活复用生产配置:避免重复定义相同的 Bean
定制测试环境:针对测试场景进行特殊配置
渐进式迁移:从 XML 配置平滑过渡到注解配置
团队协作友好:不同团队成员可以使用熟悉的配置方式

关键要点

混合配置的核心思想是"一个入口,多种补充"。选择一种主要的配置方式作为入口点,然后通过导入、扫描等方式整合其他配置,这样既保持了配置的清晰性,又获得了最大的灵活性。

通过合理运用混合配置,我们可以构建出既强大又灵活的测试环境,为项目的质量保驾护航! 🚀