Cloud Platform Engineering, DevOps, SRE, Kubernetes, KOPS

Unit testing and Date equality

Date equality is one of those things which could be painful to deal with when writing unit tests. If you are reading this post, I guess you came across this problem at some point in time 🙂 There are different approaches that could be taken to make tests pass consistently when java.util.Date is used as part of a model object. To make examples more clear, assume you are working with a system that records some sort of Events and Event has a variable createdOn of type java.util.Date.

Let’s consider some possible solutions to make tests to pass consistently:

  1. Implement hashCode/equals and exclude any Date objects – seems like a bad idea in most cases I came across. If Date is part of a model object, it should be part of hashCode/equals.

  2. Call new Date() through i.e. DateFactory and mock it when running tests. The down side of this solution is that you either pass that factory to constructor/setter of Event or pass it to each class that creates Event objects and create date there:

//1. passing DateFactory to constructor
public Event(DateFactory dateFactory) {
    this.createdOn = dateFactory.now();
}

//2. or create event through service...
public void recordUserEvent(String message){
    Event event = new Event(message);
    event.setCreatedOn(dateFactory.now());
    //do something with event
}

The problem is that you need to introduce an ‘artificial’ class (DateFactory) just to make unit tests pass. Also, you need to take care of providing instance of DateFactory each time you need it, which makes production and test code slightly more complex.

  1. Use org.joda.time.DateTime.now().toDate() to get dates and then fix time inside of unit test. Time could be fixed with DateTimeUtils.setCurrentMillisFixed(0) and unfix with DateTimeUtils.setCurrentMillisSystem() – to me that’s the most clean and least invasive solution. You can set the date without dependency on class (DateFactory) which would need to be initialised and passed around somehow (autowiring, injecting, etc). Now, DateTime could be used anywhere in your code and doesn’t dictate you where Date object is created and doesn’t introduce unnecessary dependencies on ‘artificial’ classes.

Checkout this project from a git repo if you like to see the problem of unit testing and new Date() in action and how DateTime solves it. Just edit constructor of Event class to use new Date() and run the unit test. Optionally copy-paste this code into your favourite IDE. Relevant bits are highlighted.

public class Event {
    enum EventType {
        SYSTEM, USER
    }

    private EventType eventType;
    private Date createdOn;
    private String message;

    public Event(EventType eventType, String message) {
        this.eventType = eventType;
        this.message = message;

        // to see test failure, use new Date() instead of DateTime
        // this.createdOn = new Date();
        this.createdOn = DateTime.now().toDate();

    }

    public EventType getEventType() {
        return eventType;
    }

    public Date getCreatedOn() {
        return createdOn;
    }

    public String getMessage() {
        return message;
    }

    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this, true);
    }

    public boolean equals(Object o) {
        return EqualsBuilder.reflectionEquals(this, o, true);
    }

    public String toString() {
        // appended createdOn.getTime() so that when unit test fails, you can
        // see millisecond of difference
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE) + "; createdOn " + createdOn.getTime();
    }
}
public class EventService {
	private final EventDAO eventDAO;

	public EventService(EventDAO eventDAO){
		this.eventDAO = eventDAO;
	}

	public void recordUserEvent(String message){
		eventDAO.storeEvent(new Event(USER, message));
	}

	public void recordSystemEvent(String systemUUID, String eventCode){
		String message = systemUUID + ": " + eventCode;
		eventDAO.storeEvent(new Event(SYSTEM, message));
	}
}
public interface EventDAO {
	void storeEvent(Event event);
}
public class EventServiceTest {

	private EventService eventService;
	private EventDAO eventDAO;

	@Before
	public void setUp() {
		eventDAO = mock(EventDAO.class);
		eventService = new EventService(eventDAO);
		DateTimeUtils.setCurrentMillisFixed(0);
	}

	@After
	public void after() {
		DateTimeUtils.setCurrentMillisSystem();
	}

	@Test
	public void storeUserEventTest() {

		//run multiple times to see failures when using new Date() instead of DateTime.now().toDate()
		for (int i = 0; i < 3000; i++) {
			String message = "User did something..." + i;

			Event expectedEvent = createExpectedEvent(EventType.USER, message);
			eventService.recordUserEvent(message);
			verify(eventDAO).storeEvent(expectedEvent);

			// just checking you are not cheating ;-)
			assertThat(
					"createdOn must be set on Event to demonstrate the example",
					expectedEvent.getCreatedOn(), is(notNullValue()));
		}
	}

	private Event createExpectedEvent(EventType eventType, String message) {
		return new Event(eventType, message);
	}
}