Appearance
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 测试依赖注入的优势
- 简化测试代码:无需手动创建复杂的依赖关系
- 真实环境测试:使用真实的 Spring 配置和 Bean
- 配置复用:可以复用生产环境的配置
- 集成测试:测试真实的 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
在测试中,字段注入是最推荐的方式,因为它简洁明了,而且测试类不需要被外部直接实例化。
通过合理使用依赖注入,我们可以编写出更加简洁、可维护的集成测试,确保我们的应用在真实环境中能够正常工作。 🎉