4

I have a Util class with 1 method which calculates age using joda-time:

public static int getAge(LocalDate birthdate) {
    LocalDate today = new LocalDate();
    Period period = new Period(birthdate, today, PeriodType.yearMonthDay());
    return period.getYears();
}

I've written an extensive JUnit test class but how do I make it time-independent? If I create a test case to calculate someone's age if they were born today in 1970 then the result would be 46 years old. But 1 year from now if i run the test case it would fail because the result would be 47.

So how do I make these test cases time-independent. I was thinking of having some sort of calendar interface that the test cases would create a date object from. I also stumbled across this post which is another possible solution to this but I'm not really sure how to go about it.

Community
  • 1
  • 1
Richard
  • 5,840
  • 36
  • 123
  • 208

4 Answers4

4

A common practice (described in the book Growing Object Oriented Software guided by Tests) is to delegate the responsability of creating the date instance to a collaborator.

Imagine you have a class like this:

public class Clock {
  public LocaleDate now(){
    return  new LocalDate();
  }
}

then your code could become

public static int getAge(LocalDate birthdate, Clock clock) {
    LocalDate today = clock.now();
    Period period = new Period(birthdate, today, PeriodType.yearMonthDay());
    return period.getYears();
}

I would be fine not testing Clock#now as it is trivially correct, but I can now pass in a mocked clock instance to test the getAge method.

Now this means a class using the static getAge must provide a clock, the clock would be a dependency for this class. Such dependencies can be injected in various mockable ways.

I personnally am of the injection by constructor school, but anything from injection by reflection to proctected factory method (which can therefore be overloaded in an anonymous subclass used to create a test instance) works.

This problem and the design I suggest are discussed at length in the book, in many blog posts and in stack overflow (it looks like java 8 has direct support for it if you are willing to move to java.time)

Community
  • 1
  • 1
Jean
  • 21,329
  • 5
  • 46
  • 64
3

Use fixed date :

    LocalDate today = new LocalDate(2016, 1, 5);
Mrinal
  • 1,846
  • 14
  • 17
  • Wow....I'm not sure why I didn't think of this......the most obvious solution...thank you – Richard Jan 05 '16 at 19:26
  • actually this doesn't work now that I think about it because the `new LocalDate` is inside the getAge function and I can't hardcode a value in there. It has to be current date when in production use. So I'm wondering if there's some sort of way to replace that using `Mockito.when` – Richard Jan 05 '16 at 19:49
  • You can very well use `Mockito.when`. Or you can override the `getAge` method to return static date for your test case, if permitted. – Mrinal Jan 05 '16 at 19:54
  • i would rather go the `Mockito.when` route but I'm not sure how to implement that in this case. – Richard Jan 05 '16 at 19:56
1

So, first let me say I agree with Jean. However I have found that this pattern is so common I have implemented a reusable solution in a library on GitHub called LibEx. It is called DateSupplier. To get the current DateTime you use DateSupplier.getCurrentDateTime(). Then in the tests you use a @Rule DateController which allows for datController.setCurrentDateTo...(). When this is set in a test the DateSupplier will return the value as specified by the DateController.

Here are the links:

Your test would then look like this..

public class DateControllerTest extends TestBase {

@Rule
public DateController dateContoller = new DateController();

@Test
public void testSetCurrentTime() {
    // setup
    Date date = new Date(123443);

    // test
    dateContoller.setCurrentTime(date);

    // verify
    assertDateEquals(date);
}

private void assertDateEquals(Date date) {
    assertThat(DateSupplier.getCurrentDate(), equalTo(date));
    assertThat(DateSupplier.getCurrentDateInMilliseconds(), equalTo(date.getTime()));
}

Because DateController is a Rule, it will reset to the default behavior after each test. Also, DateController is in the libex-test module that should be test scoped, so it is in deployed in your production code.

John B
  • 32,493
  • 6
  • 77
  • 98
-1

So you want the birthdate of someone turning 46 today? (Your question is not clear.)

java.time

The makers of Joda-Time tell us to move to using the java.time framework built into Java 8 and later as soon as is comfortable.

ZonedDateTime birthdateOf46YearOld = ZonedDateTime.now().minusYears( 46 );

This object is implicitly assigned the JVM’s current default time zone. I recommend always being explicit with your time zone.

ZoneId zoneId = ZoneId.of( "America/Montreal" );  // Or… ZoneOffset.UTC …or… ZoneId.systemDefault().
ZonedDateTime birthdateOf46YearOld = ZonedDateTime.now( zoneId ).minusYears( 46 );

Or do you want to test if a date-time value in hand is the same as that birthdate?

Boolean hit = someZdt.isEqual( birthdateOf46YearOld );

If you care only about date without time-of-day or time zone, then use LocalDate, similar to Joda-Time. Note that time zone is crucial in determining today’s date as it varies across time zones around the globe.

LocalDate birthdate = LocalDate.now( zoneId ).minusYears( 46) ;

If you simply want the elapsed time in years between a pair of LocalDate objects, use the Period class.

LocalDate today = LocalDate.now( zoneId );
LocalDate birthDate = … ;
Period period = Period.between( birthDate , today );
Integer ageInYears = period.getYears();
Boolean hit = ageInYears.equals ( Integer.valueOf( 46 ) ) ;    

Joda-Time

If Java 8 or later is not available to you, the same kind of code works in Joda-Time.

DateTimeZone zone = DateTimeZone.forID( "America/Montreal" );
DateTime birthdate = DateTime.now( zone ).minusYears( 46 );
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154