This is an automated email from the ASF dual-hosted git repository. jdaugherty pushed a commit to branch feature/dbcleanup-end-of-spec in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 43be3c18590490f25a1dbdbdd46266756f27963c Author: James Daugherty <[email protected]> AuthorDate: Wed Mar 11 10:07:05 2026 -0400 feature - support cleaning up only after the spec ends --- grails-testing-support-dbcleanup-core/README.md | 58 +++++++ .../testing/cleanup/core/DatabaseCleanup.groovy | 19 +++ .../cleanup/core/DatabaseCleanupExtension.groovy | 38 ++++- .../cleanup/core/DatabaseCleanupInterceptor.groovy | 40 ++++- .../core/DatabaseCleanupExtensionSpec.groovy | 64 +++++++ .../core/DatabaseCleanupInterceptorSpec.groovy | 184 +++++++++++++++++++-- 6 files changed, 386 insertions(+), 17 deletions(-) diff --git a/grails-testing-support-dbcleanup-core/README.md b/grails-testing-support-dbcleanup-core/README.md index db1b6157c9..5ed80a37cb 100644 --- a/grails-testing-support-dbcleanup-core/README.md +++ b/grails-testing-support-dbcleanup-core/README.md @@ -18,6 +18,64 @@ limitations under the License. Provides the core database cleanup testing support for Grails integration tests, including the `@DatabaseCleanup` annotation and the `DatabaseCleaner` SPI. +### Usage + +#### Basic Usage + +Apply `@DatabaseCleanup` at the class level to clean all datasources after every test method: + +```groovy +@DatabaseCleanup +class MyIntegrationSpec extends Specification { ... } +``` + +Apply at the method level to clean only after specific test methods: + +```groovy +class MyIntegrationSpec extends Specification { + @DatabaseCleanup + void "test that modifies the database"() { ... } +} +``` + +#### Specifying Datasources + +Clean only specific datasources (auto-discover cleaners): + +```groovy +@DatabaseCleanup(['dataSource', 'dataSource_secondary']) +class MySpec extends Specification { ... } +``` + +Clean specific datasources with explicit cleaner types: + +```groovy +@DatabaseCleanup(['dataSource:h2', 'dataSource_pg:postgresql']) +class MySpec extends Specification { ... } +``` + +#### Deferred Cleanup (cleanupAfterSpec) + +By default, database cleanup runs after each test method. Use `cleanupAfterSpec = true` to defer cleanup until after the entire spec finishes. This is useful when test methods build on shared data or when per-test cleanup is too expensive: + +```groovy +@DatabaseCleanup(cleanupAfterSpec = true) +class MySpec extends Specification { + void "first test creates data"() { ... } + void "second test uses data from first test"() { ... } + // Database is cleaned once after both tests complete +} +``` + +**Note:** `cleanupAfterSpec` is only valid on class-level annotations. Using it on a method-level annotation will throw an `IllegalStateException`. + +#### Custom ApplicationContext Resolver + +```groovy +@DatabaseCleanup(resolver = MyCustomResolver) +class MySpec extends Specification { ... } +``` + ### Supported Database Implementations Database cleanup is automatically discovered and applied based on your datasource configuration. The following database implementations are available: diff --git a/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanup.groovy b/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanup.groovy index 3163daac31..68c8ea2ba7 100644 --- a/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanup.groovy +++ b/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanup.groovy @@ -75,6 +75,10 @@ import java.lang.annotation.Target * // Use a custom ApplicationContext resolver * @DatabaseCleanup(resolver = MyCustomResolver) * class MySpec extends Specification { ... } + * + * // Clean only once after the entire spec finishes (class-level only) + * @DatabaseCleanup(cleanupAfterSpec = true) + * class MySpec extends Specification { ... } * </pre> */ @Retention(RetentionPolicy.RUNTIME) @@ -103,4 +107,19 @@ import java.lang.annotation.Target * @return the resolver class to use */ Class<? extends ApplicationContextResolver> resolver() default DefaultApplicationContextResolver + + /** + * When {@code true}, database cleanup is deferred until after the entire spec finishes + * ({@code cleanupSpec}) instead of running after each individual test method. + * + * <p>This attribute is only valid on class-level annotations. Setting it to {@code true} + * on a method-level annotation will result in an {@link IllegalStateException} at + * spec visit time.</p> + * + * <p>This is useful for specs where test methods build on each other's data, or where + * per-test cleanup is too expensive and a single cleanup at the end is sufficient.</p> + * + * @return {@code true} to defer cleanup until after the spec finishes; defaults to {@code false} + */ + boolean cleanupAfterSpec() default false } diff --git a/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtension.groovy b/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtension.groovy index b50524f8d9..f2add1f414 100644 --- a/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtension.groovy +++ b/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtension.groovy @@ -109,10 +109,17 @@ class DatabaseCleanupExtension implements IGlobalExtension { def annotation = spec.getAnnotation(DatabaseCleanup) def mapping = DatasourceCleanupMapping.parse(annotation.value()) def resolver = createResolver(annotation.resolver()) - def interceptor = new DatabaseCleanupInterceptor(context, true, mapping, resolver) + boolean afterSpec = annotation.cleanupAfterSpec() + def interceptor = new DatabaseCleanupInterceptor(context, true, afterSpec, mapping, resolver) spec.addSetupInterceptor(interceptor) spec.addCleanupInterceptor(interceptor) - log.debug('Registered DatabaseCleanupInterceptor for spec: {} (class-level)', spec.name) + if (afterSpec) { + spec.addCleanupSpecInterceptor(interceptor) + log.debug('Registered DatabaseCleanupInterceptor for spec: {} (class-level, cleanupAfterSpec)', spec.name) + } + else { + log.debug('Registered DatabaseCleanupInterceptor for spec: {} (class-level)', spec.name) + } return } @@ -128,12 +135,15 @@ class DatabaseCleanupExtension implements IGlobalExtension { } if (hasMethodAnnotation) { + // Validate that no method-level annotation uses cleanupAfterSpec = true + validateNoMethodLevelCleanupAfterSpec(spec) + // For method-level, pass a clean-all mapping as default; the interceptor reads // each method's own annotation at runtime. // Use the default resolver; the interceptor will read the method-level resolver at runtime. def defaultMapping = DatasourceCleanupMapping.parse(new String[0]) def defaultResolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, false, defaultMapping, defaultResolver) + def interceptor = new DatabaseCleanupInterceptor(context, false, false, defaultMapping, defaultResolver) spec.addSetupInterceptor(interceptor) spec.addCleanupInterceptor(interceptor) log.debug('Registered DatabaseCleanupInterceptor for spec: {} (method-level)', spec.name) @@ -192,6 +202,28 @@ class DatabaseCleanupExtension implements IGlobalExtension { } } + /** + * Validates that no method-level {@link DatabaseCleanup} annotation has + * {@code cleanupAfterSpec = true}. This attribute is only valid at the class level. + * + * @param spec the spec to validate + * @throws IllegalStateException if a method-level annotation has cleanupAfterSpec = true + */ + private static void validateNoMethodLevelCleanupAfterSpec(SpecInfo spec) { + def invalid = spec.features.find { + def method = it.featureMethod + method.isAnnotationPresent(DatabaseCleanup) && + method.getAnnotation(DatabaseCleanup).cleanupAfterSpec() + } + if (invalid) { + throw new IllegalStateException( + "@DatabaseCleanup(cleanupAfterSpec = true) on method '${invalid.featureMethod.name}' " + + "in ${spec.name} is not valid. The cleanupAfterSpec attribute " + + 'can only be used on class-level @DatabaseCleanup annotations.' + ) + } + } + /** * Checks whether the spec has any {@link DatabaseCleanup} annotation, either at the * class level or on any feature method. diff --git a/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptor.groovy b/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptor.groovy index 5e2054d389..6f2c03980d 100644 --- a/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptor.groovy +++ b/grails-testing-support-dbcleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptor.groovy @@ -44,6 +44,7 @@ class DatabaseCleanupInterceptor extends AbstractMethodInterceptor { private final DatabaseCleanupContext context private final boolean classLevelCleanup + private final boolean cleanupAfterSpec private final DatasourceCleanupMapping mapping private final ApplicationContextResolver resolver @@ -51,6 +52,8 @@ class DatabaseCleanupInterceptor extends AbstractMethodInterceptor { * @param context the cleanup context containing the cleaners and configuration * @param classLevelCleanup if true, cleanup runs after every test method; * if false, only after methods annotated with @DatabaseCleanup + * @param cleanupAfterSpec if true, cleanup is deferred until after the entire spec + * finishes (cleanupSpec phase) instead of after each test method * @param mapping the parsed datasource-to-type mapping from the class-level annotation; * for method-level cleanup, the method's own annotation values are parsed at runtime * @param resolver the strategy for resolving the ApplicationContext from test instances @@ -58,11 +61,13 @@ class DatabaseCleanupInterceptor extends AbstractMethodInterceptor { DatabaseCleanupInterceptor( DatabaseCleanupContext context, boolean classLevelCleanup, + boolean cleanupAfterSpec, DatasourceCleanupMapping mapping, ApplicationContextResolver resolver ) { this.context = context this.classLevelCleanup = classLevelCleanup + this.cleanupAfterSpec = cleanupAfterSpec this.mapping = mapping this.resolver = resolver } @@ -84,18 +89,26 @@ class DatabaseCleanupInterceptor extends AbstractMethodInterceptor { } finally { try { - def methodMapping = invocation. + def methodAnnotated = invocation. feature?. featureMethod?. isAnnotationPresent(DatabaseCleanup) - if (!classLevelCleanup && !methodMapping) { + + // When cleanupAfterSpec is true, skip class-level per-test cleanup — + // it will happen in cleanupSpec. But if the individual method has its own + // @DatabaseCleanup annotation, still honor that and clean up after this method. + if (cleanupAfterSpec && !methodAnnotated) { + return + } + + if (!classLevelCleanup && !methodAnnotated) { return } log.debug( 'Performing database cleanup after test method: {}', invocation.feature?.name ?: 'unknown' ) - def selectedMapping = methodMapping ? + def selectedMapping = methodAnnotated ? getMethodMapping(invocation) : mapping long startTime = System.currentTimeMillis() @@ -108,6 +121,27 @@ class DatabaseCleanupInterceptor extends AbstractMethodInterceptor { } } + @Override + void interceptCleanupSpecMethod(IMethodInvocation invocation) throws Throwable { + try { + invocation.proceed() + } + finally { + try { + log.debug( + 'Performing database cleanup after spec: {}', + invocation.spec?.name ?: 'unknown' + ) + long startTime = System.currentTimeMillis() + def stats = context.performCleanup(mapping) + logStats(stats, startTime) + } + finally { + TestContextHolderListener.CURRENT.remove() + } + } + } + /** * Gets the parsed mapping from the method-level @DatabaseCleanup annotation. */ diff --git a/grails-testing-support-dbcleanup-core/src/test/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtensionSpec.groovy b/grails-testing-support-dbcleanup-core/src/test/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtensionSpec.groovy index 90dd3eb7f0..ca585762d7 100644 --- a/grails-testing-support-dbcleanup-core/src/test/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtensionSpec.groovy +++ b/grails-testing-support-dbcleanup-core/src/test/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtensionSpec.groovy @@ -183,6 +183,7 @@ class DatabaseCleanupExtensionSpec extends Specification { def annotatedMethod = MethodAnnotatedSpec.getDeclaredMethod('annotatedMethod') def methodInfo = Mock(MethodInfo) { isAnnotationPresent(DatabaseCleanup) >> true + getAnnotation(DatabaseCleanup) >> annotatedMethod.getAnnotation(DatabaseCleanup) getReflection() >> annotatedMethod } def feature = Mock(FeatureInfo) { @@ -206,6 +207,59 @@ class DatabaseCleanupExtensionSpec extends Specification { 0 * spec.addCleanupSpecInterceptor(_) } + def "visitSpec registers cleanupSpec interceptor when cleanupAfterSpec is true"() { + given: + def extension = createExtensionWithCleaner() + + def spec = Mock(SpecInfo) { + isAnnotationPresent(SpringBootTest) >> true + getAnnotation(SpringBootTest) >> CleanupAfterSpecClassSpec.getAnnotation(SpringBootTest) + isAnnotationPresent(DatabaseCleanup) >> true + getAnnotation(DatabaseCleanup) >> CleanupAfterSpecClassSpec.getAnnotation(DatabaseCleanup) + } + + when: + extension.visitSpec(spec) + + then: + 1 * spec.addSetupInterceptor(_ as DatabaseCleanupInterceptor) + 1 * spec.addCleanupInterceptor(_ as DatabaseCleanupInterceptor) + 1 * spec.addCleanupSpecInterceptor(_ as DatabaseCleanupInterceptor) + } + + def "visitSpec throws when method-level @DatabaseCleanup has cleanupAfterSpec = true"() { + given: + def extension = createExtensionWithCleaner() + + def annotatedMethod = MethodAnnotatedWithCleanupAfterSpec.getDeclaredMethod('annotatedMethod') + def methodAnnotation = annotatedMethod.getAnnotation(DatabaseCleanup) + def methodInfo = Mock(MethodInfo) { + isAnnotationPresent(DatabaseCleanup) >> true + getAnnotation(DatabaseCleanup) >> methodAnnotation + getName() >> 'annotatedMethod' + } + def feature = Mock(FeatureInfo) { + getFeatureMethod() >> methodInfo + } + + def spec = Mock(SpecInfo) { + isAnnotationPresent(SpringBootTest) >> true + getAnnotation(SpringBootTest) >> MethodAnnotatedWithCleanupAfterSpec.getAnnotation(SpringBootTest) + isAnnotationPresent(DatabaseCleanup) >> false + getReflection() >> MethodAnnotatedWithCleanupAfterSpec + getFeatures() >> [feature] + getName() >> 'MethodAnnotatedWithCleanupAfterSpec' + } + + when: + extension.visitSpec(spec) + + then: + def ex = thrown(IllegalStateException) + ex.message.contains('cleanupAfterSpec') + ex.message.contains('class-level') + } + def "visitSpec skips spec with no annotations at all"() { given: def extension = createExtensionWithCleaner() @@ -379,4 +433,14 @@ class DatabaseCleanupExtensionSpec extends Specification { void featureMethod() {} } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @DatabaseCleanup(cleanupAfterSpec = true) + static class CleanupAfterSpecClassSpec {} + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + static class MethodAnnotatedWithCleanupAfterSpec { + @DatabaseCleanup(cleanupAfterSpec = true) + void annotatedMethod() {} + } } diff --git a/grails-testing-support-dbcleanup-core/src/test/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptorSpec.groovy b/grails-testing-support-dbcleanup-core/src/test/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptorSpec.groovy index f99f6a08e5..066ceef490 100644 --- a/grails-testing-support-dbcleanup-core/src/test/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptorSpec.groovy +++ b/grails-testing-support-dbcleanup-core/src/test/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptorSpec.groovy @@ -48,7 +48,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, true, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, true, false, mapping, resolver) def invocation = Mock(IMethodInvocation) { getInstance() >> new InstanceWithAppCtx(applicationContext: appCtx) @@ -74,7 +74,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, false, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, false, false, mapping, resolver) def unannotatedMethod = NonAnnotatedTestClass.getDeclaredMethod('someTest') def methodInfo = Mock(MethodInfo) { @@ -113,7 +113,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, false, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, false, false, mapping, resolver) def annotatedMethod = AnnotatedMethodTestClass.getDeclaredMethod('annotatedTest') def annotation = annotatedMethod.getAnnotation(DatabaseCleanup) @@ -156,7 +156,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, false, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, false, false, mapping, resolver) def annotatedMethod = AnnotatedMethodWithDatasource.getDeclaredMethod('annotatedTest') def annotation = annotatedMethod.getAnnotation(DatabaseCleanup) @@ -200,7 +200,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, false, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, false, false, mapping, resolver) def annotatedMethod = AnnotatedMethodWithExplicitType.getDeclaredMethod('annotatedTest') def annotation = annotatedMethod.getAnnotation(DatabaseCleanup) @@ -248,7 +248,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, true, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, true, false, mapping, resolver) def invocation = Mock(IMethodInvocation) { getFeature() >> Mock(FeatureInfo) { @@ -281,7 +281,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def resolver = Mock(ApplicationContextResolver) { resolve(_) >> null } - def interceptor = new DatabaseCleanupInterceptor(context, true, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, true, false, mapping, resolver) def invocation = Mock(IMethodInvocation) { getInstance() >> new InstanceWithNoContext() @@ -322,7 +322,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, true, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, true, false, mapping, resolver) def invocation = Mock(IMethodInvocation) { getFeature() >> Mock(FeatureInfo) { @@ -361,7 +361,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, true, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, true, false, mapping, resolver) def invocation = Mock(IMethodInvocation) { getInstance() >> new InstanceWithAppCtx(applicationContext: appCtx) @@ -419,7 +419,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, true, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, true, false, mapping, resolver) def invocation = Mock(IMethodInvocation) { getInstance() >> new InstanceWithAppCtx(applicationContext: appCtx) @@ -463,7 +463,7 @@ class DatabaseCleanupInterceptorSpec extends Specification { def mapping = DatasourceCleanupMapping.parse(new String[0]) def resolver = new DefaultApplicationContextResolver() - def interceptor = new DatabaseCleanupInterceptor(context, true, mapping, resolver) + def interceptor = new DatabaseCleanupInterceptor(context, true, false, mapping, resolver) def testFailure = new RuntimeException('Test failed') def invocation = Mock(IMethodInvocation) { @@ -485,6 +485,168 @@ class DatabaseCleanupInterceptorSpec extends Specification { ex.is(testFailure) } + def "interceptCleanupMethod skips cleanup when cleanupAfterSpec is true"() { + given: + def dataSource = Mock(DataSource) + def appCtx = Mock(ApplicationContext) { + getBeansOfType(DataSource) >> ['dataSource': dataSource] + } + def cleaner = Mock(DatabaseCleaner) { + databaseType() >> 'h2' + supports(dataSource) >> true + } + def context = new DatabaseCleanupContext([cleaner]) + context.applicationContext = appCtx + + def mapping = DatasourceCleanupMapping.parse(new String[0]) + def resolver = new DefaultApplicationContextResolver() + def interceptor = new DatabaseCleanupInterceptor(context, true, true, mapping, resolver) + + def unannotatedMethod = NonAnnotatedTestClass.getDeclaredMethod('someTest') + def methodInfo = Mock(MethodInfo) { + isAnnotationPresent(DatabaseCleanup) >> false + } + def feature = Mock(FeatureInfo) { + getFeatureMethod() >> methodInfo + getName() >> 'test feature' + } + def invocation = Mock(IMethodInvocation) { + getInstance() >> new InstanceWithAppCtx(applicationContext: appCtx) + getFeature() >> feature + } + + when: + interceptor.interceptCleanupMethod(invocation) + + then: 'invocation is proceeded' + 1 * invocation.proceed() + + and: 'no cleanup is performed (deferred to cleanupSpec)' + 0 * cleaner.cleanup(_, _) + } + + def "interceptCleanupMethod still runs cleanup for method-level annotation even when cleanupAfterSpec is true"() { + given: + def dataSource = Mock(DataSource) + def appCtx = Mock(ApplicationContext) { + getBeansOfType(DataSource) >> ['dataSource': dataSource] + } + def cleaner = Mock(DatabaseCleaner) { + databaseType() >> 'h2' + supports(dataSource) >> true + cleanup(appCtx, dataSource) >> new DatabaseCleanupStats() + } + def context = new DatabaseCleanupContext([cleaner]) + context.applicationContext = appCtx + + def mapping = DatasourceCleanupMapping.parse(new String[0]) + def resolver = new DefaultApplicationContextResolver() + def interceptor = new DatabaseCleanupInterceptor(context, true, true, mapping, resolver) + + def annotatedMethod = AnnotatedMethodTestClass.getDeclaredMethod('annotatedTest') + def annotation = annotatedMethod.getAnnotation(DatabaseCleanup) + def methodInfo = Mock(MethodInfo) { + isAnnotationPresent(DatabaseCleanup) >> true + getAnnotation(DatabaseCleanup) >> annotation + getReflection() >> annotatedMethod + } + def feature = Mock(FeatureInfo) { + getFeatureMethod() >> methodInfo + getName() >> 'annotatedTest' + } + def invocation = Mock(IMethodInvocation) { + getInstance() >> new InstanceWithAppCtx(applicationContext: appCtx) + getFeature() >> feature + } + + when: + interceptor.interceptCleanupMethod(invocation) + + then: 'invocation is proceeded' + 1 * invocation.proceed() + + and: 'cleanup IS performed because the method has its own @DatabaseCleanup' + 1 * cleaner.cleanup(appCtx, dataSource) >> new DatabaseCleanupStats() + } + + def "interceptCleanupSpecMethod performs cleanup when cleanupAfterSpec is true"() { + given: + def dataSource = Mock(DataSource) + def appCtx = Mock(ApplicationContext) { + getBeansOfType(DataSource) >> ['dataSource': dataSource] + } + def cleaner = Mock(DatabaseCleaner) { + databaseType() >> 'h2' + supports(dataSource) >> true + cleanup(appCtx, dataSource) >> new DatabaseCleanupStats() + } + def context = new DatabaseCleanupContext([cleaner]) + context.applicationContext = appCtx + + def mapping = DatasourceCleanupMapping.parse(new String[0]) + def resolver = new DefaultApplicationContextResolver() + def interceptor = new DatabaseCleanupInterceptor(context, true, true, mapping, resolver) + + def specInfo = Mock(org.spockframework.runtime.model.SpecInfo) { + getName() >> 'MySpec' + } + def invocation = Mock(IMethodInvocation) { + getSpec() >> specInfo + } + + when: + interceptor.interceptCleanupSpecMethod(invocation) + + then: 'invocation is proceeded' + 1 * invocation.proceed() + + and: 'cleanup is performed' + 1 * cleaner.cleanup(appCtx, dataSource) >> new DatabaseCleanupStats() + } + + def "interceptCleanupSpecMethod clears ThreadLocal after cleanup"() { + given: + def dataSource = Mock(DataSource) + def appCtx = Mock(ApplicationContext) { + getBeansOfType(DataSource) >> ['dataSource': dataSource] + } + def testContext = Mock(TestContext) { + getApplicationContext() >> appCtx + } + TestContextHolderListener.CURRENT.set(testContext) + + def cleaner = Mock(DatabaseCleaner) { + databaseType() >> 'h2' + supports(dataSource) >> true + cleanup(appCtx, dataSource) >> new DatabaseCleanupStats() + } + def context = new DatabaseCleanupContext([cleaner]) + context.applicationContext = appCtx + + def mapping = DatasourceCleanupMapping.parse(new String[0]) + def resolver = new DefaultApplicationContextResolver() + def interceptor = new DatabaseCleanupInterceptor(context, true, true, mapping, resolver) + + def specInfo = Mock(org.spockframework.runtime.model.SpecInfo) { + getName() >> 'MySpec' + } + def invocation = Mock(IMethodInvocation) { + getSpec() >> specInfo + } + + when: + interceptor.interceptCleanupSpecMethod(invocation) + + then: + 1 * invocation.proceed() + + and: 'ThreadLocal is cleared after cleanup' + TestContextHolderListener.CURRENT.get() == null + + cleanup: + TestContextHolderListener.CURRENT.remove() + } + // --- Helper classes --- static class InstanceWithAppCtx {
