GH-256 - Added Kotlin variant for Java code.

This commit is contained in:
Devashish Bhattacharjee
2023-10-13 21:38:36 +05:30
committed by Oliver Drotbohm
parent 6653bcb527
commit 9b79e09729
7 changed files with 550 additions and 39 deletions

View File

@@ -13,7 +13,11 @@ Spring Modulith's `Documenter` abstraction can produce two different kinds of sn
The documentation snippets can be generated by handing the `ApplicationModules` instance into a `Documenter`.
.Generating application module component diagrams using `Documenter`
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
class DocumentationTests {
@@ -28,6 +32,22 @@ class DocumentationTests {
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
class DocumentationTests {
private val modules = ApplicationModules.of(Application::class)
@Test
fun writeDocumentationSnippets() {
Documenter(modules)
.writeModulesAsPlantUml()
.writeIndividualModulesAsPlantUml()
}
}
----
======
The first call on `Documenter` will generate a C4 component diagram containing all modules within the system.
@@ -90,13 +110,23 @@ SHOW_LEGEND()
=== Using Traditional UML Component Diagrams
If you prefer the traditional UML style component diagrams, tweak the `DiagramOptions` to rather use that style as follows:
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
DiagramOptions.defaults()
.withStyle(DiagramStyle.UML);
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
DiagramOptions.defaults()
.withStyle(DiagramStyle.UML)
----
======
This will cause the diagrams to look like this:
.All modules and their relationships rendered as UML component diagram
@@ -184,7 +214,11 @@ package "Application" <<Container>> {
The Application Module Canvases can be generated by calling `Documenter.writeModuleCanvases()`:
.Generating application module canvases using `Documenter`
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
class DocumentationTests {
@@ -198,7 +232,22 @@ class DocumentationTests {
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
class DocumentationTests {
private val modules = ApplicationModules.of(Application::class)
@Test
fun writeDocumentationSnippets() {
Documenter(modules)
.writeModuleCanvases()
}
}
----
======
By default, the documentation will be generated to `spring-modulith-docs` folder in your build system's build folder.
A generated canvas looks like this:

View File

@@ -7,8 +7,11 @@ To keep application modules as decoupled as possible from each other, their prim
This avoids the originating module to know about all potentially interested parties, which is a key aspect to enable application module integration testing (see xref:testing.adoc[Integration Testing Application Modules]).
Often we will find application components defined like this:
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Service
@RequiredArgsConstructor
@@ -26,6 +29,20 @@ public class OrderManagement {
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Service
class OrderManagement(val inventory: InventoryManagement) {
@Transactional
fun complete(order: Order) {
inventory.updateStockFor(order)
}
}
----
======
The `complete(…)` method creates functional gravity in the sense that it attracts related functionality and thus interaction with Spring beans defined in other application modules.
This especially makes the component harder to test as we need to have instances available of those depended on beans just to create an instance of `OrderManagement` (see xref:testing.adoc#efferent-dependencies[Dealing with Efferent Dependencies]).
@@ -34,7 +51,11 @@ It also means that we will have to touch the class whenever we would like to int
We can change the application module interaction as follows:
.Publishing an application event via Spring's `ApplicationEventPublisher`
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Service
@RequiredArgsConstructor
@@ -53,6 +74,21 @@ public class OrderManagement {
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Service
class OrderManagement(val events: ApplicationEventPublisher, val dependency: OrderInternal) {
@Transactional
fun complete(order: Order) {
events.publishEvent(OrderCompleted(order.id))
}
}
----
======
Note, how, instead of depending on the other application module's Spring bean, we use Spring's `ApplicationEventPublisher` to publish a domain event, once we have completed the state transitions on the primary aggregate.
For a more aggregate-driven approach to event publication, see https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#core.domain-events[Spring Data's application event publication mechanism] for details.
As event publication happens synchronously by default, the transactional semantics of the overall arrangement stay the same as in the example above.
@@ -61,7 +97,11 @@ Both for the good, as we get to a very simple consistency model (either both the
A different way of approaching this is by moving the event consumption to asynchronous handling at transaction commit and treat secondary functionality exactly as that:
.An async, transactional event listener
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Component
class InventoryManagement {
@@ -71,7 +111,19 @@ class InventoryManagement {
void on(OrderCompleted event) { /* … */ }
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Component
class InventoryManagement {
@Async
@TransactionalEventListener
fun on(event: OrderCompleted) { /* … */ }
}
----
======
This now effectively decouples the original transaction from the execution of the listener.
While this avoids the expansion of the original business transaction, it also creates a risk: if the listener fails for whatever reason, the event publication is lost, unless each listener actually implements its own safety net.
Even worse, that doesn't even fully work, as the system might fail before the method is even invoked.
@@ -82,7 +134,11 @@ Even worse, that doesn't even fully work, as the system might fail before the me
To run a transactional event listener in a transaction itself, it would need to be annotated with `@Transactional` in turn.
.An async, transactional event listener running in a transaction itself
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Component
class InventoryManagement {
@@ -93,11 +149,29 @@ class InventoryManagement {
void on(OrderCompleted event) { /* … */ }
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Component
class InventoryManagement {
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener
fun on(event: OrderCompleted) { /* … */ }
}
----
======
To ease the declaration of what is supposed to describe the default way of integrating modules via events, Spring Modulith provides `@ApplicationModuleListener` to shortcut the declaration
.An application module listener
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Component
class InventoryManagement {
@@ -106,6 +180,18 @@ class InventoryManagement {
void on(OrderCompleted event) { /* … */ }
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Component
class InventoryManagement {
@ApplicationModuleListener
fun on(event: OrderCompleted) { /* … */ }
}
----
======
[[publication-registry]]
== The Event Publication Registry
@@ -257,7 +343,11 @@ To define a custom routing key via the `@Externalized` annotations, a pattern of
The key can be a SpEL expression which will get the event instance configured as root object.
.Defining a dynamic routing key via SpEL expression
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Externalized("customer-created::#{#this.getLastname()}") // <2>
class CustomerCreated {
@@ -267,6 +357,18 @@ class CustomerCreated {
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Externalized("customer-created::#{#this.getLastname()}") // <2>
class CustomerCreated {
fun getLastname(): String { // <1>
// …
}
}
----
======
The `CustomerCreated` event exposes the lastname of the customer via an accessor method.
That method is then used via the ``&#35;this.getLastname()`` expression in key expression following the `::` delimiter of the target declaration.
@@ -274,10 +376,21 @@ That method is then used via the ``&#35;this.getLastname()`` expression in key e
If the key calculation becomes more involved, it is advisable to rather delegate that into a Spring bean that takes the event as argument:
.Invoking a Spring bean to calculate a routing key
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Externalized("…::#{@beanName.someMethod(#this)}")
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Externalized("…::#{@beanName.someMethod(#this)}")
----
======
[[externalization.api]]
=== Programmatic Event Externalization Configuration
@@ -286,7 +399,11 @@ If the key calculation becomes more involved, it is advisable to rather delegate
The `spring-modulith-events-api` artifact contains `EventExternalizationConfiguration` that allows developers to customize all of the above mentioned steps.
.Programmatically configuring event externalization
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Configuration
class ExternalizationConfiguration {
@@ -302,6 +419,26 @@ class ExternalizationConfiguration {
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Configuration
class ExternalizationConfiguration {
@Bean
fun eventExternalizationConfiguration(): EventExternalizationConfiguration {
EventExternalizationConfiguration.externalizing() // <1>
.select(EventExternalizationConfiguration.annotatedAsExternalized()) // <2>
.mapping(SomeEvent::class, it -> …) // <3>
.routeKey(WithKeyProperty::class, WithKeyProperty::getKey) // <4>
.build()
}
}
----
======
<1> We start by creating a default instance of `EventExternalizationConfiguration`.
<2> We customize the event selection by calling one of the `select(…)` methods on the `Selector` instance returned by the previous call.
This step fundamentally disables the application base package filter as we only look for the annotation now.
@@ -321,7 +458,11 @@ For a more holistic approach on testing modules that use <<events.aml, `@Applica
Spring Modulith's `@ApplicationModuleTest` enables the ability to get a `PublishedEvents` instance injected into the test method to verify a particular set of events has been published during the course of the business operation under test.
.Event-based integration testing of the application module arrangement
[source, java, subs="quotes"]
[tabs]
======
Java::
+
[source, java, subs="quotes", role="primary"]
----
@ApplicationModuleTest
class OrderIntegrationTests {
@@ -337,13 +478,35 @@ class OrderIntegrationTests {
}
}
----
Kotlin::
+
[source, kotlin, subs="quotes", role="secondary"]
----
@ApplicationModuleTest
class OrderIntegrationTests {
@Test
fun someTestMethod(events: PublishedEvents events) {
// …
var matchingMapped = events.ofType(OrderCompleted::class)
.matching(OrderCompleted::getOrderId, reference.getId())
assertThat(matchingMapped).hasSize(1)
}
}
----
======
Note, how `PublishedEvents` exposes API to select events matching a certain criteria.
The verification is concluded by an AssertJ assertion that verifies the number of elements expected.
If you are using AssertJ for those assertions anyway, you can also use `AssertablePublishedEvents` as test method parameter type and use the fluent assertion APIs provided through that.
.Using `AssertablePublishedEvents` to verify event publications
[source, java, subs="quotes"]
[tabs]
======
Java::
+
[source, java, subs="quotes", role="primary"]
----
@ApplicationModuleTest
class OrderIntegrationTests {
@@ -358,6 +521,24 @@ class OrderIntegrationTests {
}
}
----
Kotlin::
+
[source, kotlin, subs="quotes", role="secondary"]
----
@ApplicationModuleTest
class OrderIntegrationTests {
@Test
fun someTestMethod(events: AssertablePublishedEvents) {
// …
assertThat(events)
.contains(OrderCompleted::class)
.matching(OrderCompleted::getOrderId, reference.getId())
}
}
----
======
Note, how the type returned by the `assertThat(…)` expression allows to define constraints on the published events directly.

View File

@@ -78,13 +78,27 @@ In this case, the Java compiler is not of much use to prevent these illegal refe
A module can opt into declaring its allowed dependencies by using the `@ApplicationModule` annotation on the `package-info.java` type.
.Inventory explicitly configuring module dependencies
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary", chomp="none"]
----
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "order"
)
package example.inventory;
----
Kotlin::
+
[source, kotlin, role="secondary", chomp="none"]
----
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "order"
)
package example.inventory
----
======
In this case code within the __inventory__ module was only allowed to refer to code in the __order__ module (and code not assigned to any module in the first place).
Find out about how to monitor that in xref:verification.adoc[Verifying Application Module Structure].
@@ -96,19 +110,39 @@ Spring Moduliths allows to inspect a codebase to derive an application module mo
The `spring-modulith-core` artifact contains `ApplicationModules` that can be pointed to a Spring Boot application class:
.Creating an application module model
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
var modules = ApplicationModules.of(Application.class);
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
var modules = ApplicationModules.of(Application::class)
----
======
To get an impression about what the analyzed arrangement looks like, we can just write the individual modules contained in the overall model to the console:
.Writing the application module arranagement to the console
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
modules.forEach(System.out::println);
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
modules.forEach(println(it))
----
======
.The console output of our application module arrangement
[source]
----
@@ -154,23 +188,48 @@ icon:cubes[] Example
----
.`package-info.java` in `example.order.spi`
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary", chomp="none"]
----
@org.springframework.modulith.NamedInterface("spi")
package example.order.spi;
----
Kotlin::
+
[source, kotlin, role="secondary", chomp="none"]
----
@org.springframework.modulith.NamedInterface("spi")
package example.order.spi
----
======
The effect of that declaration is two fold: first, code in other application modules is allowed to refer to `SomeSpiInterface`.
Application modules are able to refer to the named interface in explicit dependency declarations.
Assume the __inventory__ module was making use of that, it could refer to the above declared named interface like this:
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary", chomp="none"]
----
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "order::spi"
)
package example.inventory;
----
Kotlin::
+
[source, kotlin, role="secondary", chomp="none"]
----
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "order::spi"
)
package example.inventory
----
======
Note how we concatenate the named interface's name `spi` via the double colon `::`.
In this setup, code in __inventory__ would be allowed to depend on `SomeSpiInterface` and other code residing in the `order.spi` interface, but not on `OrderManagement` for example.
@@ -185,7 +244,11 @@ You can then inspect the packages residing within that and select the ones to be
Assume you declare a custom `ApplicationModuleDetectionStrategy` implementation like this:
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
package example;
@@ -197,6 +260,20 @@ class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetec
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
package example
class CustomApplicationModuleDetectionStrategy : ApplicationModuleDetectionStrategy {
override fun getModuleBasePackages(basePackage: JavaPackage): Stream<JavaPackage> {
// Your module detection goes here
}
}
----
======
This class needs to be registered in `META-INF/spring.factories` as follows:

View File

@@ -36,7 +36,11 @@ The dependency added to the project's classpath causes the following things in y
By default, Moments uses a `Clock.systemUTC()` instance. To customize this, declare a bean of type `Clock`.
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Configuration
class MyConfiguration {
@@ -47,6 +51,20 @@ class MyConfiguration {
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Configuration
class MyConfiguration {
@Bean
fun myCustomClock(): Clock {
// Create a custom Clock here
}
}
----
======
Moments exposes the following application properties for advanced customization:

View File

@@ -74,7 +74,11 @@ ComponentB --> ComponentA
While developers could of course define the execution order via Spring's standard `@Order` annotation or `Ordered` interface, Spring Modulith provides an `ApplicationModuleInitializer` interface for beans to be run on application startup.
The execution order of those beans will automatically follow the application module dependency structure.
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@Component
class MyInitializer implements ApplicationModuleInitializer {
@@ -85,5 +89,18 @@ class MyInitializer implements ApplicationModuleInitializer {
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@Component
class MyInitializer : ApplicationModuleInitializer {
override fun initialize() {
// Initialization code goes here
}
}
----
======
Note, that the `ApplicationModuleInitializer` beans will only be invoked if the `spring-modulith-runtime` JAR is on the classpath (see xref:runtime.adoc#setup[Setting up Runtime Support for Application Modules]) as that pulls in the dependencies that are needed to topologically sort the initializers according to the application module structure.

View File

@@ -5,7 +5,11 @@ Spring Modulith allows to run integration tests bootstrapping individual applica
To achieve this, place JUnit test class in an application module package or any sub-package of that and annotate it with `@ApplicationModuleTest`:
.A application module integration test class
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
package example.order;
@@ -15,6 +19,19 @@ class OrderIntegrationTests {
// Individual test cases go here
}
----
Kotlin::
+
[source, kortlin, role="secondary"]
----
package example.order
@ApplicationModuleTest
class OrderIntegrationTests {
// Individual test cases go here
}
----
======
This will run your integration test similar to what `@SpringBootTest` would have achieved but with the bootstrap actually limited to the application module the test resides in.
If you configure the log level for `org.springframework.modulith` to `DEBUG`, you will see detailed information about how the test execution customizes the Spring Boot bootstrap:
@@ -64,7 +81,11 @@ If those contain bean references that cross module boundaries, the bootstrap wil
While a natural reaction might be to expand the scope of the application modules included, it is usually a better option to mock the target beans.
.Mocking Spring bean dependencies in other application modules
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@ApplicationModuleTest
class InventoryIntegrationTests {
@@ -72,7 +93,17 @@ class InventoryIntegrationTests {
@MockBean SomeOtherComponent someOtherComponent;
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockBean SomeOtherComponent someOtherComponent
}
----
======
Spring Boot will create bean definitions and instances for the types defined as `@MockBean` and add them to the `ApplicationContext` bootstrapped for the test run.
If you find your application module depending on too many beans of other ones, that is usually a sign of high coupling between them.
@@ -88,7 +119,11 @@ Also, it requires dealing with quite a few infrastructure components: `Transacti
To ease the definition of application module integration tests, Spring Modulith provides the `Scenario` abstraction that can be used by declaring it as test method parameter in tests declared as `@ApplicationModuleTest`.
.Using the `Scenario` API in a JUnit 5 test
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@ApplicationModuleTest
class SomeApplicationModuleTest {
@@ -99,6 +134,20 @@ class SomeApplicationModuleTest {
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@ApplicationModuleTest
class SomeApplicationModuleTest {
@Test
fun someModuleIntegrationTest(scenario: Scenario) {
// Use the Scenario API to define your integration test
}
}
----
======
The test definition itself usually follows the following skeleton:
@@ -110,7 +159,11 @@ The test definition itself usually follows the following skeleton:
`Scenario` exposes API to define these steps and guide you through the definition.
.Defining a stimulus as starting point of the `Scenario`
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
// Start with an event publication
scenario.publish(new MyApplicationEvent(…)).…
@@ -118,6 +171,17 @@ scenario.publish(new MyApplicationEvent(…)).…
// Start with a bean invocation
scenario.stimulate(() -> someBean.someMethod(…)).…
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
// Start with an event publication
scenario.publish(MyApplicationEvent(…)).…
// Start with a bean invocation
scenario.stimulate(() -> someBean.someMethod(…)).…
----
======
Both the event publication and bean invocation will happen within a transaction callback to make sure the given event or any ones published during the bean invocation will be delivered to transactional event listeners.
Note, that this will require a *new* transaction to be started, no matter whether the test case is already running inside a transaction or not.
@@ -130,12 +194,25 @@ The setup phase will be concluded by defining the actual expectation of the outc
This can be an event of a particular type in turn, optionally further constraint by matchers:
.Expecting an event being published as operation result
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
….andWaitForEventOfType(SomeOtherEvent.class)
.matching(event -> …) // Use some predicate here
.…
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
….andWaitForEventOfType(SomeOtherEvent.class)
.matching(event -> …) // Use some predicate here
.…
----
======
These lines set up a completion criteria that the eventual execution will wait for to proceed.
In other words, the example above will cause the execution to eventually block until either the default timeout is reached or a `SomeOtherEvent` is published that matches the predicate defined.
@@ -143,7 +220,11 @@ In other words, the example above will cause the execution to eventually block u
The terminal operations to execute the event-based `Scenario` are named `….toArrive…()` and allow to optionally access the expected event published, or the result object of the bean invocation defined in the original stimulus.
.Triggering the verification
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
// Executes the scenario
….toArrive(…)
@@ -151,28 +232,66 @@ The terminal operations to execute the event-based `Scenario` are named `….toA
// Execute and define assertions on the event received
….toArriveAndVerify(event -> …)
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
// Executes the scenario
….toArrive(…)
// Execute and define assertions on the event received
….toArriveAndVerify(event -> …)
----
======
The choice of method names might look a bit weird when looking at the steps individually but they actually read quite fluent when combined.
.A complete `Scenario` definition
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
scenario.publish(new MyApplicationEvent(…))
.andWaitForEventOfType(SomeOtherEvent.class)
.matching(event -> …)
.toArriveAndVerify(event -> …);
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
scenario.publish(new MyApplicationEvent(…))
.andWaitForEventOfType(SomeOtherEvent::class)
.matching(event -> …)
.toArriveAndVerify(event -> …)
----
======
Alternatively to an event publication acting as expected completion signal, we can also inspect the state of the application module by invoking a method on one of the components exposed.
The scenario would then rather look like this:
.Expecting a state change
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
scenario.publish(new MyApplicationEvent(…))
.andWaitForStateChange(() -> someBean.someMethod(…)))
.andVerify(result -> …);
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
scenario.publish(new MyApplicationEvent(…))
.andWaitForStateChange(() -> someBean.someMethod(…)))
.andVerify(result -> …)
----
======
The `result` handed into the `….andVerify(…)` method will be the value returned by the method invocation to detect the state change.
By default, non-`null` values and non-empty ``Optional``s will be considered a conclusive state change.
@@ -184,7 +303,11 @@ This can be tweaked by using the `….andWaitForStateChange(…, Predicate)` ove
To customize the execution of an individual scenario, call the `….customize(…)` method in the setup chain of the `Scenario`:
.Customizing a `Scenario` execution
[source, java, subs="+quotes"]
[tabs]
======
Java::
+
[source, java, subs="+quotes", role="primary"]
----
scenario.publish(new MyApplicationEvent(…))
**.customize(it -> it.atMost(Duration.ofSeconds(2)))**
@@ -192,11 +315,26 @@ scenario.publish(new MyApplicationEvent(…))
.matching(event -> …)
.toArriveAndVerify(event -> …);
----
Kotlin::
+
[source, kotlin, subs="+quotes", role="secondary"]
----
scenario.publish(MyApplicationEvent(…))
**.customize(it -> it.atMost(Duration.ofSeconds(2)))**
.andWaitForEventOfType(SomeOtherEvent::class)
.matching(event -> …)
.toArriveAndVerify(event -> …)
----
======
To globally customize all `Scenario` instances of a test class, implement a `ScenarioCustomizer` and register it as JUnit extension.
.Registering a `ScenarioCustomizer`
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
@ExtendWith(MyCustomizer.class)
class MyTests {
@@ -215,3 +353,24 @@ class MyTests {
}
}
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
@ExtendWith(MyCustomizer::class)
class MyTests {
@Test
fun myTestCase(scenario : Scenario) {
// scenario will be pre-customized with logic defined in MyCustomizer
}
class MyCustomizer : ScenarioCustomizer {
override fun getDefaultCustomizer(method : Method, context : ApplicationContext) : Function<ConditionFactory, ConditionFactory> {
return it -> …
}
}
}
----
======

View File

@@ -4,11 +4,21 @@
We can verify whether our code arrangement adheres to the intended constraints by calling the `….verify()` method on our `ApplicationModules` instance:
[source, java]
[tabs]
======
Java::
+
[source, java, role="primary"]
----
ApplicationModules.of(Application.class).verify();
----
Kotlin::
+
[source, kotlin, role="secondary"]
----
ApplicationModules.of(Application::class).verify()
----
======
The verification includes the following rules:
* _No cycles on the application module level_ -- the dependencies between modules have to form directed, acyclic graph.