25

I'm using JodaTime 2.1 and I'm looking for a pattern to unit test code which performs date/time operations to make sure it behaves well for all time zones and independent of DST.

Specifically:

  1. How can I mock the system clock (so I don't have to mock all the places where I call new DateTime() to get the current time)
  2. How can I do the same for the default time zone?
Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • possible duplicate of [Make unit tests with dates pass in all time zones and with/out DST](http://stackoverflow.com/questions/10833948/make-unit-tests-with-dates-pass-in-all-time-zones-and-with-out-dst) – Caleb Jun 02 '12 at 04:43
  • It's not a duplicate; it's a followup. – Aaron Digulla Jun 04 '12 at 11:23

2 Answers2

31

You can use a @Rule for this. Here is the code for the rule:

import org.joda.time.DateTimeZone;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;

public class UTCRule extends TestWatcher {

    private DateTimeZone origDefault = DateTimeZone.getDefault();

    @Override
    protected void starting( Description description ) {
        DateTimeZone.setDefault( DateTimeZone.UTC );
    }

    @Override
    protected void finished( Description description ) {
        DateTimeZone.setDefault( origDefault );
    }
}

You can use the rule like this:

public class SomeTest {

    @Rule
    public UTCRule utcRule = new UTCRule();

    ....
}

This will change the current time zone to UTC before each test in SomeTest will be executed and it will restore the default time zone after each test.

If you want to check several time zones, use a rule like this one:

import org.joda.time.DateTimeZone;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;

public class TZRule extends TestWatcher {

    private DateTimeZone origDefault = DateTimeZone.getDefault();

    private DateTimeZone tz;

    public TZRule( DateTimeZone tz ) {
        this.tz = tz;
    }

    @Override
    protected void starting( Description description ) {
        DateTimeZone.setDefault( tz );
    }

    @Override
    protected void finished( Description description ) {
        DateTimeZone.setDefault( origDefault );
    }
}

Put all the affected tests in an abstract base class AbstractTZTest and extend it:

public class UTCTest extends AbstractTZTest {
    @Rule public TZRule tzRule = new TZRule( DateTimeZone.UTC );
}

That will execute all tests in AbstractTZTest with UTC. For each time zone that you want to test, you'll need another class:

public class UTCTest extends AbstractTZTest {
    @Rule public TZRule tzRule = new TZRule( DateTimeZone.forID( "..." );
}

Since test cases are inherited, that's all - you just need to define the rule.

In a similar way, you can shift the system clock. Use a rule that calls DateTimeUtils.setCurrentMillisProvider(...) to simulate that the test runs at a certain time and DateTimeUtils.setCurrentMillisSystem() to restore the defaults.

Note: Your provider will need a way to make the clock tick or all new DateTime instances will have the same value. I often advance the value by a millisecond each time getMillis() is called.

Note 2: That only works with joda-time. It doesn't affect new java.util.Date().

Note 3: You can't run these tests in parallel anymore. They must run in sequence or one of them will most likely restore the default timezone while another test is running.

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • 1
    Nice solution, but I think it can be risky to set the original value back, if you run tests in parallel, as the first test, which finishes, sets the timezone (static field) back for all other tests. – Spille Nov 08 '18 at 11:10
  • 1
    @CSpille Correct. If you want to run tests in parallel, you have to group the tests which need this rule somehow and execute the rule around the group. – Aaron Digulla Jan 25 '19 at 12:58
2
for (String zoneId : DateTimeZone.getAvailableIDs())
{
   DateTime testedDate1;
   DateTime testedDate2;
   try
   {
      final DateTimeZone tz = DateTimeZone.forID(zoneId);
      // your test with testedDate1 and testedDate2 
   }
   catch (final IllegalArgumentException e)
   {
      // catching DST problem
      testedDate1 = testetDate1.plusHours(1);
      testedDate2 = testetDate2.plusHours(1);
      // repeat your test for this dates
   }
}

Change for single test

DateTimeZone default;  

DateTimeZone testedTZ;

@Before
public void setUp()
{
   default = GateTimeZone.getDefault();
   DateTimeZone.setDefault
}  

@After
public void tearDown()
{
   default = GateTimeZone.setDefault();
   DateTimeZone.setDefault(testedTZ)
}   

@Test
public void test()
{
//...
}
Ilya
  • 29,135
  • 19
  • 110
  • 158
  • 1
    I'm not sure this will work independently of Daylight Savings Time. For instance, right now I'm getting the time zone for "US/Pacific." And right now, because we're still in DST, I get back an offset of -25200. But when we switch back to standard time in November, the same code will return -28800. Am I mistaken about that? – Marvo Oct 03 '13 at 20:33