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

时间:2010-03-03 14:02:22

标签: php phpunit

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

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

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

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

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

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

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

编辑: 我的一部分问题也在于,历史事件发生的时间会影响阈值。以下是我的部分问题的一个例子......

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

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

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

12 个答案:

答案 0 :(得分:56)

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

为了进一步阅读,我在blog

中详细描述了它

答案 1 :(得分:6)

  

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

您可以使用Clock中的ouzo-goodies模拟测试时间。

在代码中使用简单:

$time = Clock::now();

然后在测试中:

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

答案 2 :(得分:4)

对于那些使用symfony(> = 2.8)的人:Symfony的PHPUnit Bridge包含一个ClockMock功能,可以覆盖内置方法timemicrotime,{{1 }和sleep

请参阅:http://symfony.com/doc/2.8/components/phpunit_bridge.html#clock-mocking

答案 3 :(得分:3)

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

答案 4 :(得分:3)

我必须在应用程序本身(而不是单元测试中)模拟未来和过去日期中的特定请求。因此,对\ 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

答案 5 :(得分:3)

Carbon::setTestNow(Carbon $time = null)会同时调用Carbon::now()new Carbon('now')

https://medium.com/@stefanledin/mock-date-and-time-with-carbon-8a9f72cb843d

示例:

    public function testSomething()
    {
        $now = Carbon::now();
        // Mock Carbon::now() / new Carbon('now') to always return the same time
        Carbon::setTestNow($now);

        // Do the time sensitive test:
        $this->retroEncabulator('prefabulate')
            ->assertJsonFragment(['whenDidThisHappen' => $now->timestamp])

        // Release the Carbon::now() mock
        Carbon::setTestNow();
    }

$this->retroEncabulator()函数当然需要在内部使用Carbon::now()new Carbon('now')

答案 6 :(得分:2)

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

答案 7 :(得分:2)

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

答案 8 :(得分: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秒,则应通过此测试以获得正确的工作功能。

它并不完美且非常准确,但它非常简单,在很多情况下它足以测试你想测试的内容。

答案 9 :(得分:1)

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

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

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

答案 10 :(得分:1)

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

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

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

答案 11 :(得分:0)

<块引用>

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

如果您可以在系统中随意安装 php 扩展,则可以使用 https://github.com/slope-it/clock-mock

该库需要 ext-uopz >= 6.1.1,通过使用 ClockMock::freezeClockMock::reset,您可以将内部 php 时钟移动到您喜欢的任何日期和时间。它很酷的一点是它需要对您的生产代码进行零修改,因为它透明地模拟 \DateTime\DateTimeImmutable 对象以及一些全局函数(例如 date()、{{1 }},等等...)