Over a million developers have joined DZone.

Mocking Time with NetBeans RCP

· Java Zone

Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code! Brought to you in partnership with ZeroTurnaround.

Rick and Geertjan have invited me to write here on NetBeans DZone since a long time and I'm guilty of not having reacted to that so far. Indeed I had written a pretty code-dense article a few months sgo (at the time for JavaLobby), but for some reasons that I won't tell you now I can't publish it at the moment.

But now I've got something, even though it's definitely a simpler topic.

I've recently written an integration test for a blueMarine component that imports the metadata from a batch of photos and store them into a database. The test then dumps the database to a flat file after the import and compares it with an expected file dump. Whenever a new photo is added to the test suite or some data changes because of a fix or a different approach in storing it, I just "accept" the new file by copying it over the previous expected file.

Pretty simple, but with a major problem: timestamps. In fact, one of the columns contains the timestamp of the import operation, obtained by means of System.currentTimeMillis(), and of course the returned values differ from run to run, thus making the dump inconsistent. How to solve this? I'm going to illustrate what I've done with some code which is specific for NetBeans RCP; in any case, the idea could be easily implemented if you use other technologies, such as Spring.


A TimestampProvider

The answer is: mocking the time provider. Well, this implies that I need a time provider, since System.currentTimeMillis() is unfortunately a static method and can't be e.g. replaced by polymorphism (that's one of the reasons for which static methods are considered harmful - stay away of them!).

This is how a TimestampProvider interface might look like:

package it.tidalwave.metadata;

public interface TimestampProvider
{
public Date getTimestamp();
}

Since I'm working with NetBeans RCP, I can use its default mechanism to register services: they must be declared in META-INF/services folders and retrieved by means of Lookup. So the complete listing for the interface is:


package it.tidalwave.metadata;

public interface TimestampProvider
{
public Date getTimestamp();

public static final class Locator
{
private Locator()
{
}

public static TimestampProvider findTimestampProvider()
{
final TimestampProvider timestampProvider = Lookup.getDefault().lookup(TimestampProvider.class);

if (timestampProvider == null)
{
throw new RuntimeException("Cannot find TimestampProvider");
}

return timestampProvider;
}
}
}
The implementation might be:


package it.tidalwave.metadata.impl;

import java.util.Date;
import it.tidalwave.metadata.TimestampProvider;

public class TimestampProviderImpl implements TimestampProvider
{
public Date getTimestamp()
{
return new Date();
}
}
And the service registration just consists in creating a file META-INF/services/it.tidalwave.metadata.TimestampProvider which contains it.tidalwave.metadata.impl.TimestampProviderImpl.

At this point I just had to replace all the occurrences of System.currentTimeMillis() with


Date timestamp = TimestampProvider.Locator.findTimestampProvider().getTimestamp();
In your tests, you could for instance define a MockTimestampProviderImpl that provides different values for the time. To have the mock service registered for tests in place of the standard service, you just need a new file META-INF/services/it.tidalwave.metadata.TimestampProvider, this time stored under test/unit/src instead of src: NetBeans will use it only when running JUnit instead of putting it in the production distribution. The contents of the file are such as:


#-it.tidalwave.metadata.impl.TimestampProviderImpl
it.tidalwave.bluemarine.metadata.impl.MockTimestampProviderImpl

While the dash is normally a comment, the dash+minus combination is an extension of NetBeans RCP and it means that you want to unregister a previously registered service; then the replacement is provided in the line below.


Sample and hold

Good. How to mock time in tests? Well, first one could think of having time restart from zero, or a default base time, at the beginning of each test run. This is not enough, in any case, since you cannot guarantee that every operation will be performed in the very same timing, to the millisecond! Heck, this is not a real-time application and indeed time could change for a number of reasons, in primis the fact that you're also optimizing the code so it will hopefully execute faster and faster (not counting the idea of running tests on different computers and/or with different CPU loads).

The only solution I see is to switch to a "discrete" model for the time. That is, every invocation of getTimeStamp() might just increase the time with a constant value. For instance:


package it.tidalwave.bluemarine.metadata.impl;

import java.util.Date;
import it.tidalwave.metadata.TimestampProvider;

public class MockTimestampProviderImpl implements TimestampProvider
{
private Date previous;

public Date getTimestamp()
{
final Date now = (previous != null) ? new Date(previous.getTime() + 1000) : new Date(2008 - 1900, 0, 1);
date = previous;
return new Date(now.getTime());
}
}
Note that I'm cloning Date before returning from getTimestamp() since that class is not immutable.

Is it ok? Not yet. Unfortunately the results depend on how many times you invoke getTimestamp() from your code. In fact, you would be forced to store a copy of the timestamp value and propagate it to the other places of code needing it in a consistent way; but this would introduce unneeded couplings into the code. Thinking of it twice, it's not only a test problem: this leads to an issue - even though a light one - even in production. For instance, think of a set of records logically belonging to the same group that could be independently timestamped with slightly different values during the insertion into the database:


It would be a better thing if the timestamp for them was the same:


A possible solution is to introduce a sample-and-hold approach: you would call a sample() method at the beginning of each logical group of operations and the returned timestamp would be the same for any further getTimeStamp() invocation until the next sample(). You could even think of calling sample() at the beginning of each regular transaction.


Multithreading

Done? Not yet! What if you have multiple threads? If in production code you are using the sample-and-hold technique bound to the current transaction and you have multiple concurrent transactions from multiple threads, each call of sample() would change values for all threads, typically in the middle of a transaction, which is not good. But even if you're running a single test and you're not interested in keeping the timestamp constant in any transaction, you could still have problems: since thread scheduling is unpredictable, you can't guarantee that every thread runs through the same sequence of sample() calls, thus there's no guarantee to have a consistent result of the test.

Fortunately, this is an easy problem to solve. Instead of saving the current value of the timestamp in a simple reference, you can use a ThreadLocal, which offers a unique storage per thread; in other words, every thread in this case will see its own sequence of timestamps.

Now, while per-thread timestamps are for sure the best solution in production code, for tests things are not as easy because of the need for perfect reproducibility. If each thread produces its own set of data, per-threads sequences are the proper choice. If threads merge their results into a single bag of data (e.g. think of a Master/Worker pattern that consumes a queue of data to process, elaborates and populates a database), per-thread sequences are still ok since, independently of thread scheduling, each data item would be processed in the same order and would get the same timestamp. But in more complicated case, things could be not as easy.

These are the final interfaces and implementations with the ThreadLocal approach, taken from the real thing. For the sake of flexibility, I've separated getTimestamp(), which returns the regular time, from getSampledTimestamp().

public interface TimestampProvider
{
public Date getTimestamp();

public Date getSampledTimestamp();

public Date sample();

public static final class Locator
{
private Locator()
{
}

public static TimestampProvider findTimestampProvider()
{
final TimestampProvider timestampProvider = Lookup.getDefault().lookup(TimestampProvider.class);

if (timestampProvider == null)
{
throw new RuntimeException("Cannot find TimestampProvider");
}

return timestampProvider;
}
}
}

public class TimestampProviderImpl
{
private final ThreadLocal<Date> dateHolder = new ThreadLocal<Date>();

public Date getTimestamp()
{
return new Date();
}

public Date getSampledTimestamp()
{
if (dateHolder.get() == null)
{
sample();
}

return new Date(dateHolder.get().getTime());
}

public Date sample()
{
final Date now = new Date();
dateHolder.set(now);
return new Date(now.getTime());
}
}

public class MockTimestampProviderImpl implements TimestampProvider
{
private final Date INITIAL_TIMESTAMP = new Date(2008 - 1900, 0, 1);
private final ThreadLocal<Date> dateHolder = new ThreadLocal<Date>();

public Date getTimestamp()
{
return new Date();
}

public Date getSampledTimestamp()
{
if (dateHolder.get() == null)
{
sample();
}

return new Date(dateHolder.get().getTime());
}

public Date sample()
{
final Date previous = dateHolder.get();
final Date now = (previous != null) ? new Date(previous.getTime() + 1000) : INITIAL_TIMESTAMP;
dateHolder.set(now);
return new Date(now.getTime());
}
}

The Java Zone is brought to you in partnership with ZeroTurnaround. Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code!

Topics:

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}