views:

154

answers:

3

I have a web application that I want to run some system tests on, and in order to do that I'm going to need to move the system time around. The application used DateTime all the way through.

Has anyone got any recommendations for how to change the time that DateTime->now reports? The only thing that comes to mind is subclassing DateTime and messing about with all the 'use' lines, but this seems rather invasive.

Note on answers:

All three will work fine, but the Hook::LexWrap one is the one I've chosen because (a) I want to move the clock rather than jiggle it a bit (which is more the purpose of what Time::Mock and friends do); (b) I do, consistently, use DateTime, and I'm happy to have errors come out if I've accidentally not used it; and (c) Hook::LexWrap is simply more elegant than a hack in the symbol table, for all that it does the same thing. (Also, it turns out to be a dependency of some module I already installed, so I didn't even have to CPAN it...)

A: 

You can use code injection via Hook::LexWrap to intercept the now() method.

use Hook::LexWrap;

use DateTime;

# Use real now
test();

{
    my $wrapper = wrap 'DateTime::now',
        post => sub {
            $_[-1] = DateTime->from_epoch( epoch => 0 );
        };

    # Use fake now
    test();

}

# use real now again
test();

sub test {
    my $now = DateTime->now;

    print "The time is $now\n";
}
daotoad
http://search.cpan.org/perldoc/Hook::LexWrap would also work as a link.
Brad Gilbert
Interesting, and the doc pointer was very helpful. I would recommend mentioning the non-scoped version in the answer as well, since that what I was originally looking for, but both versions will be useful to me.
ijw
+4  A: 

I think Hook::LexWrap is overkill for this situation. It's easier to just redefine such a simple function.

use DateTime;

my $offset;

BEGIN {
  $offset = 24 * 60 * 60; # Pretend it's tomorrow

  no warnings 'redefine';

  sub DateTime::now
  {
    shift->from_epoch( epoch => ($offset + scalar time), @_ )
  }
} # end BEGIN

You can replace my $offset with our $offset if you need to access the $offset from outside the file which contains this code.

You can adjust $offset at any time, if you want to change DateTime's idea of the current time during the run.

The calculation of $offset should probably be more complicated than shown above. For example, to set the "current time" to an absolute time:

my $want = DateTime->new(
   year   => 2009,
   month  => 9,
   day    => 14,
   hour   => 12,
   minute => 0,
   second => 0,
   time_zone => 'America/Chicago',
);

my $current = DateTime->from_epoch(epoch => scalar time);

$offset = $want->subtract_datetime_absolute($current)->in_units('seconds');

But you probably do want to calculate a fixed number of seconds to add to the current time, so that time will advance normally after that. The problem with using add( days => 1 ); in the redefined now method is that things like DST changes will cause the time to jump at the wrong pseudotime.

cjm
What you show works fine if you only have to override now() to one behavior during the entire run. If the OP is changing the behavior of now() repeatedly, and over narrow portions of the test suite, I believe H::LW is a better choice. Also, to pretend it's tomorrow, wouldn't it be better to use DateTime's facilities and do it right? DateTime->now->add( days => 1 );
daotoad
Well, you can easily change `$offset` at any time during the run. As for using `add` instead, that depends on exactly what you're trying to do. I was just giving one example of a possible new definition.
cjm
Change "my $offset = ..." to "our $_offset = ...", and then the test script can twiddle $DateTime::_offset as much as it likes.
Ether
daotoad: give the ->add() method a try and let us know when it succeeds.
ysth
@ysth, you are correct, I did inadvertently create an infinite recursion by calling `now()`. So, the trivial fix is `DateTime->from_epoch( epoch => time )->add( days =>1 );` But the fundamental point, that adding 86400 seconds is a bad way to move forward exactly one day, is still true. The difficulties that arise when doing date time math is one of the key reasons to use DateTime rather than just hacking something together with time().
daotoad
You should do all of your definitions in a BEGIN to ensure nothing gets to call the real ones.
brian d foy
+9  A: 

Rather than taking the high-level approach and wrapping DateTime specifically, you might want to look into the modules Test::MockTime and Time::Mock, which override the low-level functions that DateTime etc. make use of, and (with any luck) will do the right thing on any time-sensitive code. To me it seems like a more robust way to test.

hobbs
This seems like the best solution. The tests will continue to work when the tested code switches to a different library for date and time handling.
daotoad
Indeed so, though in this case, I want to standardise on DateTime throughout the application. So having a test that fails when not using DateTime is a good thing.
ijw