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]

Reply via email to