Did you ever feel the need to unleash your inner Doctor Strange and manipulate time concisely when testing your Spring application?
Let's check out the newest addition to the Spring ecosystem!
Spring Modulith
Spring Modulith supports developers implementing logical modules in Spring Boot applications. It allows them to apply structural validation, document the module arrangement, run integration tests for individual modules, observe the modules' interaction at runtime and generally implement module interaction in a loosely-coupled way.
Documentation
In this article we will focus on one specific area of Modulith, the Moments API.
Moments
The Moments API enables developers to easily react to the passage of time-based events in your application.
This allows you to easily write code which should be executed once a day, once a week etc.
Example application
I created a small sample application demonstrating features of the Moments API.
The code can be found on Github.
Let's walk through the application code together.
Setup
First we need to add the Modulith dependencies to our Spring application.
I'm using Maven to manage the dependencies in the sample application:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>0.6.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
and the actual dependency:
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-modulith-moments</artifactId>
</dependency>
Of course, we also want to test our application code, so we will add the test dependency as well:
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
Daily notifications
In the example application we want to send out notifications daily to our customers.
At the end of each day, we want to fetch all our customers and check if they opted in for receiving a notification on this day.
Let's create a simple Spring service called CustomerNotificationService
.
To listen to events emitted by Moments, we annotate a public method with @EventListener
and add the event we want to listen to as a method parameter:
@EventListener
public void on(final DayHasPassed dayHasPassed) {}
Now we can easily implement our business logic and let Moments take care of the rest:
@EventListener
public void on(final DayHasPassed dayHasPassed) {
var passedDate = dayHasPassed.getDate();
log.info("{} has passed. Checking notifications for customers.", passedDate);
for (var customer : customerService.getCustomers()) {
if (customer.allowedNotificationDays().contains(passedDate.getDayOfWeek())) {
eventPublisher.publishEvent(new CustomerNotificationEvent(this, customer.id(), passedDate.getDayOfWeek()));
}
}
}
For simplicity and brevity reasons the CustomerService
returns two hard-coded customers:
@Service
public class CustomerService {
public Collection<Customer> getCustomers() {
return List.of(new Customer(1, Set.of(DayOfWeek.MONDAY, DayOfWeek.FRIDAY)),
new Customer(2, Set.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)));
}
}
If the customer opted in for receiving notifications on this day, we simply emit a new CustomerNotificationEvent
which could then be picked up by some other service and actual send out the notification.
Since we care about our code, we want to ensure that it is working properly. Thankfully Modulith comes with great test support.
Time for some time bending :)
Testing time
The Moments API exposes a class called TimeMachine
.
With it, we can easily manipulate time in our tests.
In the following test we will enable the TimeMachine
by setting spring.modulith.moments.enable-time-machine=true
.
Additionally, we will annotate the test class with @ApplicationModuleTest
. This is another cool feature provided by Modulith.
With it only beans defined in the given module (in our case customer
) will be created upon test execution.
@ApplicationModuleTest
@Import(CustomerNotificationServiceTestConfig.class)
@TestPropertySource(properties = "spring.modulith.moments.enable-time-machine=true")
class CustomerNotificationServiceTest {
private final TimeMachine timeMachine;
CustomerNotificationServiceTest(final TimeMachine timeMachine) {
this.timeMachine = timeMachine;
}
@Test
void sendNotificationToCustomer(final PublishedEvents publishedEvents) {
for (var i = 0; i < 7; i++) {
timeMachine.shiftBy(Duration.ofDays(1));
}
assertThat(publishedEvents.ofType(CustomerNotificationEvent.class)
.matching(e -> e.getCustomerId() == 1))
.hasSize(2)
.extracting(CustomerNotificationEvent::getDayOfWeek)
.containsOnly(DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
assertThat(publishedEvents.ofType(CustomerNotificationEvent.class)
.matching(e -> e.getCustomerId() == 2))
.hasSize(2)
.extracting(CustomerNotificationEvent::getDayOfWeek)
.containsOnly(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
;
}
}
Bootstrapping with ApplicationModuleTest
If we run the test and check the logs we can see what @ApplicationModuleTest
does under the hood:
Bootstrapping @org.springframework.modulith.test.ApplicationModuleTest for Customer in mode STANDALONE (class dev.tobhai.modulithevents.ModulithEventsApplication)…
# Customer
> Logical name: customer
> Base package: dev.tobhai.modulithevents.customer
> Direct module dependencies: none
> Spring beans:
+ ….CustomerNotificationService
+ ….CustomerService
With the help of @ApplicationModuleTest
we can easily test the interaction between multiple beans defined in a module without the need for a full-blown Spring Context.
Shifting time in the test
Now to the time-bending part of the test:
First we define a fixed Clock
instance in the CustomerNotificationServiceTestConfig
.
Then we enable the TimeMachine
by setting: spring.modulith.moments.enable-time-machine
to true
.
Now we can simply inject the TimeMachine
instance into our test class and use it to shift time around in a very readable and concise way:
timeMachine.shiftBy(Duration.ofDays(1));
Assertions on emitted events
How can we actually verify that the CustomerNotificationEvent
was emitted properly?
Modulith helps us out in this scenario as well:
By adding PublishedEvents publishedEvents
to the signature of our test method, we can access all emitted events and perform assertions on them.
publishedEvents.ofType(CustomerNotificationEvent.class)
helps us to access all
.matching(e -> e.getCustomerId() == 1)CustomerNotificationEvent
s for customer with ID.
The time in our test starts on a Monday, and we shift "time" by seven days.
Therefore, we expect that two events (for Monday and Friday) are emitted for customer 1.
Wrap up
In this article we explored the Moments API of the Spring Modulith projects.
Additionally, we had a look at testing support provided Modulith.
Did you play around with Modulith yet?
If so, what is your favorite feature?
Feel free to follow for more Java related content :)
Cover photo by Aron Visuals on Unsplash
Top comments (5)
This is a great article, I only want to know if you know an alternative to avoid multiples instances problem.
Hi, thanks!
I'm not sure if I fully understand your question. Can you maybe rephrase it?
Yes, if I add this code to a microservice that it has at least 3 instances, the event will be launched 3 times, so I suppose you should have an specific microservice to run this kind of "jobs" or do you know how to avoid this?
You are correct, the event will be launched 3 times if you have 3 instances running.
If you need distributed job scheduling have a look at: JobRunr or db-scheduler!
thanks, for your article and the recomendations.