web-dev-qa-db-ja.com

テスト目的でDateTimeのすべてのインスタンスによって使用される時間をモックする

PHPUnitまたはBehatTestの期間中にインスタンス化されたDateTimeのすべてのインスタンスの時間を設定できるようにしたいと思います。

時間に関連するビジネスロジックをテストしています。たとえば、クラスのメソッドは過去または未来のイベントのみを返します。

可能であればやりたくないこと:

  1. DateTimeのラッパーを記述し、コード全体でDateTimeの代わりにこれを使用します。これには、現在のコードベースを少し書き直す必要があります。

  2. テスト/スイートが実行されるたびに、データセットを動的に生成します。

したがって、問題は次のとおりです。DateTimesの動作をオーバーライドして、要求されたときに常に特定の時間を提供することは可能ですか?

22
Ben Waine

期待値を返すには、テストで必要なDateTimeメソッドをスタブ化する必要があります。

$stub = $this->getMock('DateTime');
$stub->expects($this->any())
     ->method('theMethodYouNeedToReturnACertainValue')
     ->will($this->returnValue('your certain value'));

https://phpunit.de/manual/current/en/test-doubles.html を参照してください

コードにハードコードされているためにメソッドをスタブできない場合は、以下を参照してください。

これは、newが呼び出されるたびにコールバックを呼び出す方法を説明しています。次に、DateTimeクラスを、時刻が固定されたカスタムDateTimeクラスに置き換えることができます。別のオプションは、使用することです http://antecedent.github.io/patchwork

23
Gordon

Aop phppeclエクステンションを使用するタイムトラベラーlibを使用して、Rubyモンキーパッチ https://github.com/rezzza/TimeTraveler

Ruby timecop one: https://github.com/hnw/php-timecop からインスピレーションを得たこのphp拡張機能もあります

8
shouze

@Gordonがすでに指摘していることに加えて、現在の時刻に依存するコードをテストする方法が1つあります。

私は、現在の時刻などを要求できるクラスを自分で作成する必要があるという問題を回避できる「グローバル」値を取得する保護されたメソッドを1つだけモックアウトしています(これはよりクリーンですが、phpでは議論の余地があります/人々がそれをしたくないことは理解できます)。

これは次のようになります。

class Calendar {
    public function getCurrentTimeAsISO() {
        return $this->currentTime()->format('Y-m-d H:i:s');
    }

    protected function currentTime() {
        return new DateTime();
    }
}

class CalendarTest extends PHPUnit_Framework_TestCase {
    public function testCurrentDate() {
        $cal = $this->getMockBuilder('Calendar')
            ->setMethods(array('currentTime'))
            ->getMock();
        $cal->expects($this->once())
            ->method('currentTime')
            ->will($this->returnValue(
                new DateTime('2011-01-01 12:00:00')
            )
        );
        $this->assertSame(
            '2011-01-01 12:00:00',
            $cal->getCurrentTimeAsISO()
        );
    }
}
2
edorian

SymfonyのWebTestCase を使用してPHPUnitテストバンドルを使用して機能テストを実行しているため、DateTimeクラスのすべての使用法をモックすることはすぐに非現実的になりました。

Cookieやキャッシュの有効期限のテストなど、時間の経過とともにリクエストを処理するアプリケーションをテストしたかったのです。

これを行うために私が見つけた最善の方法は、デフォルトクラスを拡張する独自のDateTimeクラスを実装し、その時点以降に作成されるすべてのDateTimeオブジェクトにデフォルトのタイムスキューを追加/減算できるようにする静的メソッドを提供することです。 。

これは実装が非常に簡単な機能であり、カスタムライブラリをインストールする必要はありません。

警告エンプター:このメソッドの唯一の欠点は、Symfonyフレームワーク(または使用しているフレームワーク)がライブラリを使用しないため、動作がないことです。内部キャッシュ/ Cookieの有効期限など、それ自体を処理することが期待されていますが、これらの変更による影響は受けません。

namespace My\AppBundle\Util;

/**
 * Class DateTime
 *
 * Allows unit-testing of DateTime dependent functions
 */
class DateTime extends \DateTime
{
    /** @var \DateInterval|null */
    private static $defaultTimeOffset;

    public function __construct($time = 'now', \DateTimeZone $timezone = null)
    {
        parent::__construct($time, $timezone);
        if (self::$defaultTimeOffset && $this->isRelativeTime($time)) {
            $this->modify(self::$defaultTimeOffset);
        }
    }

    /**
     * Determines whether to apply the default time offset
     *
     * @param string $time
     * @return bool
     */
    public function isRelativeTime($time)
    {
        if($time === 'now') {
            //important, otherwise we get infinite recursion
            return true;
        }
        $base = new \DateTime('2000-01-01T01:01:01+00:00');
        $base->modify($time);
        $test = new \DateTime('2001-01-01T01:01:01+00:00');
        $test->modify($time);

        return ($base->format('c') !== $test->format('c'));
    }

    /**
     * Apply a time modification to all future calls to create a DateTime instance relative to the current time
     * This method does not have any effect on existing DateTime objects already created.
     *
     * @param string $modify
     */
    public static function setDefaultTimeOffset($modify)
    {
        self::$defaultTimeOffset = $modify ?: null;
    }

    /**
     * @return int the unix timestamp, number of seconds since the Epoch (Jan 1st 1970, 00:00:00)
     */
    public static function getUnixTime()
    {
        return (int)(new self)->format('U');
    }

}

これを使用するのは簡単です:

public class myTestClass() {
    public function testMockingDateTimeObject()
    {
        echo "fixed:  ". (new DateTime('18th June 2016'))->format('c') . "\n";
        echo "before: ". (new DateTime('tomorrow'))->format('c') . "\n";
        echo "before: ". (new DateTime())->format('c') . "\n";

        DateTime::setDefaultTimeOffset('+25 hours');

        echo "fixed:  ". (new DateTime('18th June 2016'))->format('c') . "\n";
        echo "after:  ". (new DateTime('tomorrow'))->format('c') . "\n";
        echo "after:  ". (new DateTime())->format('c') . "\n";

        // fixed:  2016-06-18T00:00:00+00:00 <-- stayed same
        // before: 2016-09-20T00:00:00+00:00
        // before: 2016-09-19T11:59:17+00:00
        // fixed:  2016-06-18T00:00:00+00:00 <-- stayed same
        // after:  2016-09-21T01:00:00+00:00 <-- added 25 hours
        // after:  2016-09-20T12:59:17+00:00 <-- added 25 hours
    }
}
1
StampyCode

実装を変更して、DateTime()time()で明示的にインスタンス化することができます。

_new \DateTime("@".time());
_

これによってクラスの動作が変わることはありません。しかし、名前空間化された関数を提供することで、 モックtime() が可能になりました。

_namespace foo;
function time() {
    return 123;
}
_

私のパッケージを使用することもできます php-mock/php-mock-phpunit そうするために:

_namespace foo;

use phpmock\phpunit\PHPMock;

class DateTimeTest extends \PHPUnit_Framework_TestCase {

    use PHPMock;

    public function testDateTime() {
        $time = $this->getFunctionMock(__NAMESPACE__, "time");
        $time->expects($this->once())->willReturn(123);

        $dateTime = new \DateTime("@".time());
        $this->assertEquals(123, $dateTime->getTimestamp());
    }
}
_
1
Markus Malkusch