Skip to content

Commit 71f0b3b

Browse files
committed
feat: flamingock test support new validatorArgs and integration test
1 parent 07bb838 commit 71f0b3b

17 files changed

+714
-23
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
dependencies {
22
api(project(":core:flamingock-core"))
33

4+
45
testImplementation(platform("org.junit:junit-bom:5.10.0"))
56
testImplementation("org.junit.jupiter:junit-jupiter")
67
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
8+
9+
// Add test utilities from the repository so tests can use InMemoryTestKit and pipeline helpers
10+
testImplementation(project(":utils:test-util"))
11+
testImplementation(project(":core:target-systems:nontransactional-target-system"))
12+
api("org.mockito:mockito-inline:4.11.0")
713
}
814

915
description = "Test support module for Flamingock framework"

core/flamingock-test-support/src/main/java/io/flamingock/support/domain/AuditEntryDefinition.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@
2929
import java.lang.reflect.Method;
3030
import java.time.LocalDateTime;
3131

32-
import static io.flamingock.internal.common.core.audit.AuditEntry.Status.APPLIED;
33-
import static io.flamingock.internal.common.core.audit.AuditEntry.Status.FAILED;
34-
import static io.flamingock.internal.common.core.audit.AuditEntry.Status.ROLLBACK_FAILED;
35-
import static io.flamingock.internal.common.core.audit.AuditEntry.Status.ROLLED_BACK;
32+
import static io.flamingock.internal.common.core.audit.AuditEntry.Status.*;
3633

3734
/**
3835
* Defines an audit entry for use in BDD-style tests.
@@ -61,6 +58,7 @@
6158
*
6259
* <h3>String-based (manual configuration)</h3>
6360
* <ul>
61+
* <li>{@link #STARTED(String)} - Define a started change</li>
6462
* <li>{@link #APPLIED(String)} - Define an applied change</li>
6563
* <li>{@link #FAILED(String)} - Define a failed change</li>
6664
* <li>{@link #ROLLED_BACK(String)} - Define a rolled-back change</li>
@@ -69,6 +67,7 @@
6967
*
7068
* <h3>Class-based (auto-extraction from annotations)</h3>
7169
* <ul>
70+
* <li>{@link #STARTED(Class)} - Extracts changeId, author, className, methodName from annotations</li>
7271
* <li>{@link #APPLIED(Class)} - Extracts changeId, author, className, methodName from annotations</li>
7372
* <li>{@link #FAILED(Class)} - Same extraction for failed changes</li>
7473
* <li>{@link #ROLLED_BACK(Class)} - Extracts rollback method name from {@code @Rollback}</li>
@@ -120,6 +119,16 @@ private AuditEntryDefinition(String changeId, AuditEntry.Status state) {
120119

121120
// ========== Static Factory Methods (String-based) ==========
122121

122+
/**
123+
* Creates a definition for a started change.
124+
*
125+
* @param changeId the unique identifier of the change
126+
* @return a new definition builder for further configuration
127+
*/
128+
public static AuditEntryDefinition STARTED(String changeId) {
129+
return new AuditEntryDefinition(changeId, STARTED);
130+
}
131+
123132
/**
124133
* Creates a definition for an applied change.
125134
*
@@ -162,6 +171,29 @@ public static AuditEntryDefinition ROLLBACK_FAILED(String changeId) {
162171

163172
// ========== Static Factory Methods (Class-based) ==========
164173

174+
/**
175+
* Creates a definition for a started change by extracting metadata from annotations.
176+
*
177+
* <p>Extracts from the change class:</p>
178+
* <ul>
179+
* <li>Change ID and author from {@code @Change} annotation</li>
180+
* <li>Class name from the class itself</li>
181+
* <li>Method name from the method annotated with {@code @Apply}</li>
182+
* <li>Target system ID from {@code @TargetSystem} annotation (if present)</li>
183+
* <li>Recovery strategy from {@code @Recovery} annotation (if present)</li>
184+
* <li>Order from class name pattern {@code _ORDER__CHANGE-NAME} (if applicable)</li>
185+
* <li>Transactional flag from {@code @Change.transactional()}</li>
186+
* </ul>
187+
*
188+
* @param changeClass the change class annotated with {@code @Change}
189+
* @return a new definition builder pre-populated with annotation values
190+
* @throws IllegalArgumentException if the class is not annotated with {@code @Change}
191+
* or does not contain a method annotated with {@code @Apply}
192+
*/
193+
public static AuditEntryDefinition STARTED(Class<?> changeClass) {
194+
return fromChangeClass(changeClass, STARTED);
195+
}
196+
165197
/**
166198
* Creates a definition for an applied change by extracting metadata from annotations.
167199
*
@@ -261,7 +293,7 @@ private static AuditEntryDefinition fromChangeClass(Class<?> changeClass, AuditE
261293

262294
private static String findMethodName(Class<?> changeClass, AuditEntry.Status status) {
263295
Class<? extends Annotation> annotationClass =
264-
(status == APPLIED || status == FAILED) ? Apply.class : Rollback.class;
296+
(status == STARTED || status == APPLIED || status == FAILED) ? Apply.class : Rollback.class;
265297

266298
for (Method method : changeClass.getDeclaredMethods()) {
267299
if (method.isAnnotationPresent(annotationClass)) {

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,52 +19,53 @@
1919
import io.flamingock.support.domain.AuditEntryDefinition;
2020
import io.flamingock.support.precondition.PreconditionInserter;
2121
import io.flamingock.support.validation.ValidationHandler;
22-
import io.flamingock.support.validation.Validator;
23-
import io.flamingock.support.validation.ValidatorFactory;
22+
import io.flamingock.support.validation.ValidatorArgs;
23+
import io.flamingock.support.validation.impl.AuditSequenceStrictValidator;
24+
import io.flamingock.support.validation.impl.DefaultExceptionValidator;
2425

2526
import java.util.ArrayList;
2627
import java.util.List;
2728
import java.util.function.Consumer;
29+
import java.util.Arrays;
30+
import java.util.Collections;
2831

2932
final class ThenStageImpl implements ThenStage {
3033

31-
private final List<Validator> validators = new ArrayList<>();
32-
private final ValidatorFactory validatorFactory;
34+
private final List<ValidatorArgs> validators = new ArrayList<>();
3335
private final TestContext testContext;
3436

3537
ThenStageImpl(TestContext testContext) {
3638
this.testContext = testContext;
37-
validatorFactory = new ValidatorFactory(testContext.getAuditReader());
3839
}
3940

4041
@Override
4142
public ThenStage andExpectAuditSequenceStrict(AuditEntryDefinition... definitions) {
42-
validators.add(validatorFactory.getAuditSeqStrictValidator(definitions));
43+
List<AuditEntryDefinition> definitionsList = definitions != null ? Arrays.asList(definitions) : Collections.<AuditEntryDefinition>emptyList();
44+
validators.add(new AuditSequenceStrictValidator.Args(definitionsList));
4345
return this;
4446
}
4547

4648
@Override
4749
public ThenStage andExpectException(Class<? extends Throwable> exceptionClass, Consumer<Throwable> exceptionConsumer) {
48-
validators.add(validatorFactory.getExceptionValidator(exceptionClass, exceptionConsumer));
50+
validators.add(new DefaultExceptionValidator.Args(exceptionClass, exceptionConsumer));
4951
return this;
5052
}
5153

5254
@Override
5355
public void verify() throws AssertionError {
54-
// Insert preconditions first
55-
PreconditionInserter preconditionInserter = new PreconditionInserter(testContext.getAuditWriter());
56-
preconditionInserter.insert(testContext.getPreconditions());
57-
5856
ValidationHandler validationHandler;
5957
try {
6058
testContext.run();
61-
validationHandler = new ValidationHandler(validators);
59+
validationHandler = new ValidationHandler(testContext, validators);
6260

6361
} catch (Throwable actualException) {
64-
validationHandler = new ValidationHandler(validators, actualException);
62+
validationHandler = new ValidationHandler(testContext, validators, actualException);
6563

6664
}
6765

6866
validationHandler.validate();
67+
68+
PreconditionInserter preconditionInserter = new PreconditionInserter(testContext.getAuditWriter());
69+
preconditionInserter.insert(testContext.getPreconditions());
6970
}
7071
}

core/flamingock-test-support/src/main/java/io/flamingock/support/validation/ValidationHandler.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.flamingock.support.validation;
1717

18+
import io.flamingock.support.context.TestContext;
1819
import io.flamingock.support.validation.error.ExceptionNotExpectedError;
1920
import io.flamingock.support.validation.error.ValidationResult;
2021

@@ -47,6 +48,25 @@ public ValidationHandler(List<Validator> validators, Throwable executionExceptio
4748
this.formatter = new ValidationErrorFormatter();
4849
}
4950

51+
// TestContext and deferred ValidatorArgs
52+
public ValidationHandler(TestContext testContext, List<ValidatorArgs> args) {
53+
this(testContext, args, null);
54+
}
55+
56+
public ValidationHandler(TestContext testContext, List<ValidatorArgs> args, Throwable executionException) {
57+
this.executionException = executionException;
58+
this.formatter = new ValidationErrorFormatter();
59+
// Build actual validators now that we have access to the TestContext
60+
ValidatorFactory factory = new ValidatorFactory(testContext.getAuditReader());
61+
List<Validator> built = new ArrayList<>();
62+
if (args != null) {
63+
for (ValidatorArgs a : args) {
64+
built.add(factory.getValidator(a));
65+
}
66+
}
67+
this.validators = built;
68+
}
69+
5070
/**
5171
* Executes all validators and throws an AssertionError if any validation fails.
5272
*
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.support.validation;
17+
18+
/**
19+
* Marker interface for validator argument carriers used in the BDD test framework.
20+
*
21+
* <p>This interface serves as a deferred construction pattern for validators. Instead of
22+
* creating validators immediately when defining expectations in the "Then" stage, implementations
23+
* of this interface carry the necessary arguments until the {@link io.flamingock.support.stages.ThenStage#verify()} method
24+
* is called. This allows validators to be constructed with access to the {@link io.flamingock.support.context.TestContext},
25+
* particularly the {@link io.flamingock.internal.common.core.audit.AuditReader}.</p>
26+
*
27+
* <h2>Purpose</h2>
28+
* <p>The deferred construction is necessary because:</p>
29+
* <ul>
30+
* <li>The audit reader is only available after the test context is fully initialized</li>
31+
* <li>Validators need access to actual audit entries, which are only available after execution</li>
32+
* <li>The BDD API is lazy - expectations are defined but not executed until {@code verify()}</li>
33+
* </ul>
34+
*
35+
* <h2>Implementation</h2>
36+
* <p>Implementations are typically static inner classes within their corresponding validators:</p>
37+
* <pre>{@code
38+
* public class AuditSequenceStrictValidator implements SimpleValidator {
39+
* // ... validator implementation
40+
*
41+
* public static class Args implements ValidatorArgs {
42+
* private final List<AuditEntryDefinition> expectations;
43+
*
44+
* public Args(List<AuditEntryDefinition> expectations) {
45+
* this.expectations = expectations;
46+
* }
47+
*
48+
* public List<AuditEntryDefinition> getExpectations() {
49+
* return expectations;
50+
* }
51+
* }
52+
* }
53+
* }</pre>
54+
*
55+
* <h2>Usage in Framework</h2>
56+
* <p>The framework uses these argument carriers as follows:</p>
57+
* <ol>
58+
* <li>User calls expectation method (e.g., {@code thenExpectAuditSequenceStrict(...)})</li>
59+
* <li>Stage creates an {@code Args} instance and stores it in the validators list</li>
60+
* <li>When {@code verify()} is called, the {@link ValidationHandler} constructs actual
61+
* validators using {@link ValidatorFactory#getValidator(ValidatorArgs)}</li>
62+
* <li>Validators are executed with access to the test context and audit reader</li>
63+
* </ol>
64+
*
65+
* @see ValidatorFactory#getValidator(ValidatorArgs)
66+
* @see ValidationHandler
67+
* @see io.flamingock.support.stages.ThenStage#verify()
68+
*/
69+
public interface ValidatorArgs {
70+
}
71+
72+

core/flamingock-test-support/src/main/java/io/flamingock/support/validation/ValidatorFactory.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,18 @@ public Validator getAuditSeqStrictValidator(AuditEntryDefinition... definitions)
3737
public Validator getExceptionValidator(Class<? extends Throwable> exceptionClass, Consumer<Throwable> exceptionConsumer) {
3838
return new DefaultExceptionValidator(exceptionClass, exceptionConsumer);
3939
}
40+
41+
public Validator getValidator(ValidatorArgs args) {
42+
if (args instanceof AuditSequenceStrictValidator.Args) {
43+
AuditSequenceStrictValidator.Args a = (AuditSequenceStrictValidator.Args) args;
44+
return new AuditSequenceStrictValidator(auditReader, a.getExpectations().toArray(new AuditEntryDefinition[0]));
45+
}
46+
47+
if (args instanceof DefaultExceptionValidator.Args) {
48+
DefaultExceptionValidator.Args a = (DefaultExceptionValidator.Args) args;
49+
return new DefaultExceptionValidator(a.getExceptionClass(), a.getExceptionConsumer());
50+
}
51+
52+
throw new IllegalArgumentException("Unknown ValidatorArgs type: " + (args != null ? args.getClass() : "null"));
53+
}
4054
}

core/flamingock-test-support/src/main/java/io/flamingock/support/validation/impl/AuditSequenceStrictValidator.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.flamingock.internal.common.core.audit.AuditReader;
2020
import io.flamingock.support.domain.AuditEntryDefinition;
2121
import io.flamingock.support.validation.SimpleValidator;
22+
import io.flamingock.support.validation.ValidatorArgs;
2223
import io.flamingock.support.validation.error.*;
2324

2425
import java.util.ArrayList;
@@ -77,8 +78,8 @@ public ValidationResult validate() {
7778
allErrors.addAll(getValidationErrors(expectations, actualEntries));
7879

7980
return allErrors.isEmpty()
80-
? ValidationResult.success(VALIDATOR_NAME)
81-
: ValidationResult.failure(VALIDATOR_NAME, allErrors.toArray(new ValidationError[0]));
81+
? ValidationResult.success(VALIDATOR_NAME)
82+
: ValidationResult.failure(VALIDATOR_NAME, allErrors.toArray(new ValidationError[0]));
8283
}
8384

8485
private static List<ValidationError> getValidationErrors(List<AuditEntryExpectation> expectedEntries, List<AuditEntry> actualEntries) {
@@ -93,15 +94,15 @@ private static List<ValidationError> getValidationErrors(List<AuditEntryExpectat
9394
AuditEntryExpectation expected = i < expectedEntries.size() ? expectedEntries.get(i) : null;
9495
AuditEntry actual = i < actualEntries.size() ? actualEntries.get(i) : null;
9596
if( expected != null && actual != null) {
96-
allErrors.addAll(expected.compareWith(actual));
97+
allErrors.addAll(expected.compareWith(actual));
9798
} else if( expected != null) {
98-
AuditEntryDefinition def = expected.getDefinition();
99+
AuditEntryDefinition def = expected.getDefinition();
99100
allErrors.add(new MissingEntryError(i, def.getChangeId(), def.getState()));
100101
} else {
101102
assert actual != null;
102103
allErrors.add(new UnexpectedEntryError(i, actual.getTaskId(), actual.getState()));
103104
}
104-
105+
105106
}
106107
return allErrors;
107108
}
@@ -117,4 +118,16 @@ private List<String> getActualChangeIds() {
117118
.map(AuditEntry::getTaskId)
118119
.collect(Collectors.toList());
119120
}
121+
122+
public static class Args implements ValidatorArgs {
123+
private final List<AuditEntryDefinition> expectations;
124+
125+
public Args(List<AuditEntryDefinition> expectations) {
126+
this.expectations = expectations;
127+
}
128+
129+
public List<AuditEntryDefinition> getExpectations() {
130+
return expectations;
131+
}
132+
}
120133
}

core/flamingock-test-support/src/main/java/io/flamingock/support/validation/impl/DefaultExceptionValidator.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,22 @@ public ValidationResult validate(Throwable actualException) {
6868
}
6969
return ValidationResult.success(VALIDATOR_NAME);
7070
}
71+
72+
public static class Args implements io.flamingock.support.validation.ValidatorArgs {
73+
private final Class<? extends Throwable> exceptionClass;
74+
private final Consumer<Throwable> exceptionConsumer;
75+
76+
public Args(Class<? extends Throwable> exceptionClass, Consumer<Throwable> exceptionConsumer) {
77+
this.exceptionClass = exceptionClass;
78+
this.exceptionConsumer = exceptionConsumer;
79+
}
80+
81+
public Class<? extends Throwable> getExceptionClass() {
82+
return exceptionClass;
83+
}
84+
85+
public Consumer<Throwable> getExceptionConsumer() {
86+
return exceptionConsumer;
87+
}
88+
}
7189
}

0 commit comments

Comments
 (0)