diff --git a/grails-doc/src/en/guide/testing/integrationTesting.adoc b/grails-doc/src/en/guide/testing/integrationTesting.adoc index dd5e6635769..4b845640291 100644 --- a/grails-doc/src/en/guide/testing/integrationTesting.adoc +++ b/grails-doc/src/en/guide/testing/integrationTesting.adoc @@ -137,6 +137,56 @@ NOTE: It isn't possible to make `grails.gorm.transactions.Rollback` behave the s This has the downside that you cannot implement it differently for different cases (as Spring does for testing). +==== Session Binding Without Transactions + +By default, integration tests wrap each test method in a transaction. However, this doesn't always match the runtime behavior of your application. In a running Grails application, the Open Session In View (OISV) pattern provides a Hibernate session for the duration of the request, but operations outside of `@Transactional` methods don't have an active transaction. + +To test code that relies on having a session but not a transaction (matching the application's runtime behavior), you can use the `@WithSession` annotation: + +[source,groovy] +---- +import grails.testing.mixin.integration.Integration +import grails.testing.mixin.integration.WithSession +import spock.lang.* + +@Integration +@WithSession +class ExampleServiceSpec extends Specification { + + // Disable automatic transaction wrapping + static transactional = false + + void "test non-transactional service method"() { + when: "calling a service method that does SELECT operations" + def count = MyDomain.count() // Works with session only + + then: "the operation succeeds" + count >= 0 + + when: "trying to save with flush" + new MyDomain(name: "test").save(flush: true) + + then: "it fails because no transaction is active" + thrown(Exception) // Matches runtime behavior + } +} +---- + +The `@WithSession` annotation: + +* Binds a Hibernate session to the test thread without starting a transaction +* Mimics the OISV pattern used in running applications +* Allows testing of service methods that rely on session availability but are not transactional +* Can be applied at class or method level +* Supports specifying specific datasources: `@WithSession(datasources = ["secondary"])` + +This is particularly useful when: + +* Testing non-transactional service methods that perform read operations +* Ensuring test behavior matches production behavior +* Debugging issues where tests pass but the application fails (or vice versa) due to transaction differences + + ==== DirtiesContext diff --git a/grails-test-core/src/main/groovy/org/grails/test/spock/IntegrationSpecConfigurerExtension.groovy b/grails-test-core/src/main/groovy/org/grails/test/spock/IntegrationSpecConfigurerExtension.groovy index f2e1f26eca1..fbd4c769cb2 100644 --- a/grails-test-core/src/main/groovy/org/grails/test/spock/IntegrationSpecConfigurerExtension.groovy +++ b/grails-test-core/src/main/groovy/org/grails/test/spock/IntegrationSpecConfigurerExtension.groovy @@ -61,7 +61,10 @@ class IntegrationSpecConfigurerExtension implements IAnnotationDrivenExtension= 0 + domain.id != null + sessionFactory.currentSession != null + sessionFactory.currentSession.transaction.active + } + + // Test 3: With @WithSession - provides session only, no transaction + @WithSession + void "test with session annotation provides session without transaction"() { + given: + static transactional = false + + when: "Perform SELECT operation" + def count = TestComparisonDomain.count() + + then: "SELECT works with session only" + count >= 0 + sessionFactory.currentSession != null + !sessionFactory.currentSession.transaction.active + + when: "Try save without flush" + def domain = new TestComparisonDomain(name: "Session Only") + domain.save() + + then: "Save without flush works" + domain.id != null + + when: "Try save with flush" + domain.save(flush: true) + + then: "Save with flush fails without transaction" + thrown(Exception) + } + + // Test 4: Method-level @WithSession annotation + void "test method level session annotation"() { + given: + static transactional = false + + expect: "This specific test method has session bound" + withSessionMethod() + } + + @WithSession + private boolean withSessionMethod() { + TestComparisonDomain.count() >= 0 + sessionFactory.currentSession != null + !sessionFactory.currentSession.transaction.active + } +} + +// Test domain class for comparison tests +class TestComparisonDomain { + String name + + static constraints = { + name nullable: false + } +} \ No newline at end of file diff --git a/grails-test-examples/hibernate-integration-tests/src/integration-test/groovy/functional/tests/WithSessionIntegrationSpec.groovy b/grails-test-examples/hibernate-integration-tests/src/integration-test/groovy/functional/tests/WithSessionIntegrationSpec.groovy new file mode 100644 index 00000000000..6d206ea7822 --- /dev/null +++ b/grails-test-examples/hibernate-integration-tests/src/integration-test/groovy/functional/tests/WithSessionIntegrationSpec.groovy @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package functional.tests + +import grails.testing.mixin.integration.Integration +import grails.testing.mixin.integration.WithSession +import grails.gorm.transactions.Transactional +import org.hibernate.SessionFactory +import spock.lang.Specification +import org.springframework.beans.factory.annotation.Autowired + +/** + * Test demonstrating the @WithSession annotation functionality. + * This test class shows how to have a Hibernate session bound without a transaction, + * matching the runtime OISV behavior. + */ +@Integration +@WithSession +class WithSessionIntegrationSpec extends Specification { + + @Autowired + SessionFactory sessionFactory + + @Autowired + TestService testService + + // Set transactional to false to prevent automatic transaction wrapping + static transactional = false + + void "test that session is bound without transaction"() { + given: "A test domain object" + def testDomain = new TestDomain(name: "Test Session Binding") + + when: "We perform a SELECT operation (which requires session but not transaction)" + def count = TestDomain.count() + + then: "The operation succeeds because a session is bound" + count >= 0 + sessionFactory.currentSession != null + !sessionFactory.currentSession.transaction.active + } + + void "test that save without flush works with session only"() { + given: "A domain object" + def testDomain = new TestDomain(name: "Session Only Save") + + when: "We save without flush (no transaction required)" + testDomain.save() + + then: "Save succeeds because session is available" + testDomain.id != null + !sessionFactory.currentSession.transaction.active + } + + void "test that save with flush fails without transaction"() { + given: "A domain object" + def testDomain = new TestDomain(name: "Flush Test") + + when: "We try to save with flush (requires transaction)" + testDomain.save(flush: true) + + then: "An exception is thrown because no transaction is active" + thrown(Exception) + } + + void "test service method without @Transactional behaves correctly"() { + when: "Calling a non-transactional service method that does SELECT" + def result = testService.performNonTransactionalRead() + + then: "The method succeeds because session is bound" + result != null + !sessionFactory.currentSession.transaction.active + } + + void "test service method with @Transactional creates transaction"() { + when: "Calling a transactional service method" + def result = testService.performTransactionalOperation() + + then: "The method runs in a transaction" + result != null + // Transaction will be committed after method completes + } +} + +// Test domain class +class TestDomain { + String name + + static constraints = { + name nullable: false + } +} + +// Test service class +@grails.gorm.services.Service(TestDomain) +abstract class TestService { + + // Non-transactional method - relies on session from OISV + def performNonTransactionalRead() { + return TestDomain.count() + } + + // Transactional method - creates its own transaction + @Transactional + def performTransactionalOperation() { + def domain = new TestDomain(name: "Transactional Save") + domain.save(flush: true) + return domain + } +} \ No newline at end of file diff --git a/grails-testing-support-core/build.gradle b/grails-testing-support-core/build.gradle index 4a816f0f60e..0f5b96da741 100644 --- a/grails-testing-support-core/build.gradle +++ b/grails-testing-support-core/build.gradle @@ -48,6 +48,7 @@ dependencies { api project(':grails-databinding') api project(':grails-datamapping-core') api project(':grails-test-core') + api('org.spockframework:spock-core') { transitive = false } api 'org.springframework.boot:spring-boot-test' api('org.spockframework:spock-spring') { transitive = false } api 'org.junit.jupiter:junit-jupiter-api' diff --git a/grails-testing-support-core/src/main/groovy/grails/testing/mixin/integration/WithSession.groovy b/grails-testing-support-core/src/main/groovy/grails/testing/mixin/integration/WithSession.groovy new file mode 100644 index 00000000000..e594c7a6ac3 --- /dev/null +++ b/grails-testing-support-core/src/main/groovy/grails/testing/mixin/integration/WithSession.groovy @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.testing.mixin.integration + +import org.codehaus.groovy.transform.GroovyASTTransformationClass +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +/** + * Annotation to bind a Hibernate session to the test thread without starting a transaction. + * This mimics the application runtime behavior where OISV (Open Session In View) provides + * a session but not a transaction. + * + * Use this annotation when you want to test service methods that rely on having a session + * but are not transactional themselves. This ensures test behavior matches runtime behavior. + * + * @author Grails Team + * @since 7.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.TYPE, ElementType.METHOD]) +@GroovyASTTransformationClass("org.grails.compiler.injection.testing.WithSessionTransformation") +public @interface WithSession { + + /** + * The datasource names to bind sessions for. + * If empty, sessions will be bound for all configured datasources. + */ + String[] datasources() default [] +} \ No newline at end of file diff --git a/grails-testing-support-core/src/main/groovy/org/grails/compiler/injection/testing/WithSessionTransformation.groovy b/grails-testing-support-core/src/main/groovy/org/grails/compiler/injection/testing/WithSessionTransformation.groovy new file mode 100644 index 00000000000..18e600fe8f9 --- /dev/null +++ b/grails-testing-support-core/src/main/groovy/org/grails/compiler/injection/testing/WithSessionTransformation.groovy @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.compiler.injection.testing + +import grails.testing.mixin.integration.WithSession +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.* +import org.codehaus.groovy.ast.expr.* +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.transform.ASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.grails.test.spock.WithSessionSpecExtension + +/** + * AST transformation for @WithSession annotation. + * Registers the Spock extension for handling session binding. + * + * @author Grails Team + * @since 7.1 + */ +@CompileStatic +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +class WithSessionTransformation implements ASTTransformation { + + static final ClassNode MY_TYPE = new ClassNode(WithSession) + private static final String SPEC_CLASS = "spock.lang.Specification" + + @Override + void visit(ASTNode[] nodes, SourceUnit source) { + if (!(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) { + throw new RuntimeException("Internal error: wrong types: ${nodes[0].getClass()} / ${nodes[1].getClass()}") + } + + AnnotatedNode parent = (AnnotatedNode) nodes[1] + AnnotationNode annotationNode = (AnnotationNode) nodes[0] + + if (MY_TYPE != annotationNode.classNode) { + return + } + + if (parent instanceof ClassNode) { + ClassNode classNode = (ClassNode) parent + + // For Spock specifications, the extension will handle it + if (isSubclassOf(classNode, SPEC_CLASS)) { + // Extension is registered via META-INF/services + return + } + + // For JUnit tests, add a property to signal session binding + addWithSessionProperty(classNode, annotationNode) + } else if (parent instanceof MethodNode) { + // For method-level annotations, we need to handle differently + // This would require more complex transformation + // For now, method-level annotations are handled by the Spock extension + } + } + + private void addWithSessionProperty(ClassNode classNode, AnnotationNode annotationNode) { + // Extract datasources from annotation + Expression datasourcesExpr = annotationNode.getMember("datasources") + + if (datasourcesExpr instanceof ListExpression) { + ListExpression listExpr = (ListExpression) datasourcesExpr + List datasources = [] + + for (Expression expr : listExpr.expressions) { + if (expr instanceof ConstantExpression) { + datasources << expr.value.toString() + } + } + + // Add a static property for the datasources + if (datasources) { + classNode.addProperty( + "withSession", + Modifier.STATIC | Modifier.PUBLIC, + ClassHelper.make(List), + new ListExpression(datasources.collect { new ConstantExpression(it) }) + ) + } else { + // Empty list means all datasources + classNode.addProperty( + "withSession", + Modifier.STATIC | Modifier.PUBLIC, + ClassHelper.BOOLEAN_TYPE, + new ConstantExpression(true) + ) + } + } else { + // No datasources specified means all datasources + classNode.addProperty( + "withSession", + Modifier.STATIC | Modifier.PUBLIC, + ClassHelper.BOOLEAN_TYPE, + new ConstantExpression(true) + ) + } + } + + private boolean isSubclassOf(ClassNode classNode, String superClassName) { + if (classNode == null) { + return false + } + + if (classNode.name == superClassName) { + return true + } + + return isSubclassOf(classNode.superClass, superClassName) + } +} \ No newline at end of file diff --git a/grails-testing-support-core/src/main/groovy/org/grails/test/spock/WithSessionSpecExtension.groovy b/grails-testing-support-core/src/main/groovy/org/grails/test/spock/WithSessionSpecExtension.groovy new file mode 100644 index 00000000000..cc9901e6931 --- /dev/null +++ b/grails-testing-support-core/src/main/groovy/org/grails/test/spock/WithSessionSpecExtension.groovy @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.test.spock + +import grails.testing.mixin.integration.WithSession +import grails.util.Holders +import groovy.transform.CompileStatic +import org.grails.test.support.GrailsTestSessionInterceptor +import org.spockframework.runtime.extension.IAnnotationDrivenExtension +import org.spockframework.runtime.extension.IMethodInterceptor +import org.spockframework.runtime.extension.IMethodInvocation +import org.spockframework.runtime.model.FeatureInfo +import org.spockframework.runtime.model.SpecInfo +import org.springframework.context.ApplicationContext + +/** + * Spock extension for @WithSession annotation. + * Binds Hibernate sessions without transactions for testing. + */ +@CompileStatic +class WithSessionSpecExtension implements IAnnotationDrivenExtension { + + @Override + void visitSpecAnnotation(WithSession annotation, SpecInfo spec) { + final context = Holders.getApplicationContext() + if (context) { + for (FeatureInfo info : spec.getAllFeatures()) { + info.addInterceptor(new WithSessionMethodInterceptor(context, annotation)) + } + } + } + + @Override + void visitFeatureAnnotation(WithSession annotation, FeatureInfo feature) { + final context = Holders.getApplicationContext() + if (context) { + feature.addInterceptor(new WithSessionMethodInterceptor(context, annotation)) + } + } + + @CompileStatic + static class WithSessionMethodInterceptor implements IMethodInterceptor { + private final ApplicationContext applicationContext + private final WithSession annotation + + WithSessionMethodInterceptor(ApplicationContext applicationContext, WithSession annotation) { + this.applicationContext = applicationContext + this.annotation = annotation + } + + @Override + void intercept(IMethodInvocation invocation) { + final instance = invocation.instance ?: invocation.sharedInstance + if (instance && applicationContext) { + GrailsTestSessionInterceptor sessionInterceptor = new GrailsTestSessionInterceptor(applicationContext) + + // Configure datasources from annotation + if (annotation.datasources().length > 0) { + // Create a simple map with datasources list + def testConfig = [withSession: annotation.datasources() as List] + sessionInterceptor.shouldBindSessions(testConfig) + } else { + // Bind all datasources + def testConfig = [withSession: true] + sessionInterceptor.shouldBindSessions(testConfig) + } + + try { + sessionInterceptor.init() + invocation.proceed() + } finally { + sessionInterceptor.destroy() + } + } else { + invocation.proceed() + } + } + } +} \ No newline at end of file diff --git a/grails-testing-support-core/src/main/groovy/org/grails/test/support/GrailsTestSessionInterceptor.groovy b/grails-testing-support-core/src/main/groovy/org/grails/test/support/GrailsTestSessionInterceptor.groovy new file mode 100644 index 00000000000..a7a14bbb453 --- /dev/null +++ b/grails-testing-support-core/src/main/groovy/org/grails/test/support/GrailsTestSessionInterceptor.groovy @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.test.support + +import grails.testing.mixin.integration.WithSession +import org.grails.datastore.mapping.core.connections.ConnectionSource +import org.hibernate.FlushMode +import org.hibernate.Session +import org.hibernate.SessionFactory +import org.springframework.context.ApplicationContext +import org.springframework.orm.hibernate5.SessionHolder +import org.springframework.transaction.support.TransactionSynchronizationManager +import groovy.transform.CompileStatic +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Binds Hibernate sessions to the test thread without starting transactions. + * This mimics the OISV (Open Session In View) pattern used in running applications. + */ +@CompileStatic +class GrailsTestSessionInterceptor { + + private static final Logger LOG = LoggerFactory.getLogger(GrailsTestSessionInterceptor) + private static final String WITH_SESSION = "withSession" + + private final ApplicationContext applicationContext + private final Map sessionFactories = [:] + private final Map sessionHolders = [:] + private final Set datasourcesToBind = [] as Set + + GrailsTestSessionInterceptor(ApplicationContext applicationContext) { + this.applicationContext = applicationContext + initializeSessionFactories() + } + + private void initializeSessionFactories() { + def datasourceNames = [] + + if (applicationContext.containsBean('sessionFactory')) { + datasourceNames << ConnectionSource.DEFAULT + } + + // Check for additional datasources by looking for sessionFactory beans + String[] beanNames = applicationContext.getBeanNamesForType(SessionFactory) + for (String beanName : beanNames) { + if (beanName.startsWith('sessionFactory_')) { + String datasourceName = beanName.substring('sessionFactory_'.length()) + datasourceNames << datasourceName + } + } + + for (datasourceName in datasourceNames) { + boolean isDefault = datasourceName == ConnectionSource.DEFAULT + String suffix = isDefault ? '' : '_' + datasourceName + String beanName = "sessionFactory$suffix" + + if (applicationContext.containsBean(beanName)) { + sessionFactories[datasourceName] = applicationContext.getBean(beanName, SessionFactory) + } + } + } + + /** + * Determines if sessions should be bound for the given test instance. + */ + boolean shouldBindSessions(Object test) { + if (!test) return false + + // Check for class-level annotation + WithSession classAnnotation = test.class.getAnnotation(WithSession) + if (classAnnotation) { + configureDatasources(classAnnotation) + return true + } + + // Check for property-based configuration + def value = null + if (test instanceof Map) { + value = test[WITH_SESSION] + } else if (test.metaClass.hasProperty(test, WITH_SESSION)) { + value = test[WITH_SESSION] + } + + if (value instanceof Boolean && value) { + // Bind sessions for all datasources if withSession = true + datasourcesToBind.addAll(sessionFactories.keySet()) + return true + } else if (value instanceof List) { + // Bind sessions for specific datasources + datasourcesToBind.addAll(value as List) + return true + } + + return false + } + + private void configureDatasources(WithSession annotation) { + if (annotation.datasources().length > 0) { + datasourcesToBind.addAll(annotation.datasources()) + } else { + // If no specific datasources specified, bind all + datasourcesToBind.addAll(sessionFactories.keySet()) + } + } + + /** + * Binds Hibernate sessions without transactions. + */ + void init() { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.initSynchronization() + } + + for (datasourceName in datasourcesToBind) { + SessionFactory sessionFactory = sessionFactories[datasourceName] + if (!sessionFactory) { + LOG.warn("SessionFactory not found for datasource: $datasourceName") + continue + } + + if (TransactionSynchronizationManager.hasResource(sessionFactory)) { + LOG.debug("Session already bound for datasource: $datasourceName") + continue + } + + Session session = sessionFactory.openSession() + // Set flush mode to MANUAL to match OISV behavior + session.flushMode = FlushMode.MANUAL + + SessionHolder holder = new SessionHolder(session) + TransactionSynchronizationManager.bindResource(sessionFactory, holder) + sessionHolders[datasourceName] = holder + + LOG.debug("Bound Hibernate session for datasource: $datasourceName") + } + } + + /** + * Unbinds and closes the sessions. + */ + void destroy() { + for (Map.Entry entry : sessionHolders.entrySet()) { + String datasourceName = entry.key + SessionHolder holder = entry.value + SessionFactory sessionFactory = sessionFactories[datasourceName] + + if (sessionFactory && TransactionSynchronizationManager.hasResource(sessionFactory)) { + TransactionSynchronizationManager.unbindResource(sessionFactory) + + try { + Session session = holder.session + if (session?.isOpen()) { + session.close() + } + LOG.debug("Closed Hibernate session for datasource: $datasourceName") + } catch (Exception e) { + LOG.error("Error closing Hibernate session for datasource: $datasourceName", e) + } + } + } + + sessionHolders.clear() + datasourcesToBind.clear() + + if (TransactionSynchronizationManager.isSynchronizationActive() && + TransactionSynchronizationManager.resourceMap.isEmpty()) { + TransactionSynchronizationManager.clearSynchronization() + } + } +} \ No newline at end of file diff --git a/grails-testing-support-core/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension b/grails-testing-support-core/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension index 41355544117..0b928bb6d3b 100644 --- a/grails-testing-support-core/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension +++ b/grails-testing-support-core/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension @@ -1 +1 @@ -org.grails.testing.spock.TestingSupportExtension +org.grails.test.spock.WithSessionSpecExtension \ No newline at end of file