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


75

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

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

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

我遇到的问题是,我正在测试的部分代码使用对time()的调用,因此基于我的事实,我很难确定确切的阈值时间是多少。我不太确切地知道何时会调用time()函数。

所以我的问题基本上是这样的:我是否可以通过某种方式“替代” time()调用,或者以某种方式“模拟”时间,以使测试在“已知时间”内工作?

还是我只需要接受这样一个事实,即我将不得不在正在测试的代码中做某件事,以某种方式允许我在需要时强迫它使用特定的时间?

无论哪种方式,是否都存在开发对时间敏感的,易于测试的功能的“通用做法”?

编辑:我的问题也有一部分是事实,即历史发生的时间会影响阈值。这是我部分问题的一个例子...

想象一下,你有一根香蕉,而当你需要把它吃掉的时候,你正在努力解决。假设它将在3天内失效,除非用某种化学药品喷洒了,在这种情况下,我们从喷涂开始算起将有效期增加了4天。然后,我们可以通过冻结将其再添加3个月,但是如果将其冻结,则融化后只有1天的时间可以使用它。

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

正如您可能无法知道的那样,我仍在尝试掌握所有这种“测试”概念;)


对于PHP7你可以使用github.com/runkit7/Timecop-PHP它是基于runkit7
亚历克斯

Answers:


62

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

为了进一步阅读,我在博客中对其进行了详细描述


1
我真的很喜欢这个想法Fabian。另一个好处是,它迫使我的同事升级到5.3;)
水仙

非常有用,谢谢您与我们的Fabian分享这项技术-非常感谢!
MicE 2011年

天才在这里工作。函数的命名空间确实使替换内置函数进行测试很有用。
毛里求斯,2013年

2
我最近实现了php-mock库,该库使用该语言功能来模拟非确定性的PHP函数,例如time()
Markus Malkusch 2014年

2
很好的解决方案。即使你的SUT是在不同的命名空间比你的测试,您可以通过使用“多重命名空间在同一个文件”使用它php.net/manual/en/language.namespaces.definitionmultiple.php
antonienko


6

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

你可以嘲笑时间使用测试时钟茴香烈酒,好吃的东西

在代码中简单地使用:

$time = Clock::now();

然后在测试中:

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

31
当您链接到自己的软件时,应包括一个免责声明,让所有人都知道您编写了该软件。
威尔

5

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

对于new \DateTime()在他们的代码中使用a的人来说似乎很酷。但是应该如何安装呢?github仓库中没有信息。
kekko12 '16

3

就个人而言,我一直在测试的函数/方法中使用time()。在测试代​​码中,只需确保不与time()进行相等性测试,而只需使时间差小于1或2(取决于函数执行所需的时间)即可。


目前看来,这就是我要走的路。如果有帮助,我也为问题添加了“示例”。谢谢。
水仙

我看到了你的榜样。再次,个人而言,对于此类测试,我使用phpunit设置方法准备“正确的”历史数据(例如在数据库内部)
Dominik 2010年

1
这会使您的测试非常脆弱。每当被测过程经历延迟(无论出于何种原因)时,它们显然可能会毫无理由地失败。
t.heintz 2013年

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')内部使用。


也许您可以像这样使用它,$now = Carbon::now(); Carbon::setTestNow($now);
mohammad.kaab,

@ mohammad.kaab我添加了一个完全做到这一点的示例
Henk


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

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函数(请参见此处)。

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

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


谢谢米兰:尽管我同意在VM中运行将是“费力”时间的一种选择,但我认为我仍然必须考虑“运行时变量”,因此最终还是要按照Dominik的建议进行。有趣的想法,谢谢。
Narcissus 2010年

1

这是fab帖子的补充。我使用eval进行了基于名称空间的覆盖。这样,我只能运行它进行测试,而不能运行其余的代码。我运行类似的功能:

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.