【问题标题】:Mocking The Time used by all instances of DateTime for testing purposes模拟所有 DateTime 实例用于测试目的的时间
【发布时间】:2011-12-18 15:38:44
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • 您尚未接受答案。您能否澄清您在答案中寻找的内容以及为什么给定的答案不让您满意。
  • 遇到了完全相同的问题,@shouze 的答案中的 php timecop 扩展就像一个魅力。

标签: php testing datetime phpunit behat


【解决方案1】:

您应该在测试中存根您需要的 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

【讨论】:

  • 感谢 Gordon - DateTime 依赖项在我的大部分代码中都是硬编码的。我犯了一个错误,将它用作原语。所有其他依赖项都被注入,因此很容易模拟。我宁愿不使用扩展来模拟,因为这会降低代码的可移植性。虽然它可能是唯一的选择!谢谢你的回答。
  • 这种方式很难检查日期比较。
【解决方案2】:

你还可以使用 time traveler lib,它使用 aop php pecl 扩展来带来类似于 ruby​​ monkey 补丁 https://github.com/rezzza/TimeTraveler

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

【讨论】:

  • 我想...但我没有特权这样做 ATM,新注册 ;)
  • 上次看到的TimeTraveler坏了。 timecop 替代方案要好得多。
【解决方案3】:

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

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

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

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

caveat emptor:此方法的唯一缺点是 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
    }
}

【讨论】:

    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

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

      这不会改变您班级的行为。但是现在您可以通过提供命名空间函数来mock 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());
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-08-21
        • 2023-01-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-09
        相关资源
        最近更新 更多