samedi 29 juin 2013

How to test dates in java unit tests?

Testing dates in unit tests is not always an easy stuff. Take a look to the following method, I will then explain you what is the main problem you can encounter.

public class UserService {
  @Inject
  private UserEventDao userEventDao;

  @Inject
  private DateUtility dateUtility;

  public void createCreationUserEvent(User user) {
    UserEvent event = new UserEvent();
    event.setUser(user);
    event.setUserEventType(UserEventType.CREATION);
    event.setEventDate(new Date());
    userEventDao.create(event);
  }
}

To test this method, you have to check that the UserEvent object passed to the UserEventDao.create method is correctly filled. For that you can use a mock framework like Mockito and write the following test :

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
  @InjectMocks
  private UserService userService;

  @Mock
  private UserEventDao userEventDao;

  @Test
  public void createCreationUserEvent_withCorrectParameters_shouldCreateAnEvent() {
    // Given
    User user = new User();

    // When
    userService.createCreationUserEvent(user);

    // Then
    UserEvent expectedUserEvent = new UserEvent();
    expectedUserEvent.setUser(user);
    expectedUserEvent.setUserEventType(UserEventType.CREATION);
    expectedUserEvent.setEventDate(new Date());

    verify(userEventDao).create(expectedUserEvent);
    verifyNoMoreInteractions(userEventDao);
  }
}

The problem is that this test is not in success all the time because the Date object precision is in milliseconds and you can have a little difference between the date created in the method and the date created in the test.

The solution to resolve that is to be sure to manipulate the same Date object between the method and your test. Of course you could add a Date argument in your createCreationUserEvent method but this would just move our problem to the calling method.

I recommend you two solutions to do that : the first one is to use PowerMock and the second one is to create a DateUtility class.

Solution 1 : Using PowerMock

PowerMock is a mock framework allowing you to mock what cannot be mocked by others frameworks : static method calls, private methods, object constructions etc. To do that, Powermock manipulates the bytecode of the class to test.

So in our example, it can mock the new Date() call :
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserService.class)
public class UserServiceTest {

  @InjectMocks
  private UserService userService;

  @Mock
  private UserEventDao userEventDao;

  @Test
  public void createCreationUserEvent_withCorrectParameters_shouldCreateAnEvent()
  throws Exception {
    // Given
    User user = new User();
    Date eventDate = new Date();
    whenNew(Date.class).withNoArguments().thenReturn(eventDate);

    // When
    userService.createCreationUserEvent(user);

    // Then
    UserEvent expectedUserEvent = new UserEvent();
    expectedUserEvent.setUser(user);
    expectedUserEvent.setUserEventType(UserEventType.CREATION);
    expectedUserEvent.setEventDate(eventDate);

    verify(userEventDao).create(expectedUserEvent);
    verifyNoMoreInteractions(userEventDao);
  }
}

As you surely noticed, we use the PowerMock runner and a PrepareForTest annotation which indicates to Powermock that the UserService class bytecode must be modified.

You can now be sure that your test will be in success at each execution.

Solution 2 : Creating a date utility class

In this solution, the concept is to delegate the creation of the date to a new class :

public class DateUtility {
  public Date getCurrentDate() {
    return new Date();
  }
}

And to use this class in UserService :

public class UserService {

  @Inject
  private UserEventDao userEventDao;

  @Inject
  private DateUtility dateUtility;
  
  public void createCreationUserEvent(User user) {
    UserEvent event = new UserEvent();
    event.setUser(user);
    event.setUserEventType(UserEventType.CREATION);
    event.setEventDate(dateUtility.getCurrentDate());
    userEventDao.create(event);
  }
}

Then we can mock the call to DateUtility in our unit test :

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

  @InjectMocks
  private UserService userService;

  @Mock
  private UserEventDao userEventDao;
  
  @Mock
  private DateUtility dateUtility;

  @Test
  public void createCreationUserEvent_withCorrectParameters_shouldCreateAnEvent() {
    // Given
    User user = new User();
    Date eventDate = new Date();
    doReturn(eventDate).when(dateUtility).getCurrentDate();

    // When
    userService.createCreationUserEvent(user);

    // Then
    UserEvent expectedUserEvent = new UserEvent();
    expectedUserEvent.setUser(user);
    expectedUserEvent.setUserEventType(UserEventType.CREATION);
    expectedUserEvent.setEventDate(eventDate);

    verify(userEventDao).create(expectedUserEvent);
    verifyNoMoreInteractions(userEventDao);
  }
}

Like in the Powermock solution, you can now be sure that your test will be in success at each execution.

Conclusion : Powermock vs DateUtility

Firstly, these two solutions can be applied to every other case where you have to manipulate dates. It works all the time.

The advantage of the Powermock solution is that you don't have to modify your business code to write a good test. However, the bytecode manipulation done by the framework is expensive in term of execution time : in my environment, this simple test needs about 400 ms to be executed whereas the test with Mockito needs almost 0 ms. On more complicated tests with several static classes mocked, this execution time can be even worst : I already saw entire tests classes executed in almost 8 seconds, which can be very problematic in term of productivity.

Concerning the DateUtility solution,  I think it is elegant to have a class with the responsability to manipulate dates. With that, you shouldn't have any "new Date()" call or Calendar manipulation in other classes. And of course, the main bonus is that you can write a good unit test. I would so recommend you this solution!

I hope you enjoyed this thread and I would be very glad to hear which others tricks do you use to test your dates. Also, I you encounter a case where the DateUtility solution cannot help you, I would love to help you.

Aucun commentaire:

Enregistrer un commentaire