Spring Builders

Cover image for Bending Time in Spring Applications
Tobias Haindl
Tobias Haindl

Posted on • Updated on

Bending Time in Spring Applications

Disclaimer: this post was orginally published on dev.to.

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>
Enter fullscreen mode Exit fullscreen mode

and the actual dependency:

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-modulith-moments</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode

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()));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)));
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
        ;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
.matching(e -> e.getCustomerId() == 1)
helps us to access all CustomerNotificationEvents 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?

Cover photo by Aron Visuals on Unsplash

Top comments (2)

Collapse
 
odrotbohm profile image
Oliver Drotbohm

Nice one, thanks for the write-up! 🙇

Collapse
 
tobhai profile image
Tobias Haindl • Edited

Thanks! Excited to try out other features of Modulith as well :)