DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Designing a Java Connector for Software Integrations
  • Vibe Coding With GitHub Copilot: Optimizing API Performance in Fintech Microservices
  • Revolutionizing Financial Monitoring: Building a Team Dashboard With OpenObserve
  • Unlocking the Benefits of a Private API in AWS API Gateway

Trending

  • Microsoft Azure Synapse Analytics: Scaling Hurdles and Limitations
  • Why Database Migrations Take Months and How to Speed Them Up
  • *You* Can Shape Trend Reports: Join DZone's Software Supply Chain Security Research
  • Build Your First AI Model in Python: A Beginner's Guide (1 of 3)
  1. DZone
  2. Data Engineering
  3. Databases
  4. Mocking the java.time API for Better Testability

Mocking the java.time API for Better Testability

Date/time logic has many edge cases. Here, we'll look at strategies to bend the clock to your will for better testability.

By 
Jasper Sprengers user avatar
Jasper Sprengers
·
Aug. 14, 22 · Tutorial
Likes (5)
Comment
Save
Tweet
Share
22.6K Views

Join the DZone community and get the full member experience.

Join For Free

Date/times logic in code is where the messiness of the real world upsets the relatively straightforward rules of the digital realm. Blame the bewildering hodgepodge of edge cases on the movement of celestial bodies and pope Gregory XIII (of the Gregorian calendar) but deal with it you must. I’m sure you all know that the 31st of December can be in week 52 or 53, while the 1st of January can be in week 0 or 1 and that you know these rules by heart for various countries (oh yes!). I’m also confident you can hand-code the logic to calculate the difference in seconds between two representations that span multiple time zones as well as a jump in daylight saving time.

Don't Wait for the Next Leap Year

Forgive my sarcasm. Edge cases with time should be tested, especially if your code has homegrown date/time logic (which you should keep to a minimum). Application code deals with time representations that are either passed as one of the java.time classes, or as strings in some serialized format, e.g. 2022-12-03. For this category, you make up enough salient edges and make sure they are covered in unit tests. But often the code needs to know what the current time is. This new, unpredictable timestamp may just end up in a log statement, but it can also be the input to much more business-critical logic. Whenever you reference any of the *.now() methods, your program has introduced a side effect and become less deterministic.

The problem for traditional unit tests is obvious: We can’t afford to wait for the next leap day to ensure the code handles the 29th of February well.  So, we don’t usually test for it and hope all is well. I once witnessed an inexplicable crash that had the whole team puzzled. It disappeared the next day, on the first of March. It turned out just as we expected: a naïve, homegrown Date handling algorithm that would crash every four years.

Four Strategies to Manipulate the Clock

In this tutorial, I will offer some strategies to help ensure that code that asks for the current time can still be properly unit-tested. The approach in all cases is to intercept or replace the default API calls with a call to a mock or test double that returns a date/time configured in the test, rendering our production code deterministic again. Now, the best strategy is not to query the current time in a method when you can also receive it as a parameter. Minimize the places where the code needs to ask the time. There are four basic strategies with which testing code can manipulate the clock.

  • Wrap calls to the java.time API now() methods into a custom static class
  • Use an injectable custom DateTime service.
  • Inject a subclass of java.time.Clock.
  • Intercept static calls to the now() methods with a mocking framework.

The first three strategies are flexible in what you can handle but require some modest changes to the production code. The last option can only handle fixed dates but leaves your production code untouched, which can be a compelling reason to use it.

You can find all code samples in the accompanying GitLab project:

Shell
 
git clone git@gitlab.com:jsprengers/testing-for-time.git

Strategy 1: A Static Wrapper

Let’s look at the simplest variant: a static class that wraps a call to LocalDateTime.now()

Java
 
public static LocalDateTime currentDateTime() {
   return LocalDateTime.now();

}


In your production code, you use it as follows:

Java
 
public ExecutionResult runBatchWithWrapper() {
     LocalDateTime started = DateTimeWrapper.currentDateTime();
     simulateLengthyOperation();
     LocalDateTime finished = DateTimeWrapper.currentDateTime();
     return new ExecutionResult(started, finished);
 }


Suppose we want it to always return a fixed instant. In our test, we can configure this as follows:

Java
 
public class DateTimeWrapper {
 
     private static LocalDateTime instant; 
     public static void setFixed(Instant instant) {
         instant = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
     }
 
     public static LocalDateTime currentDateTime() {
         if (instant != null) {
             return instant;
         } else {
             return LocalDateTime.now();
         }
     }
 }


After calling setFixed(..) in your test, you get the same value whenever the current date/time is queried. You have effectively stopped the clock. That may however not always be what you want. Our sample code registers the difference between two invocations to the current time and these should not be the same. We want to move the clock forward for our test, but not have it stop ticking. Easy, we configure it to add a given duration to the current time. Each option invalidates the other. Offsets in the past are not supported here, but I'm sure you can implement that yourself.

Java
 
public class DateTimeWrapper {
 
     private static LocalDateTime instant;
     private static Duration offset;
 
     public static void setFixed(Instant instant) {
         DateTimeWrapper.instant = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
         offset = null;
     }
 
     public static void setOffset(Duration duration) {
         offset = duration;
         instant = null;
     }
 
     public static LocalDateTime currentDateTime() {
         if (instant != null) {
             return instant;
         } else if (offset != null) {
             return LocalDateTime.now().plus(offset);
         } else {
             return LocalDateTime.now();
         }
     }
 }


We can run the test as follows:

Java
 
@Test
 void runCustomDateTimeServiceWithFixedTime() {
     DateTimeWrapper.setFixed(fixedInstant);
     var result = service.runBatchWithWrapper();
     //assertions 
  }

Strategy 2: Injecting a Custom Mutable DateTimeService

Something feels off with this approach, though. It’s not great that production code has access to these setter methods. There’s a valid point in not introducing testing utilities in non-test code. A better, slightly more involved, strategy is to use dependency injection and have a production version next to a mutable test version. This can look as follows:

We define a simple DateTimeService interface

Java
 
public interface DateTimeService {
     LocalDateTime currentLocalDateTime();
 }


With the following production implementation:

Java
 
@Profile("!test")
@Service
public class DateTimeServiceImpl implements DateTimeService{
     public LocalDateTime currentLocalDateTime(){
         return LocalDateTime.now();
     }
}


And a mutable version for testing:

Java
 
//The test implementation contains the same logic as the static wrapper
@Profile("test")
@Service
public class MutableDateTimeService implements DateTimeService {
 
     @Override public LocalDateTime currentLocalDateTime() {
 		[...]
     }
}


And we use it as follows:

Java
 
@Autowired
DateTimeService dateTimeService;
 
public ExecutionResult runBatchWithCustomDateTimeService() {
     LocalDateTime started = dateTimeService.currentLocalDateTime();
     simulateLengthyOperation();
     LocalDateTime finished = dateTimeService.currentLocalDateTime();
     return new ExecutionResult(started, finished);
}


We have two implementations of the DateTimeService and only one of them is instantiated. Since the DateTimeServiceIntegrationTest runs under the ‘test’ profile, it picks the MutableDateTimeService instance. In production, Spring picks the default DateTimeServiceImpl.

Java
 
@SpringBootTest()
@Import(MutableDateTimeService.class)
@ActiveProfiles("test")
public class DateTimeServiceIntegrationTest {

}

Strategy 3: Injecting a java.time.Clock Instance

A similar injection-based approach that does not require you to write and configure a boilerplate production implementation uses the java.time.Clock class. The java.time API has an abstract class Clock that you can add as an argument to all the now() methods. By picking a different Clock, you can pretend that you’re in Beijing or that it’s forever 29 February 2016. In production, all that is needed is to inject a Clock instance and use it as follows:

Java
 
@Autowired
Clock clock;

public ExecutionResult runBatchWithClock() {
   LocalDateTime started = LocalDateTime.now(clock);
}


Unless otherwise configured, Spring picks java.time.Clock.SystemClock. If, in your test, you want to use a fixed date or an offset, you can use Clock.fixed(..) or Clock.offset(..) respectively in a Spring configuration class:

Java
 
@Configuration
public class TestConfig {
@Bean
Clock fixedClock(){
   return Clock.fixed(Instant.from(someDateTime), ZoneId.systemDefault());}
}


For test purposes, the standard implementations fall short if you want to manipulate your offset or fixed time dynamically because the fixed instants or offsets in Clock objects are immutable after construction. A likely use case would be to simulate thirty runs of a scheduled daily batch process, advancing the clock by 24 hours each time.  But Clock is not final, so we can make our own MutableClock version. It's very similar to all the other classes. Have a look at it in the gitlab project.

Strategy 4: Intercepting Static Invocations With Mockito

The last mocking method is an altogether different beast. It does not need dependency injection and leaves your production code entirely untouched. It comes down to intercepting the static method calls to the time API and configuring the responses with the help of the Mockito mocking framework. Mocking static methods is no longer the hassle it once was. It comes standard with Mockito since 3.4.0 but you need to add the extra Mockito-inline dependency (see the pom.xml in the project).

Java
 
@Test
void runMockedDateTimeWithFixedTime() {
    try (MockedStatic<LocalDateTime> mockedStatic = Mockito.mockStatic(LocalDateTime.class)) {
        mockedStatic.when(() -> LocalDateTime.now(ArgumentMatchers.any(Clock.class))).thenReturn(fixedLocalDateTime);
        var result = service.runBatchWithClock();
        assertThat(result.started()).isEqualTo(result.finished());
    }
    //notice that the static mocking is only in effect within the above try block, which is of course how we would want it.
    assertThat(LocalDateTime.now().getYear()).isGreaterThanOrEqualTo(2022);
}

Final Advice: Don't DIY

I hope you have found this tutorial useful. Let me close with a final word of advice, based on my experience with some clueless DIY date handling by inexperienced developers. The most compelling arguments for not re-inventing the wheel are the same reasons why you should never roll your own cryptography solution. Firstly, it’s harder than you think. The best wheel you can re-invent will still be unprepared for the bumpy road of unexpected edge cases. But more importantly, it’s a highly common requirement, so the generic solutions already handle almost everything you can possibly need. This goes also for any test utilities that I presented in this tutorial. Keep it simple.

API

Opinions expressed by DZone contributors are their own.

Related

  • Designing a Java Connector for Software Integrations
  • Vibe Coding With GitHub Copilot: Optimizing API Performance in Fintech Microservices
  • Revolutionizing Financial Monitoring: Building a Team Dashboard With OpenObserve
  • Unlocking the Benefits of a Private API in AWS API Gateway

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!