Skip to content

Commit bf03c95

Browse files
authored
feature: SpringBoot test support (#768)
1 parent 2dec48b commit bf03c95

File tree

16 files changed

+451
-16
lines changed

16 files changed

+451
-16
lines changed

core/flamingock-test-support/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
dependencies {
2-
api(project(":core:flamingock-core"))
2+
implementation(project(":core:flamingock-core"))
33

44

55
testImplementation(platform("org.junit:junit-bom:5.10.0"))

core/flamingock-test-support/src/main/java/io/flamingock/support/FlamingockTestSupport.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private FlamingockTestSupport() {
8484
* @return the {@link GivenStage} for defining initial audit state preconditions
8585
* @throws NullPointerException if {@code builder} is {@code null}
8686
*/
87-
public static GivenStage given(AbstractChangeRunnerBuilder<?, ?> builder) {
87+
public static GivenStage givenBuilder(AbstractChangeRunnerBuilder<?, ?> builder) {
8888
if (builder == null) {
8989
throw new NullPointerException("builder must not be null");
9090
}

core/flamingock-test-support/src/main/java/io/flamingock/support/stages/GivenStage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
* // ... assertions
4848
* }</pre>
4949
*
50-
* @see FlamingockTestSupport#given(io.flamingock.internal.core.builder.AbstractChangeRunnerBuilder)
50+
* @see FlamingockTestSupport#givenBuilder(io.flamingock.internal.core.builder.AbstractChangeRunnerBuilder)
5151
* @see WhenStage
5252
* @see AuditEntryDefinition
5353
*/

core/flamingock-test-support/src/test/java/io/flamingock/support/integration/FlamingockTestSupportIntegrationTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ void shouldExecuteNonTransactionalChange() {
5050
NonTransactionalTargetSystem targetSystem = new NonTransactionalTargetSystem("kafka");
5151

5252
FlamingockTestSupport
53-
.given(testKit.createBuilder().addTargetSystem(targetSystem))
53+
.givenBuilder(testKit.createBuilder().addTargetSystem(targetSystem))
5454
.whenRun()
5555
.thenExpectAuditSequenceStrict(
5656
APPLIED(_001__SimpleNonTransactionalChange.class)
@@ -75,7 +75,7 @@ void shouldVerifyMultipleChangesInSequence() {
7575
);
7676

7777
FlamingockTestSupport
78-
.given(testKit.createBuilder()
78+
.givenBuilder(testKit.createBuilder()
7979
.addTargetSystem(new NonTransactionalTargetSystem("okta"))
8080
.addTargetSystem(new NonTransactionalTargetSystem("elasticsearch"))
8181
.addTargetSystem(new NonTransactionalTargetSystem("s3")))
@@ -101,7 +101,7 @@ void shouldVerifyFailingTransactionalChangeTriggersRollback() {
101101
);
102102

103103
FlamingockTestSupport
104-
.given(testKit.createBuilder()
104+
.givenBuilder(testKit.createBuilder()
105105
.addTargetSystem(new NonTransactionalTargetSystem("salesforce"))
106106
.addTargetSystem(new NonTransactionalTargetSystem("okta"))
107107
.addTargetSystem(new NonTransactionalTargetSystem("elasticsearch"))
@@ -129,7 +129,7 @@ void shouldVerifyAlreadyAppliedChangesAreSkipped() {
129129
);
130130

131131
FlamingockTestSupport
132-
.given(testKit.createBuilder()
132+
.givenBuilder(testKit.createBuilder()
133133
.addTargetSystem(new NonTransactionalTargetSystem("stripe-api"))
134134
.addTargetSystem(new NonTransactionalTargetSystem("okta"))
135135
.addTargetSystem(new NonTransactionalTargetSystem("elasticsearch"))
@@ -164,7 +164,7 @@ void shouldVerifyDependencyInjectionInRollbackForNonTransactionalChanges() {
164164
);
165165

166166
FlamingockTestSupport
167-
.given(testKit.createBuilder().addTargetSystem(targetSystem))
167+
.givenBuilder(testKit.createBuilder().addTargetSystem(targetSystem))
168168
.whenRun()
169169
.thenExpectException(PipelineExecutionException.class, ex -> {
170170
assertTrue(ex.getMessage().contains("Intentional failure"));
@@ -192,7 +192,7 @@ void shouldVerifyTransactionalChangeExecutesSuccessfully() {
192192
);
193193

194194
FlamingockTestSupport
195-
.given(testKit.createBuilder()
195+
.givenBuilder(testKit.createBuilder()
196196
.addTargetSystem(new NonTransactionalTargetSystem("okta"))
197197
.addTargetSystem(new NonTransactionalTargetSystem("elasticsearch"))
198198
.addTargetSystem(new NonTransactionalTargetSystem("s3")))

platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/FlamingockAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ public class FlamingockAutoConfiguration {
4848
@ConditionalOnExpression("'${flamingock.runner-type:ApplicationRunner}'.toLowerCase().equals('applicationrunner')")
4949
public ApplicationRunner applicationRunner(RunnerBuilder runnerBuilder,
5050
@Value("${flamingock.autorun:true}") boolean autoRun) {
51-
return SpringbootUtil.toApplicationRunner(runnerBuilder.build(), autoRun);
51+
return SpringbootUtil.toApplicationRunner(runnerBuilder, autoRun);
5252
}
5353

5454
@Bean("flamingock-runner")
5555
@Profile(Constants.NON_CLI_PROFILE)
5656
@ConditionalOnExpression("'${flamingock.runner-type:null}'.toLowerCase().equals('initializingbean')")
5757
public InitializingBean initializingBeanRunner(RunnerBuilder runnerBuilder,
5858
@Value("${flamingock.autorun:true}") boolean autoRun) {
59-
return SpringbootUtil.toInitializingBean(runnerBuilder.build(), autoRun);
59+
return SpringbootUtil.toInitializingBean(runnerBuilder, autoRun);
6060
}
6161

6262
@Bean("flamingock-builder")

platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootUtil.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package io.flamingock.springboot;
1717

1818
import io.flamingock.internal.core.runner.Runner;
19+
import io.flamingock.internal.core.runner.RunnerBuilder;
1920
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
2021
import org.slf4j.Logger;
2122
import org.springframework.beans.factory.InitializingBean;
@@ -28,16 +29,17 @@ public final class SpringbootUtil {
2829
private SpringbootUtil() {
2930
}
3031

31-
public static InitializingBean toInitializingBean(Runner runner, boolean autoRun) {
32-
return () -> runIfApply(runner, autoRun);
32+
public static InitializingBean toInitializingBean(RunnerBuilder runnerBuilder, boolean autoRun) {
33+
return () -> runIfApply(runnerBuilder, autoRun);
3334
}
3435

35-
public static ApplicationRunner toApplicationRunner(Runner runner, boolean autoRun) {
36-
return args -> runIfApply(runner, autoRun);
36+
public static ApplicationRunner toApplicationRunner(RunnerBuilder runnerBuilder, boolean autoRun) {
37+
return args -> runIfApply(runnerBuilder, autoRun);
3738
}
3839

39-
private static void runIfApply(Runner runner, boolean autoRun) {
40+
private static void runIfApply(RunnerBuilder runnerBuilder, boolean autoRun) {
4041
if(autoRun) {
42+
Runner runner = runnerBuilder.build();
4143
runner.run();
4244
} else {
4345
logger.info(
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
val springBootVersion = "2.0.0.RELEASE"
2+
val springFrameworkVersion = "5.0.4.RELEASE"
3+
dependencies {
4+
api(project(":core:flamingock-test-support"))
5+
6+
implementation(project(":core:flamingock-core"))
7+
implementation(project(":core:flamingock-core-commons"))
8+
compileOnly("org.springframework:spring-context:${springFrameworkVersion}")
9+
compileOnly("org.springframework.boot:spring-boot:${springBootVersion}")
10+
compileOnly("org.springframework.boot:spring-boot-autoconfigure:${springBootVersion}")
11+
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
12+
13+
// Required for @FlamingockSpringBootTest annotation
14+
implementation("org.springframework.boot:spring-boot-test:${springBootVersion}")
15+
implementation("org.springframework:spring-test:${springFrameworkVersion}")
16+
17+
18+
testImplementation(platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}"))
19+
20+
testImplementation("org.springframework.boot:spring-boot-starter-test")
21+
22+
// Annotation processor for @EnableFlamingock in tests
23+
testAnnotationProcessor(project(":core:flamingock-processor"))
24+
25+
// Spring Boot integration module
26+
testImplementation(project(":platform-plugins:flamingock-springboot-integration"))
27+
28+
// InMemory test utilities
29+
testImplementation(project(":utils:test-util"))
30+
31+
// Non-transactional target system for tests
32+
testImplementation(project(":core:target-systems:nontransactional-target-system"))
33+
}
34+
35+
description = "Spring Boot testing integration module for Flamingock. Compatible with JDK 17 and above."
36+
37+
java {
38+
toolchain {
39+
languageVersion.set(JavaLanguageVersion.of(8))
40+
}
41+
}
42+
43+
configurations.testImplementation {
44+
extendsFrom(configurations.compileOnly.get())
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2025 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.springboot.testsupport;
17+
18+
import org.springframework.boot.test.context.SpringBootTest;
19+
import org.springframework.core.annotation.AliasFor;
20+
21+
import java.lang.annotation.Documented;
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Inherited;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.RetentionPolicy;
26+
import java.lang.annotation.Target;
27+
28+
/**
29+
* Annotation for Flamingock integration tests with Spring Boot.
30+
*
31+
* <p>This annotation configures a Spring Boot test context with Flamingock's
32+
* auto-run disabled ({@code flamingock.autorun=false}), allowing tests to
33+
* manually control when Flamingock executes.</p>
34+
*
35+
* <p><strong>Usage:</strong></p>
36+
* <pre>
37+
* &#64;ExtendWith(SpringExtension.class) // Required for Spring Boot &lt; 2.1
38+
* &#64;FlamingockSpringBootTest(classes = {MyApplication.class, TestConfig.class})
39+
* class MyFlamingockTest {
40+
*
41+
* &#64;Autowired
42+
* private AbstractChangeRunnerBuilder&lt;?, ?&gt; flamingockBuilder;
43+
*
44+
* &#64;Test
45+
* void testMigration() {
46+
* // Setup preconditions...
47+
*
48+
* // Execute Flamingock manually
49+
* flamingockBuilder.build().run();
50+
*
51+
* // Verify results...
52+
* }
53+
* }
54+
* </pre>
55+
*
56+
* <p><strong>Note:</strong> For Spring Boot 2.0.x, you must add
57+
* {@code @ExtendWith(SpringExtension.class)} to your test class.
58+
* Spring Boot 2.1+ does not require this.</p>
59+
*
60+
* @see SpringBootTest
61+
* @since 1.0
62+
*/
63+
@Target(ElementType.TYPE)
64+
@Retention(RetentionPolicy.RUNTIME)
65+
@Documented
66+
@Inherited
67+
@SpringBootTest(properties = "flamingock.autorun=false")
68+
public @interface FlamingockSpringBootTest {
69+
70+
/**
71+
* Alias for {@link SpringBootTest#classes()}.
72+
* The component classes to use for loading an ApplicationContext.
73+
*
74+
* @return the component classes
75+
* @see SpringBootTest#classes()
76+
*/
77+
@AliasFor(annotation = SpringBootTest.class, attribute = "classes")
78+
Class<?>[] classes() default {};
79+
80+
/**
81+
* Alias for {@link SpringBootTest#webEnvironment()}.
82+
* The type of web environment to create when applicable.
83+
*
84+
* @return the web environment mode
85+
* @see SpringBootTest#webEnvironment()
86+
*/
87+
@AliasFor(annotation = SpringBootTest.class, attribute = "webEnvironment")
88+
SpringBootTest.WebEnvironment webEnvironment() default SpringBootTest.WebEnvironment.MOCK;
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2025 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.springboot.testsupport;
17+
18+
import io.flamingock.internal.core.builder.AbstractChangeRunnerBuilder;
19+
import io.flamingock.support.FlamingockTestSupport;
20+
import io.flamingock.support.stages.GivenStage;
21+
22+
public class FlamingockSpringBootTestSupport {
23+
24+
private final AbstractChangeRunnerBuilder<?, ?> builderFromContext;
25+
26+
FlamingockSpringBootTestSupport(AbstractChangeRunnerBuilder<?,?> builderFromContext) {
27+
this.builderFromContext = builderFromContext;
28+
}
29+
30+
public GivenStage givenBuilderFromContext() {
31+
return FlamingockTestSupport.givenBuilder(builderFromContext);
32+
}
33+
34+
public GivenStage givenBuilder(AbstractChangeRunnerBuilder<?,?> builderFromContextOverwrite) {
35+
return FlamingockTestSupport.givenBuilder(builderFromContextOverwrite);
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2025 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.springboot.testsupport;
17+
18+
import io.flamingock.internal.core.builder.AbstractChangeRunnerBuilder;
19+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
20+
import org.springframework.context.annotation.Bean;
21+
import org.springframework.context.annotation.Configuration;
22+
import org.springframework.context.annotation.Scope;
23+
24+
/**
25+
* Autoconfiguration that provides a {@link FlamingockSpringBootTestSupport} bean for Flamingock testing.
26+
*
27+
* <p>This configuration is automatically picked up when using {@link FlamingockSpringBootTest}
28+
* and provides a ready-to-use {@code FlamingockSpringBootTestSupport} bean that can be autowired
29+
* directly in tests.</p>
30+
*
31+
* <p><strong>Example usage:</strong></p>
32+
* <pre>
33+
* &#64;FlamingockSpringBootTest(classes = {MyApp.class, TestConfig.class})
34+
* class MyFlamingockTest {
35+
*
36+
* &#64;Autowired
37+
* private FlamingockSpringBootTestSupport flamingockTestSupport;
38+
*
39+
* &#64;Test
40+
* void testMigration() {
41+
* flamingockTestSupport.givenBuilderFromContext()
42+
* .whenRun()
43+
* .thenExpectAuditSequenceStrict(APPLIED(MyChange.class))
44+
* .verify();
45+
* }
46+
* }
47+
* </pre>
48+
*
49+
* <p><strong>Note:</strong> The {@code FlamingockSpringBootTestSupport} bean has prototype scope,
50+
* meaning a new instance is created for each injection point. This is necessary because the
51+
* underlying {@code GivenStage} accumulates internal state (preconditions, expectations) and
52+
* is not reusable between tests.</p>
53+
*
54+
* @see FlamingockSpringBootTest
55+
* @see FlamingockSpringBootTestSupport
56+
* @see io.flamingock.support.FlamingockTestSupport
57+
* @since 1.0
58+
*/
59+
@Configuration
60+
public class FlamingockTestAutoConfiguration {
61+
62+
/**
63+
* Creates a {@link FlamingockSpringBootTestSupport} bean for Flamingock testing.
64+
*
65+
* <p>The bean has prototype scope to ensure each test gets its own fresh instance,
66+
* as the underlying {@code GivenStage} accumulates state and cannot be shared between tests.</p>
67+
*
68+
* @param builderFromContext the Flamingock builder, automatically configured by Spring Boot
69+
* @return a new {@code FlamingockSpringBootTestSupport} instance ready for testing
70+
*/
71+
@Bean
72+
@Scope("prototype")
73+
@ConditionalOnBean(AbstractChangeRunnerBuilder.class)
74+
public FlamingockSpringBootTestSupport flamingockGivenStage(AbstractChangeRunnerBuilder<?, ?> builderFromContext) {
75+
return new FlamingockSpringBootTestSupport(builderFromContext);
76+
}
77+
}

0 commit comments

Comments
 (0)