GH-256 - Added Kotlin variant for Java code.
This commit is contained in:
committed by
Oliver Drotbohm
parent
6653bcb527
commit
9b79e09729
@@ -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:
|
||||
|
||||
|
||||
@@ -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 ``#this.getLastname()`` expression in key expression following the `::` delimiter of the target declaration.
|
||||
@@ -274,10 +376,21 @@ That method is then used via the ``#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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 -> …
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user