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

…不知道“嘲笑”是否正确。

无论如何,我有一个inheritance的代码库,我试图编写一些基于时间的testing。 尽量不要模糊,代码是关于查看一个项目的历史,并确定该项目是否现在已经基于时间阈值。

在某个时候,我还需要testing添加一些历史logging,并检查阈值现在是否已更改(显然,正确)。

我碰到的问题是,我正在testing的代码的一部分是使用调用time(),所以我发现真的很难确切地知道阈值时间应该是什么,基于这样一个事实,当时间函数将被调用时,m不太确定。

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

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

无论哪种方式,是否有任何“通用实践”开发时间敏感function,testing友好?

编辑:我的问题的一部分也是历史事件发生的时间影响阈值的事实。 这是我的问题的一部分的例子…

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

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

正如你可能会或可能不知道的,我仍然试图把所有这个“testing”的概念挂起来;)

我最近想出了另一个很好的解决scheme,如果你使用的是PHP 5.3的命名空间。 您可以在当前名称空间内实现新的time()函数,并在testing中设置返回值的位置创build共享资源。 然后任何不合格的调用time()将使用你的新函数。

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

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

你可以使用ouzo-goodies的 Clock来模拟testing时间。

在代码中使用简单:

 $time = Clock::now(); 

然后在testing中:

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

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

我不得不在将来和过去的date在应用程序本身(而不是在unit testing)中模拟一个特定的请求。 因此,对\ DateTime :: now()的所有调用都应返回先前在整个应用程序中设置的date。

我决定去这个图书馆https://github.com/rezzza/TimeTraveler ,因为我可以嘲笑date而不改变所有的代码。

 \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 

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

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

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

1)将下面的函数添加到您的testing类或引导程序。 默认容忍时间是2秒。 您可以通过将第三个parameter passing给assertTimeEquals或修改函数参数来更改它。

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

2)testing例子:

 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。

如果执行testingfunction的时间less于2秒,则应通过该testing以获得正确的工作function。

这不是完美的,超级准确的,但它很简单,在很多情况下足以testing你想testing的东西。

最简单的解决scheme是重写PHP的time()函数,并用自己的版本replace它。 但是,您不能轻易replace内置的PHP函数( 请参阅此处 )。

除此之外,唯一的办法就是将time()调用抽象到你自己的某个类/函数中,以便返回testing所需的时间。

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

你可以使用runkit扩展来覆盖php的time()函数。 确保你将runkit.internal_overide设置为On

使用[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="Ymd 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'); } 

你甚至可以像这样testing模拟:

 public function testMockDate() { $this->mockDate(); $this->assertEquals(MOCK_DATE, date('Ym-d')); $this->assertEquals(MOCK_TIME, date('H:i:s')); $this->assertEquals(MOCK_DATETIME, date()); $this->unmockDate(); } 

这里是fab的post的补充。 我使用eval做了基于命名空间的覆盖。 这样,我可以运行它进行testing,而不是我的代码的其余部分。 我运行的function类似于:

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

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