You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot集成测试添加@Transactional后无数据返回问题

嘿,我之前也遇到过一模一样的问题,来给你把这个事儿说清楚,再给你几个可行的解决办法:

为啥加了@Transactional就没数据了?

当你在测试类上加上@Transactional之后,Spring会给整个测试方法套一个独立的事务,而且默认会在测试结束后把这个事务回滚掉。重点来了:你用TestRestTemplate发的REST请求,是跑在Tomcat的单独线程里的,这个线程的事务和测试代码所在的事务完全是两个独立的上下文!

你在beforeEach里调用saveAll存的数据,都在测试的事务里待着——因为最后要回滚,所以这个事务根本没提交。而REST请求的线程看不到未提交的事务数据,自然返回空啦。

没加@Transactional的时候,saveAll的操作直接就提交到数据库了,REST接口当然能读到数据。

怎么解决?

这里有几个靠谱的方案,你挑适合自己的来:

方案1:用@Sql注解提前插测试数据

@Sql可以在测试事务启动之前就执行SQL脚本把数据插进去,而且可以让它在独立的事务里执行,这样数据是提交状态,REST请求就能读到。同时测试类的@Transactional依然能保证测试过程中产生的数据被回滚,不会影响后续测试。

示例代码:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("int-test")
@Transactional
// 测试前插入数据,用独立事务执行
@Sql(
    scripts = ["classpath:sql/insert_test_organizations.sql"],
    executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
    config = SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)
)
// 测试后清理数据,同样用独立事务
@Sql(
    scripts = ["classpath:sql/clean_test_organizations.sql"],
    executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD,
    config = SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)
)
class OrganizationControllerIntTest {
    // 这里就不用beforeEach存数据啦
    @Test
    fun `When GET organizations then return all organizations`() {
        val result = restTemplate.getForEntity("/organizations", String::class.java)
        assertThat(result?.statusCode, equalTo(HttpStatus.OK))
        assertThat(result?.body, containsString("items"))
    }
}

方案2:用TransactionTemplate开独立事务存数据

如果你不想写SQL脚本,想用代码生成测试数据,可以用TransactionTemplate开一个新的事务来存数据,确保这个事务提交,这样REST请求就能读到。测试结束后,测试类的@Transactional会回滚测试过程的修改,不过你得手动清理提前存的测试数据。

示例代码:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("int-test")
@Transactional
class OrganizationControllerIntTest {
    @Autowired
    private lateinit var organizationRepository: OrganizationRepository
    @Autowired
    private lateinit var restTemplate: TestRestTemplate
    @Autowired
    private lateinit var testDataGenerator: TestDataGenerator
    @Autowired
    private lateinit var transactionTemplate: TransactionTemplate

    @BeforeEach
    fun beforeEach() {
        // 开一个新的事务,存完数据就提交
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW)
        transactionTemplate.execute {
            val organizations = testDataGenerator.organizations(10).toList()
            organizationRepository.saveAll(organizations)
            null
        }
    }

    @AfterEach
    fun afterEach() {
        // 同样开新事务清理数据,避免影响其他测试
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW)
        transactionTemplate.execute {
            organizationRepository.deleteAll()
            null
        }
    }

    @Test
    fun `When GET organizations then return all organizations`() {
        val result = restTemplate.getForEntity("/organizations", String::class.java)
        assertThat(result?.statusCode, equalTo(HttpStatus.OK))
        assertThat(result?.body, containsString("items"))
    }
}

方案3:去掉@Transactional,手动清理数据

如果你的测试不需要回滚事务,干脆把测试类上的@Transactional去掉,在@AfterEach里手动删掉测试数据就行。这种方式简单直接,就是每次测试后要自己清理,不然数据会残留。

示例代码:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("int-test")
class OrganizationControllerIntTest {
    @Autowired
    private lateinit var organizationRepository: OrganizationRepository
    @Autowired
    private lateinit var restTemplate: TestRestTemplate
    @Autowired
    private lateinit var testDataGenerator: TestDataGenerator

    @BeforeEach
    fun beforeEach() {
        val organizations = testDataGenerator.organizations(10).toList()
        organizationRepository.saveAll(organizations)
    }

    @AfterEach
    fun afterEach() {
        organizationRepository.deleteAll()
    }

    @Test
    fun `When GET organizations then return all organizations`() {
        val result = restTemplate.getForEntity("/organizations", String::class.java)
        assertThat(result?.statusCode, equalTo(HttpStatus.OK))
        assertThat(result?.body, containsString("items"))
    }
}
总结一下

核心就是测试线程的事务和REST请求线程的事务是隔离的,只要让测试数据处于已提交的状态,REST接口就能读到。你可以根据自己的需求选方案,比如想保留测试回滚就用前两个,想简单直接就用第三个。

内容的提问来源于stack exchange,提问作者pgiecek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 07:56:57