web-dev-qa-db-ja.com

PHPUnitで時間を「モック」できますか?

...「モック」が正しい言葉かどうかわからない。

とにかく、継承されたコードベースがあり、そのために時間ベースのテストを作成しようとしています。 too曖昧にならないようにしようとすると、コードはアイテムの履歴を見て、そのアイテムが時間のしきい値に基づいているかどうかを判断することに関連しています。

ある時点で、その履歴に何かを追加し、しきい値が変更されていることを確認する(そして、明らかに正しい)こともテストする必要があります。

私が当たっている問題は、テストしているコードの一部がtime()の呼び出しを使用しているため、しきい値時間を正確に知ることが非常に難しいことです。 mそのtime()関数がいつ呼び出されるか正確にはわかりません。

私の質問は基本的にこれです:私のテストが「既知の時間」で動作するように、time()呼び出しを「オーバーライド」する方法、または何らかの方法で時間を「模擬」する方法がありますか?

または、必要に応じて特定の時間を強制的に使用させるために、テストしているコードで何かをする必要があるという事実を受け入れる必要がありますか?

いずれにせよ、テストフレンドリーな時間依存機能を開発するための「共通プラクティス」はありますか?

編集:私の問題の一部は、歴史の中で発生したことがしきい値に影響を与えるという事実です。ここに私の問題の一部の例があります...

あなたがバナナを持っていて、それを食べなければならないときにうまくやろうとしていると想像してください。何らかの化学物質を噴霧しない限り、3日以内に期限が切れるとしましょう。その場合、有効期限に4日を追加します。スプレーが適用された時から。その後、凍結してさらに3か月追加できますが、凍結されている場合は、解凍後1日しか使用できません。

これらのルールはすべて、過去のタイミングによって決まります。数秒以内にDominikのテストの提案を使用できることに同意しますが、履歴データはどうですか?その場で「作成」する必要がありますか?

あなたが伝えることができるかもしれないし、できないかもしれませんが、私はまだこの「テスト」概念のすべてを理解しようとしています;)

68
Narcissus

PHP 5.3ネームスペースを使用している場合に最適な別のソリューションを最近思い付きました。現在のネームスペース内に新しいtime()関数を実装し、リターンを設定する共有リソースを作成できますテストで値を指定すると、time()の非修飾呼び出しで新しい関数が使用されます。

さらに読むために、 blog で詳しく説明しました

58

免責事項:私はこのライブラリを書きました。

Clock from ouzo-goodies を使用して、テストの時間をモックできます。

コードでは単純に使用します:

$time = Clock::now();

次に、テストで:

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

Symfony(> = 2.8)を使用している場合:SymfonyのPHPUnit Bridgeには、組み込みメソッドtimemicrotimesleepおよびusleepをオーバーライドするClockMock機能が含まれています。

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

5
simon.ro

アプリ単体で(単体テストではなく)将来および過去の日付で特定のリクエストをシミュレートする必要がありました。したがって、\ 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
3
SenG

個人的には、テストされた関数/メソッドでtime()を使い続けています。テストコードでは、time()との等価性をテストせず、単に1または2未満の時間差(関数の実行にかかる時間に応じて)をテストするようにしてください。

3
Dominik

ほとんどの場合、これで十分です。いくつかの利点があります。

  • あなたは何もm笑する必要はありません
  • 外部プラグインは必要ありません
  • time()だけでなく、DateTimeオブジェクトも使用できます。
  • 名前空間を使用する必要はありません。

Phpunitを使用していますが、他のテストフレームワークに追加することができます。phpunitのassertContains()のように機能する関数が必要です。

1)以下の関数をテストクラスまたはブートストラップに追加します。時間のデフォルトの許容値は2秒です。 3番目の引数をassertTimeEqualsに渡すか、関数の引数を変更することで変更できます。

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秒未満かかる場合、このテストは正常に機能するために合格する必要があります。

完全ではなく、非常に正確ではありませんが、非常にシンプルであり、多くの場合、テストしたいものをテストするのに十分です。

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
jhvaras

Runkit拡張機能を使用して、php time()関数をオーバーライドできます。必ずrunkit.internalオーバーライドをオンに設定してください

2
Vlad Balmos

Fabの投稿への追加です。 evalを使用して名前空間ベースのオーバーライドを行いました。このようにして、テスト用に実行するだけで、残りのコードでは実行できません。私は次のような機能を実行します:

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

次に、テストセットアップでtimeOverrides(array(...))を渡して、time()が呼び出される名前空間を追跡するだけでよいようにします。

1
Photis

最も簡単な解決策は、PHP time()関数をオーバーライドして独自のバージョンに置き換えることです。ただし、組み込みのPHP関数を簡単に置き換えることはできません(- こちらを参照 )。

それ以外では、唯一の方法は、テストに必要な時間を返す独自のクラス/関数へのtime()呼び出しを抽象化することです。

または、仮想マシンでテストシステム(オペレーティングシステム)を実行し、仮想コンピューター全体の時間を変更することもできます。

1
Milan Babuškov