jamesfredley commented on issue #15588:
URL: https://github.com/apache/grails-core/issues/15588#issuecomment-4320930620
Why `@Rollback` Doesn't Work In JUnit Jupiter Integration Tests
Looking end-to-end through the AST transformations. There are **three
concurrent problems**, and the fix is non-trivial.
### Root Cause #1 - `@Integration` AST hardcodes JUnit 4 for non-Spock tests
`grails-testing-support-core/src/main/groovy/org/grails/compiler/injection/testing/IntegrationTestAstTransformation.groovy`
lines **140-161**:
```groovy
if (GrailsASTUtils.isSubclassOf(classNode, SPEC_CLASS)) {
// Spock branch: adds @ContextConfiguration(loader =
GrailsApplicationContextLoader)
...
} else {
// Must be a JUnit 4 test so add JUnit spring annotations <-- the
comment lies in 7.x
def runWithAnnotation = new AnnotationNode(RUN_WITH_ANNOTATION_NODE)
runWithAnnotation.addMember('value', new
ClassExpression(SPRING_JUNIT4_CLASS_RUNNER))
classNode.addAnnotation(runWithAnnotation)
def contextConfigAnn = new
AnnotationNode(SPRING_APPLICATION_CONFIGURATION_CLASS_NODE)
contextConfigAnn.addMember('classes', new
ClassExpression(applicationClassNode))
classNode.addAnnotation(contextConfigAnn)
}
```
There is **no JUnit Jupiter branch**. JUnit 5 ignores `@RunWith`. Although
the transform also adds `@SpringBootTest` (which meta-includes
`@ExtendWith(SpringExtension)`), the rest of the wiring
(`GrailsTestConfiguration`, `GrailsApplicationContextLoader`) is never applied
to Jupiter tests. The result is that the test runs through `SpringExtension`
but with the wrong context loader, so the GORM `transactionManager` autowire is
fragile.
### Root Cause #2 - `@Rollback` AST treats `@BeforeEach`/`@AfterEach` as
committing setup
`grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/reflect/AstAnnotationUtils.groovy`
line **42**:
```groovy
private static final Set<String> JUNIT_ANNOTATION_NAMES =
new HashSet<String>(Arrays.asList(
'org.junit.jupiter.api.BeforeEach',
'org.junit.jupiter.api.AfterEach'))
```
In `AbstractMethodDecoratingTransformation.weaveClassNode` (lines 124-156),
any method matching `hasJunitAnnotation(md)` is routed to
`weaveTestSetupMethod`. For `RollbackTransform` that overrides
`weaveTestSetupMethod` (in `TransactionalTransform`):
```groovy
protected void weaveTestSetupMethod(...) {
def requiresNewTransaction = new AnnotationNode(annotationNode.classNode)
requiresNewTransaction.addMember('propagation',
propX(classX(Propagation), 'REQUIRES_NEW'))
weaveNewMethod(sourceUnit, requiresNewTransaction, classNode,
methodNode, genericsSpec)
}
```
And `buildDelegatingMethodCall` (line 350):
```groovy
String executeMethodName = isTestSetupOrCleanup(classNode, methodNode)
? METHOD_EXECUTE // <-- 'execute' = COMMIT
: getTransactionTemplateMethodName() // <-- 'executeAndRollback'
```
So with class-level `@Rollback`:
- `@BeforeEach setUp()` -> wrapped in **`REQUIRES_NEW` + `execute` ->
commits**
- `@AfterEach tearDown()` -> wrapped in **`REQUIRES_NEW` + `execute` ->
commits**
- `@Test testFoo()` -> wrapped in **`executeAndRollback` -> rolls back**
If the user expects data inserted by `super.setUp()` to be rolled back, **it
never will be** - that path always commits in a brand-new transaction. This is
not new behavior (Spock works the same), but it's the design.
### Root Cause #3 - Banner `BaseIntegrationTestCase` manages its own
Hibernate sessions
The screenshot shows:
```groovy
import net.hedtech.banner.testing.BaseIntegrationTestCase
import
org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException
```
Two red flags:
1. **`org.springframework.orm.hibernate5.*` does not exist on the Grails 7 /
Spring 7 / Hibernate 6 classpath.** Either this file doesn't actually compile
in their setup, or they have a legacy hibernate5 jar floating around. Either
way, the Banner base class is built against the old Spring/Hibernate stack.
2. **Ellucian Banner's `BaseIntegrationTestCase` is well known to bind its
own Hibernate session (`SessionFactory.openSession()`/`getCurrentSession()`)
and commit through it directly**, bypassing Spring transaction management. With
Spring 7 / Hibernate 6, the session-binding contract changed; data committed
via Banner's session is not visible to Spring's `PlatformTransactionManager`,
so even if `executeAndRollback` rolls back the Spring-managed transaction,
Banner's flushes have already gone to the DB.
This is exactly what `@jdaugherty` was hinting at with "Are you using new
transactions?" - new/independent transactions opened by Banner commit
out-of-band.
### What's Actually Broken In `grails-core` (Bug)
Even if Banner weren't a factor, there is a real defect:
| Area | Behavior | Should Be |
|---|---|---|
| `IntegrationTestAstTransformation` | Adds
`@RunWith(GrailsJunit4ClassRunner)` to any non-Spock class | Detect Jupiter
(`org.junit.jupiter.api.Test` on any method) and apply
`@ExtendWith(SpringExtension)` + `@ContextConfiguration(loader =
GrailsApplicationContextLoader, classes = ...)` instead |
| Comment at line 151 | `// Must be a JUnit 4 test so add JUnit spring
annotations` | The comment is a 6.x assumption that no longer holds |
| `org.grails.testing.context.junit4` package | Only JUnit 4 runner | Needs
sibling `junit5` package with a Jupiter extension that properly bootstraps the
Grails test context |
### Recommendation To The User (Workaround)
Until a JUnit 5 path is wired through `IntegrationTestAstTransformation`, do
one of:
1. **Convert the class to a Spock `Specification`.** This is the only
first-class supported integration-test path in Grails 7.x today.
2. **Stop using Banner's `BaseIntegrationTestCase`.** It binds a Hibernate
session/transaction that bypasses Spring/GORM transaction management. While it
appeared to work in Grails 6.x, it relies on
`org.springframework.orm.hibernate5.*` types that are absent in Grails 7.
3. **Use `@DatabaseCleanup` + `@Transactional`** (per the docs link
`@jdaugherty` shared) for entire-DB cleanups between Jupiter test classes if
you can't move off Jupiter.
### How Grails can Fix
In `IntegrationTestAstTransformation.weaveIntegrationTestMixin`, add a third
branch for JUnit Jupiter:
```groovy
} else if (hasJupiterTest(classNode)) {
// @ExtendWith(SpringExtension)
// @ContextConfiguration(loader = GrailsApplicationContextLoader,
classes = applicationClass)
// (do NOT add @RunWith)
} else {
// legacy JUnit 4 ...
}
```
Detect Jupiter by scanning methods for
`org.junit.jupiter.api.Test`/`@BeforeEach`/`@AfterEach`. The
`transactionManager` autowire-by-name through `setApplicationContext` already
in place will work once `SpringExtension` is on the test class via the proper
meta-annotation rather than relying solely on `@SpringBootTest`'s side-effect.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]