Contents
Refactor
If you have a method that callsSystem.currentTimeMillis()
or
new java.util.Date()
,
that method will be receiving a different value when you run it on
a different day or time.
This contradicts the need to have exact control over all inputs
for unit and regression testing.
You need to refactor your method so that you can control the date
or time value used by your method and thus be able to have
repeatable and controllable tests.
To start, create a utility class with a method corresponding to each of the time-variant methods
System.currentTimeMillis()
,
new Date()
,
or any other method whose value is a function of the current date and time.
Modify your code to replace all calls toimport java.util.Date; public class TimeVariant { public static long currentTimeMillis() { return System.currentTimeMillis(); } public static Date newDate() { return new Date(); } }
new Date()
by calls to TimeVariant.newDate()
,
replace all calls to System.currentTimeMillis()
by calls to TimeVariant.currentTimeMillis()
, etc.
If your application uses a database and takes advantage of any automatic timestamping by the database server, you will have to figure out a way to deal with those values. I'm not going to try to cover unit testing with a database; that is a big enough topic to warrant a separate post.
Upon completing this step, you have not changed any of the functionality of your code, but you have centralized all of the time-variant calls into a single location. Now we can modify the
TimeVariant
class
to make it do what we want.
Test Hook
The general goal is to have methods inTimeVariant
that behave as defined above during normal operation, but for
which we can control the return value during testing.
In order to keep the testing code out of the main code,
we want to make minimal changes to TimeVariant
and put as much
of the support code as possible into another class that we use
only in our testing environment.
We start by defining an interface that duplicates the functionality of the
TimeVariant
class:
We will be writing a test classimport java.util.Date; public interface ITimeVariant { public long currentTimeMillis(); public Date newDate(); }
TestTimeVariant
that implements this interface.
If you are already using an injection framework such as
Spring,
you can modify our original TimeVariant
class so that it implements
the ITimeVariant
interface with non-static methods,
make your code use the instance of ITimeVariant
supplied by
your injection framework,
and configure the framework to use an instance of
TimeVariant
in normal use
and TestTimeVariant
during testing.
If you are not using an injection framework, one way to allow using the methods in
TestTimeVariant
during testing is to modify TimeVariant
to allow turning it into a proxy for an instance of ITimeVariant
.
Changes from the previous version of this class above
are shown in bold.
From the setup method for your unit test you set the hook:import java.util.Date; public class TimeVariant { private static ITimeVariant tvDelegate = null; /** This method is for unit testing setup. */ protected static void setDelegate(ITimeVariant tv) { tvDelegate = tv; } public static long currentTimeMillis() { if (tvDelegate!=null) return tvDelegate.currentTimeMillis(); else return System.currentTimeMillis(); } public static Date newDate() { if (tvDelegate!=null) return tvDelegate.newDate(); else return new Date(); } }
Once the hook is in place to invoke our test version of the time variant calls, we are done with the changes to the production code and we can move on to the implementation of the test class.ITimeVariant tv = new TestTimeVariant(); TimeVariant.setDelegate(tv);
This post uses Java for all of the code examples, but the same approach as described here can be used in other languages (Ruby, C#), where their language features can make setting up the hook and delegate more elegant than in Java.
Controlled Values
Recall that the primary purpose of the test class is to ensure that we return a known and controlled value for each call to one of our methods. In the simplest case, we can just return a constant value, in which case ourTestTimeVariant
class might look like this:
The above code satisfies our desire to have a controlled value, and it may be perfectly satisfactory for testing simple methods that callimport java.util.Date; public class TestTimeVariant implements ITimeVariant { private long simulatedTime = 1234567890L*1000; //a recent timestamp public long currentTimeMillis() { return simulatedTime; } public Date newDate() { return new Date(simulatedTime); } }
newDate()
and use only the date portion
or that call currentTimeMillis()
only once,
but it can be a bit unrealistic when our methods are called more than once.
Changing Values
We can modify the methods in ourTestTimeVariant
class to return different values on each call.
To start, we add a method that allows us to set the simulated current time. This allows us to set the date and time to specific values in order to test corner cases on our code, such as February 29 or December 31 on a leap year, or whatever other conditions are considered special by the method under test.
With theimport java.util.Date; public class TestTimeVariant implements ITimeVariant { private long simulatedTime = 1234567890L*1000; //a recent timestamp public void setNow(long t) { simulatedTime = t; } public long currentTimeMillis() { return simulatedTime; } public Date newDate() { return new Date(simulatedTime); } }
setNow
test method, we can create a unit test
that alternately calls production code and changes the time.
This is suitable when testing methods that contain a single call
to one of our time methods,
but if a method makes more than one call before returning,
it will get the same return value every time.
To return different values when there are multiple calls where we are not able to call
setNow
between then,
we modify our test code to add a fixed delta to the returned value
on each call.
If we want more precise control, we can add the ability to preset a list of values to be returned, or equivalently a list of delta values to be added on each call:import java.util.Date; public class TestTimeVariant implements ITimeVariant { private long simulatedTime = 1234567890L*1000; //a recent timestamp private long deltaTime = 10; public void setNow(long t) { simulatedTime = t; } private void bumpTime() { simulatedTime += deltaTime; } public long currentTimeMillis() { long t = simulatedTime; bumpTime(); return t; } public Date newDate() { return new Date(currentTimeMillis()); } }
We have defined ourimport java.util.Date; public class TestTimeVariant implements ITimeVariant { private long simulatedTime = 1234567890L*1000; //a recent timestamp private int simCount = 0; private long[] deltaTimes = { 10 }; //default delta public void setNow(long t) { simulatedTime = t; } public void setDeltaTimes(long[] deltas) { if (deltas==null || deltas.length==0) throw new IllegalArgumentException("Must have at least one delta"); deltaTimes = deltas; //Could make a copy if you are concerned about caller changing it simCount = 0; } private void bumpTime() { simulatedTime += deltaTimes[simCount]; if (simCount < deltaTimes.length - 1) simCount++; } public long currentTimeMillis() { long t = simulatedTime; bumpTime(); return t; } public Date newDate() { return new Date(currentTimeMillis()); } }
bumpTime
method so that if we
call one of our time methods more times than we have deltas, we just
reuse the final delta for any additional calls.
Depending on your desires, you could use other approaches,
such as cycling back to the beginning of the list of deltas
or using an algorithmic approach.
For example, you could choose to use a pseudo-random number
within some range as your delta,
and as long as you seed your random number
generator with the same seed each time you will get the same
sequence of time values.
Capturing Values
Likely a more realistic series of time values is a sequence of time values from an actual run of your method. We can instrument our test class so that we can capture all of the returned time values during a test run, then play them back for our controlled testing. This adds a significant amount of additional code to our test class.You may not need this level of functionality for your tests. In fact, you may need nothing more than returning a single fixed value, as shown in the Controlled Values section. There is no need to implement more functionality in the test code than you need for your situation, so you should think about why you need changing values in the first place. Note that none of this code is useful for any kind of timing test, since it does precisely the opposite: no matter how much real time elapses between calls, the same values are returned from the methods in
TestTimeVariant
.
This is exactly the right behavior for repeatability,
and exactly the wrong behavior for ensuring that the timing
(and thus the performance)
of your application has not changed.
Running timing and performance tests on your application is a separate
concern from running the type of functional unit and regression tests
we are concerned with here.
import java.io.FileReader; import java.io.LineNumberReader; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Date; public class TestTimeVariant implements ITimeVariant { private boolean capturing = false; private ArrayList<Long> captureTimes = null; private long simulatedTime = 1234567890L*1000; //a recent timestamp private int simCount = 0; private long[] deltaTimes = { 10 }; //default delta public void setNow(long t) { simulatedTime = t; } public void setDeltaTimes(long[] deltas) { if (deltas==null || deltas.length==0) throw new IllegalArgumentException("Must have at least one delta"); deltaTimes = deltas; //Could make a copy if you are concerned about caller changing it simCount = 0; } //Starts capture mode. Call saveCapture when done. public void startCapture() { capturing = true; captureTimes = new ArrayList<Long>(); } //Save the captured timestamps to a file for later loading by loadCapture. //The first line is the fill timestamp, the rest are deltas. public void saveCapture(String fileName) throws Exception { //You can use more precise exception list if you want PrintWriter pw = new PrintWriter(fileName); long lastTime = 0; for (Long t : captureTimes) { long delta = t - lastTime; pw.println(delta); lastTime = t; } pw.close(); } //Load a file created by saveCapture. //Sets simulatedTime and deltaTimes array. public void loadCapture(String fileName) throws Exception { //You can use more precise exception list if you want LineNumberReader lr = new LineNumberReader(new FileReader(fileName)); String line = null; boolean haveInitial = false; ArrayList<Long> dTimes = new ArrayList<Long>(); while ((line=lr.readLine())!=null) { long d = Long.valueOf(line); if (haveInitial) dTimes.add(d); else { simulatedTime = d; haveInitial = true; } } //Transfer into long[] to cal setDeltaTimes long[] da = new long[dTimes.size()]; int n = 0; for (long t : dTimes) da[n++] = t; setDeltaTimes(da); } private void bumpTime() { simulatedTime += deltaTimes[simCount]; if (simCount < deltaTimes.length - 1) simCount++; } public long currentTimeMillis() { if (capturing) { long now = System.currentTimeMillis(); captureTimes.add(now); //boxed return now; } else { long t = simulatedTime; bumpTime(); return t; } } public Date newDate() { return new Date(currentTimeMillis()); } }
Demonstration
Here is a little Main program that shows how the above classes work.To run this demo, copy out the definitions forpublic class Main { public static void main(String[] args) throws Exception { TestTimeVariant tv = new TestTimeVariant(); if (args.length==0) appMethod(); //run without any test code else if (args[0].equals("-test")) { TimeVariant.setDelegate(tv); long[] zeroDelta = { 0 }; tv.setDeltaTimes(zeroDelta); //simulate fixed-time case appMethod(); } else if (args[0].equals("-delta")) { TimeVariant.setDelegate(tv); //without setting any deltas, default is 10ms appMethod(); } else if (args[0].equals("-save")) { TimeVariant.setDelegate(tv); tv.startCapture(); appMethod(); tv.saveCapture(args[1]); } else if (args[0].equals("-load")) { TimeVariant.setDelegate(tv); tv.loadCapture(args[1]); appMethod(); } else { System.out.println("unknown argument "+args[0]); //unrecognized } } private static void sleep(int ms) { try { Thread.sleep(ms); } catch (Exception ex) {} } //This is the application method under test public static void appMethod() { long startTime = TimeVariant.currentTimeMillis(); sleep(100); long midTime = TimeVariant.currentTimeMillis(); int n = 2; for (int i=0; i<1000000; i++) { n = n * n + i; //lots of overflows } long endTime = TimeVariant.currentTimeMillis(); System.out.println("App: start at "+startTime); long delta1Time = (midTime - startTime); System.out.println("App: delta1 is "+delta1Time); long delta2Time = (endTime - midTime); System.out.println("App: delta2 is "+delta2Time); } }
ITimeVariant
and TimeVariant
from the Test Hook section above,
the definition for TestTimeVariant
from the Changing Values section above
and Main
from this section
each into an appropriately named file in a test directory.
Compile all files, then run as follows to see the different cases:
- Run
java Main
to run the application as it normally runs, without any test code. The application prints out some timing values. - Run
java Main -test
to wire in the test harness with no delta times, which behaves the same as the first example in the Controlled Values section above. - Run
java Main -delta
to wire in the test harness with a fixed delta value, as you would have with the first example in the Changing Values section above. - Run
java Main -save filename
to wire in the test harness to use real times and save the results to the specified file. The file is a text file, so you can view it to confirm the values it contains. - Run
java Main -load filename
to wire in the test harness and load the values from the specified file.
3 comments:
Nice trick! Well done.
Good approach, was strugling with a good name for such entity :), and I kinda like the "TimeVariant". Thanks!
Interesting. An alternative to coding an injection function would be a 3rd party tool to handle the needed time changes. Several exist, I've had success with TimeShiftX
It makes database temporal unit tests easy as well
Post a Comment