Mocking The Time used by all instances of DateTime for testing purposes
Asked Answered
N

6

27

I'd like to be able to set the time for every instance of DateTime instantiated for the duration of a PHPUnit or Behat Test.

I'm testing business logic relating to time. For example that a method in a class only returns events in the past or future.

Thing's I don't want to do if possible:

  1. Write a wrapper around DateTime and use this instead of DateTime throughout my code. This would involve a bit of a re-write of my current code base.

  2. Dynamically generate a dataset each time the test / suite is run.

So the question is: Is it possible to override DateTimes behaviour to always supply a specific time when requested?

Nuriel answered 31/10, 2011 at 10:44 Comment(2)
You did not accept an answer yet. Can you please clarify what you are looking for in an answer and why the given answers do not satisfy you.Penal
Had exactly the same issue, php timecop extension from @shouze 's answer worked like a charm.Turnspit
P
27

You should stub the DateTime methods you need in your tests to return expected values.

$stub = $this->getMock('DateTime');
$stub->expects($this->any())
     ->method('theMethodYouNeedToReturnACertainValue')
     ->will($this->returnValue('your certain value'));

See https://phpunit.de/manual/current/en/test-doubles.html

If you cannot stub the methods because they are hardcoded into your code, have a look at

which explains how to invoke a callback whenever new is invoked. You could then replace the DateTime class with a custom DateTime class that has a fixed time. Another option would be to use http://antecedent.github.io/patchwork

Penal answered 31/10, 2011 at 10:46 Comment(2)
Thanks Gordon - The DateTime dependency is hardcoded in the majority of my code. I made the mistake of using it as a primitive. All other dependancies are injected so are easy to mock. I'd rather not use an extension to mock, as this reduces the portability of the code. Although it may be the only option! Thanks for your answer.Nuriel
Its hard to check for dates comparison this way tho.Futrell
W
8

You can also use the time traveler lib which uses aop php pecl extention to bring things similar to ruby monkey patching https://github.com/rezzza/TimeTraveler

There's also this php extension, inspired from ruby timecop one: https://github.com/hnw/php-timecop

Whore answered 24/5, 2014 at 6:40 Comment(2)
I would like to... but I don't have the privilege to do so ATM, fresh register ;)Whore
TimeTraveler the last time I saw was broken. The timecop alternative is way better.Sinistrous
O
2

Adding on to what @Gordon already pointed out there is one, rather hackish, way of testing code that relies upon current time:

My mocking out just one protected method that gets you the "global" value you can get around the issues of need to create a Class yourself that you can ask for things like the current time (which would be cleaner but in php it is arguable/understandable that people don't want to do that).

That would look something like this:

class Calendar {
    public function getCurrentTimeAsISO() {
        return $this->currentTime()->format('Y-m-d H:i:s');
    }

    protected function currentTime() {
        return new DateTime();
    }
}

class CalendarTest extends PHPUnit_Framework_TestCase {
    public function testCurrentDate() {
        $cal = $this->getMockBuilder('Calendar')
            ->setMethods(array('currentTime'))
            ->getMock();
        $cal->expects($this->once())
            ->method('currentTime')
            ->will($this->returnValue(
                new DateTime('2011-01-01 12:00:00')
            )
        );
        $this->assertSame(
            '2011-01-01 12:00:00',
            $cal->getCurrentTimeAsISO()
        );
    }
}
Overindulge answered 2/11, 2011 at 12:30 Comment(0)
H
2

You could change your implementation to instantiate DateTime() explicitly with time():

new \DateTime("@".time());

This doesn't change the behaviour of your class. But now you can mock time() by providing a namespaced function:

namespace foo;
function time() {
    return 123;
}

You could also use my package php-mock/php-mock-phpunit for doing so:

namespace foo;

use phpmock\phpunit\PHPMock;

class DateTimeTest extends \PHPUnit_Framework_TestCase {

    use PHPMock;

    public function testDateTime() {
        $time = $this->getFunctionMock(__NAMESPACE__, "time");
        $time->expects($this->once())->willReturn(123);

        $dateTime = new \DateTime("@".time());
        $this->assertEquals(123, $dateTime->getTimestamp());
    }
}
Haler answered 22/4, 2016 at 12:39 Comment(0)
P
2

As I'm using Symfony's WebTestCase to perform functional testing using the PHPUnit testing bundle, it quickly became impractical to mock all usages of the DateTime class.

I wanted to test the application as it handles requests over time, such as testing cookie or cache expiration, etc.

The best way I've found for doing this is to implement my own DateTime class that extends the default class, and providing some static methods to allow for a default time skew to be added/subtracted to all DateTime objects being created from that point onwards.

This is a really easy feature to implement, and doesn't require installing custom libraries.

caveat emptor: The only drawback to this method is the Symfony framework (or whatever framework you're using) won't use your library, so any behaviour it's expected to handle itself, such as internal cache/cookie expirations, won't be affected by these changes.

namespace My\AppBundle\Util;

/**
 * Class DateTime
 *
 * Allows unit-testing of DateTime dependent functions
 */
class DateTime extends \DateTime
{
    /** @var \DateInterval|null */
    private static $defaultTimeOffset;

    public function __construct($time = 'now', \DateTimeZone $timezone = null)
    {
        parent::__construct($time, $timezone);
        if (self::$defaultTimeOffset && $this->isRelativeTime($time)) {
            $this->modify(self::$defaultTimeOffset);
        }
    }

    /**
     * Determines whether to apply the default time offset
     *
     * @param string $time
     * @return bool
     */
    public function isRelativeTime($time)
    {
        if($time === 'now') {
            //important, otherwise we get infinite recursion
            return true;
        }
        $base = new \DateTime('2000-01-01T01:01:01+00:00');
        $base->modify($time);
        $test = new \DateTime('2001-01-01T01:01:01+00:00');
        $test->modify($time);

        return ($base->format('c') !== $test->format('c'));
    }

    /**
     * Apply a time modification to all future calls to create a DateTime instance relative to the current time
     * This method does not have any effect on existing DateTime objects already created.
     *
     * @param string $modify
     */
    public static function setDefaultTimeOffset($modify)
    {
        self::$defaultTimeOffset = $modify ?: null;
    }

    /**
     * @return int the unix timestamp, number of seconds since the Epoch (Jan 1st 1970, 00:00:00)
     */
    public static function getUnixTime()
    {
        return (int)(new self)->format('U');
    }

}

Using this is simple:

public class myTestClass() {
    public function testMockingDateTimeObject()
    {
        echo "fixed:  ". (new DateTime('18th June 2016'))->format('c') . "\n";
        echo "before: ". (new DateTime('tomorrow'))->format('c') . "\n";
        echo "before: ". (new DateTime())->format('c') . "\n";

        DateTime::setDefaultTimeOffset('+25 hours');

        echo "fixed:  ". (new DateTime('18th June 2016'))->format('c') . "\n";
        echo "after:  ". (new DateTime('tomorrow'))->format('c') . "\n";
        echo "after:  ". (new DateTime())->format('c') . "\n";

        // fixed:  2016-06-18T00:00:00+00:00 <-- stayed same
        // before: 2016-09-20T00:00:00+00:00
        // before: 2016-09-19T11:59:17+00:00
        // fixed:  2016-06-18T00:00:00+00:00 <-- stayed same
        // after:  2016-09-21T01:00:00+00:00 <-- added 25 hours
        // after:  2016-09-20T12:59:17+00:00 <-- added 25 hours
    }
}
Pascual answered 19/9, 2016 at 12:2 Comment(0)
O
0

I'm proposing a different approach here, based on a testing library called ClockMock. The idea is to have all date-time related functions, classes and method to be mocked in a transparent way at the engine level (in fact the library uses a php extension) without any hack or code change specifically made to be able to mock time in tests.

Example:

$nowYmd = ClockMock::executeAtFrozenDateTime(new \DateTime('1986-06-05'), function () {
    // Code executed in here will use the above date and time as "current"
    return date('Y-m-d');
});

$this->assertEquals('1986-06-05', $nowYmd);

You can read this article for more information.

Disclaimer: I'm the author and maintainer.

Osis answered 27/6, 2022 at 11:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.