Tuesday, February 24, 2009

Unit Testing with Dates and Times

When running unit tests or regression tests, you should know exactly what all of your inputs are and be able to control them. If you have methods that use the current date or time, you need to do a bit of extra work to do that.

Contents

Refactor

If you have a method that calls System.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.
import java.util.Date; public class TimeVariant { public static long currentTimeMillis() { return System.currentTimeMillis(); } public static Date newDate() { return new Date(); } }
Modify your code to replace all calls to 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 in TimeVariant 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:
import java.util.Date; public interface ITimeVariant { public long currentTimeMillis(); public Date newDate(); }
We will be writing a test class 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.
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(); } }
From the setup method for your unit test you set the hook:
ITimeVariant tv = new TestTimeVariant(); TimeVariant.setDelegate(tv);
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.

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 our TestTimeVariant class might look like this:
import 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); } }
The above code satisfies our desire to have a controlled value, and it may be perfectly satisfactory for testing simple methods that call 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 our TestTimeVariant 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.
import 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); } }
With the 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.
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()); } }
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 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()); } }
We have defined our 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.
public 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); } }
To run this demo, copy out the definitions for 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:
  1. Run java Main to run the application as it normally runs, without any test code. The application prints out some timing values.
  2. 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.
  3. 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.
  4. 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.
  5. Run java Main -load filename to wire in the test harness and load the values from the specified file.