DEV Community

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

Posted on

Bending Time in Spring Applications

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?

Feel free to follow for more Java related content :)

Cover photo by Aron Visuals on Unsplash

Top comments (5)

Collapse
 
xavier_1023 profile image
Javier Solis Guzman

This is a great article, I only want to know if you know an alternative to avoid multiples instances problem.

Collapse
 
tobhai profile image
Tobias Haindl

Hi, thanks!
I'm not sure if I fully understand your question. Can you maybe rephrase it?

Collapse
 
xavier_1023 profile image
Javier Solis Guzman

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?

Thread Thread
 
tobhai profile image
Tobias Haindl

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!

Thread Thread
 
xavier_1023 profile image
Javier Solis Guzman

thanks, for your article and the recomendations.