Skip to content

Spring 测试框架中的依赖注入 🧪

概述

在 Spring 测试框架中,依赖注入让我们能够轻松地将 Spring 容器中的 Bean 注入到测试类中,从而实现真正的集成测试。这是 Spring TestContext Framework 的核心特性之一。

IMPORTANT

依赖注入测试装置(Test Fixtures)是 Spring 测试框架的核心功能,它让我们能够在测试中使用真实的 Spring Bean,而不是手动创建 Mock 对象。

为什么需要测试中的依赖注入? 🤔

传统测试方式的痛点

在没有 Spring 测试框架支持的情况下,我们通常需要:

kotlin
class UserServiceTest {
    
    @Test
    fun testCreateUser() {
        // 手动创建所有依赖 😰
        val dataSource = DriverManagerDataSource() 
        dataSource.setDriverClassName("org.h2.Driver") 
        dataSource.url = "jdbc:h2:mem:testdb"
        
        val jdbcTemplate = JdbcTemplate(dataSource) 
        val userRepository = UserRepository(jdbcTemplate) 
        val userService = UserService(userRepository) 
        
        // 执行测试
        val user = userService.createUser("张三", "[email protected]")
        assertNotNull(user.id)
    }
}
kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:test-config.xml")
class UserServiceTest {
    
    @Autowired
    lateinit var userService: UserService
    
    @Test
    fun testCreateUser() {
        // 直接使用注入的服务,无需手动创建依赖 🎉
        val user = userService.createUser("张三", "[email protected]") 
        assertNotNull(user.id) 
    }
}

Spring 测试依赖注入的优势

  1. 简化测试代码:无需手动创建复杂的依赖关系
  2. 真实环境测试:使用真实的 Spring 配置和 Bean
  3. 配置复用:可以复用生产环境的配置
  4. 集成测试:测试真实的 Bean 交互

核心工作原理 ⚙️

NOTE

DependencyInjectionTestExecutionListener 是默认配置的监听器,负责处理测试类中的依赖注入。

依赖注入的三种方式

1. 字段注入(Field Injection)

这是测试中最常用的方式,简洁直观:

kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:repository-config.xml")
class HibernateTitleRepositoryTests {

    // 通过字段直接注入,无需 setter 方法
    @Autowired
    lateinit var titleRepository: HibernateTitleRepository

    @Test
    fun findById() {
        val title = titleRepository.findById(10)
        assertNotNull(title)
    }
}

TIP

在 Kotlin 中,使用 lateinit var 来声明需要依赖注入的属性,这样可以避免空值检查。

2. Setter 注入(Setter Injection)

通过 setter 方法进行注入:

kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:repository-config.xml")
class HibernateTitleRepositoryTests {

    lateinit var titleRepository: HibernateTitleRepository

    @Autowired
    fun setTitleRepository(titleRepository: HibernateTitleRepository) { 
        this.titleRepository = titleRepository 
    } 

    @Test
    fun findById() {
        val title = titleRepository.findById(10)
        assertNotNull(title)
    }
}

3. 构造器注入(Constructor Injection)

仅在 JUnit Jupiter 中支持:

kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:repository-config.xml")
class HibernateTitleRepositoryTests(
    @Autowired private val titleRepository: HibernateTitleRepository
) {

    @Test
    fun findById() {
        val title = titleRepository.findById(10)
        assertNotNull(title)
    }
}

WARNING

构造器注入只在 JUnit Jupiter 中有效,其他测试框架(如 JUnit 4)不支持这种方式。

处理多个相同类型的 Bean

当 Spring 容器中存在多个相同类型的 Bean 时,需要使用限定符来指定具体注入哪个:

使用 @Qualifier 注解

kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:multi-datasource-config.xml")
class MultiDataSourceTest {

    @Autowired
    @Qualifier("primaryDataSource") 
    lateinit var primaryDataSource: DataSource

    @Autowired
    @Qualifier("secondaryDataSource") 
    lateinit var secondaryDataSource: DataSource

    @Test
    fun testMultipleDataSources() {
        assertNotNull(primaryDataSource)
        assertNotNull(secondaryDataSource)
        // 验证它们是不同的实例
        assertNotEquals(primaryDataSource, secondaryDataSource)
    }
}

使用 @Named 注解(JSR-330)

kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:multi-datasource-config.xml")
class MultiDataSourceTest {

    @Inject
    @Named("primaryDataSource") 
    lateinit var primaryDataSource: DataSource

    @Test
    fun testPrimaryDataSource() {
        assertNotNull(primaryDataSource)
    }
}

实际业务场景示例

让我们通过一个完整的电商订单服务测试来演示依赖注入的实际应用:

完整的业务场景示例
kotlin
// 订单服务
@Service
class OrderService(
    private val orderRepository: OrderRepository,
    private val paymentService: PaymentService,
    private val inventoryService: InventoryService
) {
    
    @Transactional
    fun createOrder(userId: Long, productId: Long, quantity: Int): Order {
        // 检查库存
        if (!inventoryService.checkStock(productId, quantity)) {
            throw IllegalStateException("库存不足")
        }
        
        // 创建订单
        val order = Order(
            userId = userId,
            productId = productId,
            quantity = quantity,
            status = OrderStatus.PENDING
        )
        
        val savedOrder = orderRepository.save(order)
        
        // 扣减库存
        inventoryService.reduceStock(productId, quantity)
        
        return savedOrder
    }
}

// 测试类
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:test-application-context.xml")
@Transactional
@Rollback
class OrderServiceIntegrationTest {

    @Autowired
    lateinit var orderService: OrderService

    @Autowired
    lateinit var orderRepository: OrderRepository

    @Autowired
    lateinit var inventoryService: InventoryService

    @Test
    fun `should create order successfully when stock is sufficient`() {
        // Given - 准备测试数据
        val userId = 1L
        val productId = 100L
        val quantity = 2
        
        // 确保有足够库存
        inventoryService.addStock(productId, 10)

        // When - 执行业务操作
        val order = orderService.createOrder(userId, productId, quantity)

        // Then - 验证结果
        assertNotNull(order.id)
        assertEquals(userId, order.userId)
        assertEquals(productId, order.productId)
        assertEquals(quantity, order.quantity)
        assertEquals(OrderStatus.PENDING, order.status)
        
        // 验证订单已保存到数据库
        val savedOrder = orderRepository.findById(order.id!!)
        assertNotNull(savedOrder)
        
        // 验证库存已扣减
        val remainingStock = inventoryService.getStock(productId)
        assertEquals(8, remainingStock) // 10 - 2 = 8
    }

    @Test
    fun `should throw exception when stock is insufficient`() {
        // Given
        val userId = 1L
        val productId = 200L
        val quantity = 5
        
        // 设置库存不足
        inventoryService.addStock(productId, 2) // 只有2个库存

        // When & Then
        assertThrows<IllegalStateException> {
            orderService.createOrder(userId, productId, quantity)
        }
        
        // 验证没有订单被创建
        val orders = orderRepository.findByUserId(userId)
        assertTrue(orders.isEmpty())
        
        // 验证库存没有被扣减
        val remainingStock = inventoryService.getStock(productId)
        assertEquals(2, remainingStock)
    }
}

配置文件示例

测试使用的 Spring 配置文件:

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"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           https://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           https://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/tx
           https://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 启用注解驱动 -->
    <context:annotation-config/>
    <context:component-scan base-package="com.example"/>
    
    <!-- 数据源配置 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.h2.Driver"/>
        <property name="url" value="jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>
    
    <!-- JPA 配置 -->
    <bean id="entityManagerFactory" 
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan" value="com.example.entity"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
    </bean>
    
    <!-- 事务管理器 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    
    <!-- 启用事务注解 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
</beans>

高级技巧和最佳实践

1. 继承测试基类时的注意事项

当继承 Spring 提供的测试基类时,可能需要覆盖 setter 方法:

kotlin
abstract class BaseRepositoryTest : AbstractTransactionalJUnit4SpringContextTests() {
    
    @Autowired
    @Qualifier("testDataSource") 
    override fun setDataSource(dataSource: DataSource) { 
        super.setDataSource(dataSource) 
    } 
}

@ContextConfiguration("classpath:repository-test-config.xml")
class UserRepositoryTest : BaseRepositoryTest() {
    
    @Autowired
    lateinit var userRepository: UserRepository
    
    @Test
    fun testFindByEmail() {
        val user = userRepository.findByEmail("[email protected]")
        assertNotNull(user)
    }
}

2. 禁用依赖注入

如果不想使用依赖注入,可以通过以下方式禁用:

kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:test-config.xml")
@TestExecutionListeners( 
    listeners = [
        // 注意:这里没有包含 DependencyInjectionTestExecutionListener
        DirtiesContextTestExecutionListener::class,
        TransactionalTestExecutionListener::class
    ]
) 
class ManualDependencyTest {
    
    // 不使用 @Autowired,手动获取依赖
    private lateinit var userService: UserService
    
    @Autowired
    private lateinit var applicationContext: ApplicationContext
    
    @BeforeEach
    fun setUp() {
        // 手动从容器中获取 Bean
        userService = applicationContext.getBean("userService", UserService::class.java) 
    }
    
    @Test
    fun testUserService() {
        assertNotNull(userService)
    }
}

3. 使用 @TestConfiguration 进行测试专用配置

kotlin
@TestConfiguration
class TestConfig {
    
    @Bean
    @Primary
    fun mockEmailService(): EmailService { 
        return Mockito.mock(EmailService::class.java) 
    } 
}

@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:application-context.xml")
@Import(TestConfig::class) 
class UserServiceTest {
    
    @Autowired
    lateinit var userService: UserService
    
    @Autowired
    lateinit var emailService: EmailService // 这里注入的是 Mock 对象
    
    @Test
    fun testUserRegistration() {
        // 配置 Mock 行为
        Mockito.`when`(emailService.sendWelcomeEmail(any())).thenReturn(true)
        
        val user = userService.registerUser("张三", "[email protected]")
        
        assertNotNull(user)
        verify(emailService).sendWelcomeEmail(user)
    }
}

常见问题和解决方案

问题1:NoSuchBeanDefinitionException

kotlin
// ❌ 错误示例
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:empty-config.xml") // 配置文件中没有定义需要的 Bean
class UserServiceTest {
    
    @Autowired
    lateinit var userService: UserService // [!code error] // 会抛出 NoSuchBeanDefinitionException
}

解决方案

kotlin
// ✅ 正确示例
@ExtendWith(SpringExtension::class)
@ContextConfiguration("classpath:test-config.xml") // [!code ++] // 确保配置文件包含所需的 Bean
class UserServiceTest {
    
    @Autowired
    lateinit var userService: UserService
}

问题2:多个候选 Bean 的冲突

kotlin
// ❌ 会导致 NoUniqueBeanDefinitionException
@Autowired
lateinit var dataSource: DataSource // [!code error] // 当有多个 DataSource Bean 时会失败

解决方案

kotlin
// ✅ 使用 @Qualifier 指定具体的 Bean
@Autowired
@Qualifier("primaryDataSource") 
lateinit var dataSource: DataSource

总结

Spring 测试框架中的依赖注入是一个强大的特性,它让我们能够:

  • 简化测试代码:无需手动创建复杂的依赖关系
  • 提高测试真实性:使用真实的 Spring 配置和 Bean
  • 支持多种注入方式:字段注入、setter 注入、构造器注入
  • 灵活处理复杂场景:支持多个同类型 Bean 的精确注入

TIP

在测试中,字段注入是最推荐的方式,因为它简洁明了,而且测试类不需要被外部直接实例化。

通过合理使用依赖注入,我们可以编写出更加简洁、可维护的集成测试,确保我们的应用在真实环境中能够正常工作。 🎉