23

我希望能够在DateTimePHPUnit 或 Behat 测试期间为每个实例化实例设置时间。

我正在测试与时间相关的业务逻辑。例如,类中的方法只返回过去或未来的事件。

如果可能的话,我不想做的事情:

  1. 编写一个包装器DateTime并使用它而不是DateTime贯穿我的代码。这将涉及对我当前的代码库进行一些重写。

  2. 每次运行测试/套件时动态生成数据集。

所以问题是:是否可以覆盖DateTimes 行为以始终在请求时提供特定时间?

4

5 回答 5

23

您应该存根DateTime测试中需要的方法以返回预期值。

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

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

如果您无法存根方法,因为它们被硬编码到您的代码中,请查看

它解释了如何在调用时调用回调new。然后,您可以将 DateTime 类替换为具有固定时间的自定义 DateTime 类。另一种选择是使用http://antecedent.github.io/patchwork

于 2011-10-31T10:46:02.863 回答
8

您还可以使用时间旅行者库,它使用 aop php pecl 扩展来带来类似于 ruby​​ monkey 修补的东西https://github.com/rezzza/TimeTraveler

还有这个 php 扩展,灵感来自 ruby​​ timecop 一: https ://github.com/hnw/php-timecop

于 2014-05-24T06:40:17.203 回答
2

除了@Gordon 已经指出的内容之外,还有一种相当老套的方法来测试依赖于当前时间的代码:

我只模拟了一个受保护的方法,它可以让你获得“全局”值,你可以解决需要自己创建一个类的问题,你可以要求诸如当前时间之类的东西(这会更干净,但在 php 中它是有争议的/人们不想这样做是可以理解的)。

看起来像这样:

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()
        );
    }
}
于 2011-11-02T12:30:44.307 回答
2

您可以更改实现以DateTime()显式实例化time()

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

这不会改变您班级的行为。但是现在您可以通过提供命名空间函数来模拟:time()

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

你也可以使用我的包php-mock/php-mock-phpunit这样做:

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());
    }
}
于 2016-04-22T12:39:07.740 回答
2

当我使用Symfony 的 WebTestCase使用 PHPUnit 测试包执行功能测试时,模拟 DateTime 类的所有用法很快变得不切实际。

我想测试应用程序随着时间的推移处理请求,例如测试 cookie 或缓存过期等。

我发现这样做的最好方法是实现我自己的扩展默认类的 DateTime 类,并提供一些静态方法以允许将默认时间偏差添加/减去从该点开始创建的所有 DateTime 对象.

这是一个非常容易实现的功能,并且不需要安装自定义库。

注意事项:此方法的唯一缺点是 Symfony 框架(或您正在使用的任何框架)不会使用您的库,因此它预期自己处理的任何行为,例如内部缓存/cookie 过期,都不会受这些变化的影响。

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

}

使用它很简单:

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
    }
}
于 2016-09-19T12:02:12.147 回答