我可以在PHPUnit中“模拟”时间吗?

问题描述 投票:67回答:10

......不知道'mock'是否是正确的词。

无论如何,我有一个继承的代码库,我正在尝试编写一些基于时间的测试。试图不要太模糊,代码与查看项目的历史并确定该项目现在是否基于时间阈值有关。

在某些时候,我还需要测试在该历史记录中添加内容并检查阈值现在是否已更改(显然,更正)。

我正在测试的问题是我正在测试的部分代码是使用对time()的调用,所以我发现很难确切知道阈值时间应该是什么,基于我的事实我不确定何时会调用time()函数。

所以我的问题基本上是这样的:有没有办法让我“覆盖”time()调用,或以某种方式“模仿”时间,以便我的测试在“已知时间”工作?

或者我只是必须接受这样一个事实,即我将不得不在我正在测试的代码中执行某些操作,以某种方式允许我强制它在需要时使用特定时间?

无论哪种方式,是否有任何“常见做法”来开发对测试友好的时间敏感功能?

编辑:我的问题的一部分也是历史中发生的事情影响阈值的事实。这是我的部分问题的一个例子......

想象一下,你有一个香蕉,当你需要吃它时,你正试图解决这个问题。假设它将在3天内到期,除非它喷洒了一些化学物质,在这种情况下,我们会在施用喷雾时添加4天到期。然后,我们可以通过冻结它再添加3个月,但是如果它已经冷冻,那么我们只有1天的时间来解冻它。

所有这些规则都是由历史时间决定的。我同意我可以在几秒钟内使用Dominik的测试建议,但我的历史数据是什么?我应该在飞行中“创造”吗?

正如您可能或可能无法分辨的那样,我仍然试图了解所有这些“测试”概念;)

php phpunit
10个回答
55
投票

我最近提出了另一种解决方案,如果你使用的是PHP 5.3名称空间。您可以在当前命名空间内实现新的time()函数,并创建一个共享资源,您可以在其中设置测试中的返回值。然后对time()的任何不合格的调用都将使用你的新函数。

为了进一步阅读,我在我的blog中详细描述了它


1
投票

这是fab的帖子的补充。我使用eval完成了基于命名空间的覆盖。这样,我可以运行它来进行测试,而不是我的其余代码。我运行类似于的函数:

function timeOverrides($namespaces = array()) {
  $returnTime = time();
  foreach ($namespaces as $namespace) {
    eval("namespace $namespace; function time() { return $returnTime; }");
  }
}

然后在测试设置中传入timeOverrides(array(...)),这样我的测试只需要跟踪调用time()的名称空间。


6
投票

免责声明:我写了这个库。

你可以使用Clockouzo-goodies模拟测试时间。

在代码中使用简单:

$time = Clock::now();

然后在测试中:

Clock::freeze('2014-01-07 12:34');
$result = Class::getCurrDate();
$this->assertEquals('2014-01-07', $result);

3
投票

就个人而言,我在测试的函数/方法中继续使用time()。在您的测试代码中,只需确保不测试与time()的相等性,而只是测试小于1或2的时间差(取决于函数执行所花费的时间)


3
投票

对于那些使用symfony(> = 2.8)的人:Symfony的PHPUnit Bridge包含一个ClockMock功能,它覆盖了内置方法timemicrotimesleepusleep

见:http://symfony.com/doc/2.8/components/phpunit_bridge.html#clock-mocking


2
投票

我必须在应用程序本身(而不是单元测试中)模拟未来和过去日期中的特定请求。因此,对\ DateTime :: now()的所有调用都应返回先前在整个应用程序中设置的日期。

我决定使用这个库https://github.com/rezzza/TimeTraveler,因为我可以在不改变所有代码的情况下模拟日期。

\Rezzza\TimeTraveler::enable();
\Rezzza\TimeTraveler::moveTo('2011-06-10 11:00:00');

var_dump(new \DateTime());           // 2011-06-10 11:00:00
var_dump(new \DateTime('+2 hours')); // 2011-06-10 13:00:00

2
投票

在大多数情况下,这样做。它有一些优点:

  • 你不必嘲笑任何东西
  • 你不需要外部插件
  • 你可以使用任何时间函数,不仅是time(),还有DateTime对象
  • 您不需要使用名称空间。

它使用的是phpunit,但是您可以将它添加到任何其他测试框架中,您只需要像phpunit中的assertContains()一样工作。

1)将以下函数添加到测试类或引导程序中。默认的时间容差为2秒。您可以通过将第3个参数传递给assertTimeEquals或修改函数args来更改它。

private function assertTimeEquals($testedTime, $shouldBeTime, $timeTolerance = 2)
{
    $toleranceRange = range($shouldBeTime, $shouldBeTime+$timeTolerance);
    return $this->assertContains($testedTime, $toleranceRange);
}

2)测试示例:

public function testGetLastLogDateInSecondsAgo()
{
    // given
    $date = new DateTime();
    $date->modify('-189 seconds');

    // when
    $this->setLastLogDate($date);

    // then
    $this->assertTimeEquals(189, $this->userData->getLastLogDateInSecondsAgo());
}

assertTimeEquals()将检查(189,190,191)的数组是否包含189。

如果执行测试功能需要不到2秒,则应该通过该测试以获得正确的工作功能。

它并不完美且超精确,但它非常简单,在很多情况下它足以测试你想要测试的东西。


1
投票

最简单的解决方案是覆盖PHP time()函数并将其替换为您自己的版本。但是,您无法轻松替换内置的PHP函数(see here)。

除此之外,唯一的方法是抽象time()调用你自己的一些类/函数,这将返回你需要测试的时间。

或者,您可以在虚拟机中运行测试系统(操作系统)并更改整个虚拟计算机的时间。


1
投票

您可以使用runkit扩展覆盖php time()函数。确保将runkit.internal override设置为On


1
投票

使用[runkit] [1]扩展名:

define('MOCK_DATE', '2014-01-08');
define('MOCK_TIME', '17:30:00');
define('MOCK_DATETIME', MOCK_DATE.' '.MOCK_TIME);

private function mockDate()
{
    runkit_function_rename('date', 'date_real');
    runkit_function_add('date','$format="Y-m-d H:i:s", $timestamp=NULL', '$ts = $timestamp ? $timestamp : strtotime(MOCK_DATETIME); return date_real($format, $ts);');
}


private function unmockDate()
{
    runkit_function_remove('date');
    runkit_function_rename('date_real', 'date');
}

您甚至可以像这样测试模拟:

public function testMockDate()
{
    $this->mockDate();
    $this->assertEquals(MOCK_DATE, date('Y-m-d'));
    $this->assertEquals(MOCK_TIME, date('H:i:s'));
    $this->assertEquals(MOCK_DATETIME, date());
    $this->unmockDate();
}
© www.soinside.com 2019 - 2024. All rights reserved.