我希望能够在 PHPUnit 或 Behat 测试期间为每个实例化的 DateTime
设置时间。
我正在测试与时间相关的业务逻辑。例如,类中的某个方法仅返回过去或未来的事件。
如果可能的话,我不想执行以下操作:
编写一个围绕
DateTime
的包装器,并在整个代码中使用它代替DateTime
。这将需要对当前代码库进行一些重写。每次运行测试/套件时动态生成数据集。
所以问题是:是否可以覆盖 DateTime
的行为,始终在请求时提供特定的时间?
我希望能够在 PHPUnit 或 Behat 测试期间为每个实例化的 DateTime
设置时间。
我正在测试与时间相关的业务逻辑。例如,类中的某个方法仅返回过去或未来的事件。
如果可能的话,我不想执行以下操作:
编写一个围绕 DateTime
的包装器,并在整个代码中使用它代替 DateTime
。这将需要对当前代码库进行一些重写。
每次运行测试/套件时动态生成数据集。
所以问题是:是否可以覆盖 DateTime
的行为,始终在请求时提供特定的时间?
在测试中,您应该对需要使用的 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
如果您无法存根方法,因为它们已硬编码到您的代码中,请查看Sebastian Bergmann的Stubbing Hard-Coded Dependencies, 该文章解释了如何在调用new
时调用回调。然后,您可以使用具有固定时间的自定义DateTime类替换DateTime类。另一个选择是使用http://antecedent.github.io/patchwork
time()
显式地实例化DateTime()
:new \DateTime("@".time());
这不会改变您的类的行为。但是现在您可以通过提供一个命名空间函数来 模拟time()
:
namespace foo;
function time() {
return 123;
}
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());
}
}
我使用Symfony的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
}
}
除了@Gordon已经指出的方法外,还有一种相当hackish的测试依赖于当前时间的代码的方式:
我只需要模拟一个受保护的方法,让你获得“全局”值,就可以解决需要创建一个类自己询问诸如当前时间之类的东西的问题(这样做更加清晰,但在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()
);
}
}
$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);
更多信息请阅读本文。
声明:我是作者和维护者。