Skip to content

Commit

Permalink
GH-6 - Moar reference documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
odrotbohm committed Jul 29, 2022
1 parent e84ae44 commit fb23c45
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 12 deletions.
76 changes: 70 additions & 6 deletions src/docs/asciidoc/30-testing.adoc
Original file line number Diff line number Diff line change
@@ -1,9 +1,73 @@
[[testing]]
= Integration Testing Application Modules

[[testing.subheadline]]
== Subheadline
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Spring Modulith allows to run integration tests bootstrapping individual application modules in isolation or combination with others.
To achieve this, place JUnit test class in an application module package or any sub-package of that and annotate it with `@ApplicationModuleTest`:

[source, java]
----
@ApplicationModuleTest
class OrderIntegrationTests {
// Individual test cases go here
}
----

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:

[source]
----
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.0-SNAPSHOT)
… - Bootstrapping @ApplicationModuleTest for example.order in mode STANDALONE (class example.Application)…
… - ======================================================================================================
… - ## example.order ##
… - > Logical name: order
… - > Base package: example.order
… - > Direct module dependencies: none
… - > Spring beans:
… - + ….OrderManagement
… - + ….internal.OrderInternal
… - Starting OrderIntegrationTests using Java 17.0.3 …
… - No active profile set, falling back to 1 default profile: "default"
… - Re-configuring auto-configuration and entity scan packages to: example.order.
----

Note, how the output contains the detailed information about the module included in the test run.

[[testing.bootstrap-modes]]
== Bootstrap Modes

The application module test can be bootstrapped in a variety of modes:

* `STANDALONE` (default) -- Runs the current module only.
* `DIRECT_DEPENDENCIES` -- Runs the current module as well as all modules the current one directly depends on.
* `ALL_DEPENDENCIES` -- Runs the current module and the entire tree of modules depended on.

[[testing.efferent-dependencies]]
== Dealing with Efferent Dependencies

When an application module is bootstrapped, the Spring beans it contains will be instantiated.
If those contain bean references that cross module boundaries, the bootstrap will fail if those other modules are not included in the test run (see <<testing.bootstrap-modes>> for details).
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.

[source, java]
----
@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.
The dependencies should be reviewed for whether they are candidates for replacement by publishing a domain event (see <<events>>).

79 changes: 73 additions & 6 deletions src/docs/asciidoc/40-events.adoc
Original file line number Diff line number Diff line change
@@ -1,9 +1,76 @@
[[events]]
= Working with Application Events

[[events.subheadline]]
== Subheadline
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
To keep application modules as decoupled as possible from each other, their primary means of interaction should be event publication and consumption.
This avoids the originating module to know about all potentially interested parties, which is a key aspect to enable application module integration testing (see <<testing>>).

Often we will find application components defined like this:

[source, java]
----
@Service
@RequiredArgsConstructor
public class OrderManagement {
private final InventoryManagement inventory;
@Transactional
public void complete(Order order) {
// State transition on the order aggregate go here
// Invoke related functionality
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 <<testing.efferent-dependencies>>).
It also means that we will have to touch the class whenever we would like to integrate further functionality with the business event order completion.

[source, java]
----
@Service
@RequiredArgsConstructor
public class OrderManagement {
private final ApplicationEventPublisher events;
private final OrderInternal dependency;
@Transactional
public void complete(Order order) {
// State transition on the order aggregate go here
events.publishEvent(new OrderCompleted(order.getId()));
}
}
----

Note, how we use Spring's `ApplicationEventPublisher` to publish a domain event, once we have completed the state transitions on the primary aggregate.
As event publication happens synchronously by default, the transactional semantics of the overall arrangement stay the same as in the example above.
Both for the good, as we get to a very simple consistency model (either both the status change of the order _and_ the inventory update succeed or none of them does), but also for the bad as more triggered related functionality will widen the transaction boundary and potentially cause the entire transaction to fail, even if the functionality that is causing the error is not crucial.

A different way of approaching this is by moving the event consumption to asynchronous handling at transaction commit and treat secondary functionaly exactly as that:

[source, java]
----
@Service
@RequiredArgsConstructor
public class InventoryManagement {
@Async
@TransactionalEventListener
void on(OrderCompleted event) {
}
}
----

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 fully work, as the system might fail before the method is even invoked.

Spring Modulith ships with an event publication registry that hooks into the core event publication mechanism of Spring Framework.
On event publication, it finds out about the transactional event listeners that will get the event delivered and writes entries for each of them into an event publication log as part of the original business transaction.

0 comments on commit fb23c45

Please sign in to comment.