Software systems often use date and time information. For example, you might need the current date to add a special discount to the customer’s shopping cart, or you might need the current time to start a batch processing job. To fully exercise some pieces of code, our tests need to provide different dates and times as input.
Given that date and time operations are common, a best practice is to wrap them into a dedicated class (often called Clock
). Let’s show that using an example:
The program should give a 15% discount on the total amount of an order if the current date is Christmas. There is no discount on other dates.
A possible implementation for this requirement is shown next.
Listing 6.18 ChristmasDiscount
implementation
public class ChristmasDiscount {
public double applyDiscount(double amount) {
LocalDate today = LocalDate.now(); ❶
double discountPercentage = 0;
boolean isChristmas = today.getMonth() == Month.DECEMBER
&& today.getDayOfMonth() == 25;
if(isChristmas) ❷
discountPercentage = 0.15;
return amount - (amount * discountPercentage);
}
}
❶ Gets the current date. Note the static call.
❷ If it is Christmas, we apply the discount.
The implementation is straightforward; given the characteristics of the class, unit testing seems to be a perfect fit. The question is, how can we write unit tests for it? To test both cases (Christmas/not Christmas), we need to be able to control/stub the LocalDate
class, so it returns the dates we want. Right now, this is not easy to do, given that the method makes explicit, direct calls to LocalDate.now()
. The problem is analogous when InvoiceFilter
instantiated the IssuedInvoices
class directly: we could not stub it.
We can then ask a more specific question: how can we stub Java’s Time API? In particular, how can we do so for the static method call to LocalDate.now()
? Mockito allows developers to mock static methods so we could use this Mockito feature.
Another solution (which is still popular in code bases) is to encapsulate all the date and time logic into a class. In other words, we create a class called Clock
, and this class handles these operations. The rest of our system only uses this class when it needs dates and times. This new Clock
class is passed as a dependency to all classes that need it and can therefore be stubbed. The new version of ChristmasDiscount
is in the following listing.
Listing 6.19 The Clock
abstraction
public class Clock {
public LocalDate now() { ❶
return LocalDate.now();
}
// any other date and time operation here...
}
public class ChristmasDiscount {
private final Clock clock; ❷
public ChristmasDiscount(Clock clock) { ❷
this.clock = clock;
}
public double applyDiscount(double rawAmount) {
LocalDate today = clock.now(); ❸
double discountPercentage = 0;
boolean isChristmas = today.getMonth() == Month.DECEMBER
&& today.getDayOfMonth() == 25;
if(isChristmas)
discountPercentage = 0.15;
return rawAmount - (rawAmount * discountPercentage);
}
}
❶ Encapsulates the static call. This seems too simple, but think of other, more complex operations you will encapsulate in this class.
❷ Clock is a plain old dependency that we store in a field and receive via the constructor.
❸ Calls the clock whenever we need, for example, the current date
Testing it should be easy, given that we can stub the Clock
class (see listing 6.20). We have two tests: one for when it is Christmas (where we set the clock to December 25 of any year) and another for when it is not Christmas (where we set the clock to any other date).
Listing 6.20 Testing the new ChristmasDiscount
public class ChristmasDiscountTest {
private final Clock clock = mock(Clock.class); ❶
private final ChristmasDiscount cd = new ChristmasDiscount(clock);
@Test
public void christmas() {
LocalDate christmas = LocalDate.of(2015, Month.DECEMBER, 25);
when(clock.now()).thenReturn(christmas); ❷
double finalValue = cd.applyDiscount(100.0);
assertThat(finalValue).isCloseTo(85.0, offset(0.001));
}
@Test
public void notChristmas() {
LocalDate notChristmas = LocalDate.of(2015, Month.DECEMBER, 26);
when(clock.now()).thenReturn(notChristmas); ❸
double finalValue = cd.applyDiscount(100.0);
assertThat(finalValue).isCloseTo(100.0, offset(0.001));
}
}
❷ Stubs the now() method to return the Christmas date
❸ Stubs the now() method. It now returns a date that is not Christmas.
As I said, creating an abstraction on top of date and time operations is common. The idea is that having a class that encapsulates these operations will facilitate the testing of the other classes in the system, because they are no longer handling date and time operations. And because these classes now receive this clock abstraction as a dependency, it can be easily stubbed. Martin Fowler’s wiki even has an entry called ClockWrapper
, which explains the same thing.
Is it a problem to use Mockito’s ability to mock static methods? As always, there are no right and wrong answers. If your system does not have complex date and time operations, stubbing them using Mockito’s mockStatic()
API should be fine. Pragmatism always makes sense.
Leave a Reply