But now I've got something, even though it's definitely a simpler topic.
I've recently written an integration test for a blueMarine [1] 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 ofSystem.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.TimestampProviderSystem.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!).TimestampProvider interface might look like:package it.tidalwave.metadata;
public interface TimestampProvider
{
public Date getTimestamp();
}
package it.tidalwave.metadata;The implementation might be:
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;
}
}
}
package it.tidalwave.metadata.impl;And the service registration just consists in creating a file META-INF/services/it.tidalwave.metadata.TimestampProvider which contains it.tidalwave.metadata.impl.TimestampProviderImpl.
import java.util.Date;
import it.tidalwave.metadata.TimestampProvider;
public class TimestampProviderImpl implements TimestampProvider
{
public Date getTimestamp()
{
return new Date();
}
}
System.currentTimeMillis() withDate 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
package it.tidalwave.bluemarine.metadata.impl;Note that I'm cloning
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());
}
}
Date before returning from getTimestamp() since that class is not immutable.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:

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.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.ThreadLocal, which offers a unique storage per thread; in other words, every thread in this case will see its own sequence of timestamps.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());
}
}
| Attachment | Size |
|---|---|
| T1.jpg [2] | 151.38 KB |
| T2.jpg [3] | 150.09 KB |
Links:
[1] http://bluemarine.tidalwave.it/
[2] http://netbeans.dzone.com/sites/all/files/T1.jpg
[3] http://netbeans.dzone.com/sites/all/files/T2.jpg