Question

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?

Was it helpful?

Solution

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

OTHER TIPS

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

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()
        );
    }
}

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());
    }
}

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
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top