Grails 4.0.3集成测试疑问:setup()调用外部方法保存的数据无法持久化是Bug还是预期行为?
这是Grails集成测试的预期行为,而非框架Bug
你碰到的这个差异,根源在于Spring(Grails基于Spring构建)的事务代理机制,以及GORM的会话管理逻辑,咱们一步步理清楚:
核心原理:Spring AOP代理的"内部调用限制"
Grails的@Integration测试配合@Rollback注解时,默认会为每个测试方法(包括setup和cleanup)创建一个独立的事务,测试完成后自动回滚。但Spring的事务增强是靠动态代理实现的——只有通过代理对象调用的方法才会触发事务拦截逻辑,同一个类内部的直接方法调用,是不会经过代理层的,自然也无法继承当前的事务上下文。
为什么第一个测试用例会失败?
看你第一个测试用例的代码:
def setup() { createUser('Daonald') assert User.findByName('Daonald') // passes } def createUser(String name) { User user = new User(name: name).save(failOnError: true, flush: true) }
setup()本身运行在测试事务的上下文里,但你直接调用createUser()属于类内部方法调用,跳过了Spring的事务代理。- 当
createUser()执行save(flush: true)时,因为没有可继承的代理事务,GORM会为这个save操作创建一个临时的独立事务,并且在方法执行完毕后自动回滚(因为没有外部事务管理)。 setup()里的assert能通过,是因为flush: true把数据刷到了当前的Hibernate会话中,但这个数据并没有真正提交到数据库;当createUser()方法结束,临时事务回滚,会话里的脏数据被清理。- 到了测试方法执行时,测试事务的会话里已经没有这条数据了,所以
User.findByName('Daonald')会返回null,导致assert失败。
为什么第二个测试用例能通过?
再看第二个测试用例:
def setup() { User user = new User(name: 'Daonald').save(failOnError: true, flush: true) assert User.findByName('Daonald') // passes }
这里的save操作直接在setup()内部执行,而setup()是被测试框架通过代理调用的,所以整个操作都处于测试事务的上下文里。flush: true只是把数据刷到当前会话,并没有提交事务——测试方法执行时,依然在同一个事务会话中,自然能查到这条数据,直到测试结束后整个事务被@Rollback回滚。
如何解决第一个测试用例的问题?
如果想要在setup()里调用自定义方法保存数据,同时让数据能在测试方法中访问,可以用以下两种方式:
- 把
createUser()方法提取到单独的服务类中:通过依赖注入调用服务类的方法,这样会经过Spring代理,事务上下文会被正确继承。 - 手动获取代理对象调用自身方法:在测试类中注入
ApplicationContext,通过applicationContext.getBean(getClass())获取自身的代理对象,然后用代理对象调用createUser()方法。
举个用第一种方法实现的示例:
// 先创建一个UserService @Service class UserService { User createUser(String name) { new User(name: name).save(failOnError: true, flush: true) } } // 测试类中注入服务 @Integration @Rollback class MySpec extends Specification { UserService userService def setup() { userService.createUser('Daonald') assert User.findByName('Daonald') } void 'data persists now'() { expect: User.findByName('Daonald') != null } }
内容的提问来源于stack exchange,提问作者LazyMap
相关产品推荐
相关产品推荐

