composer require cakephp/chronos:^1.2
setTestNow()メソッドがクラスを横断して時刻セットできるようなった!
CakePHP 3.2以降、時刻操作クラスとして cakephp/chronos が採用されています。
CakePHP内ではChronosを継承した \Cake\I18n\FrozenTime, \Cake\I18n\Time, \Cake\I18n\FrozenDate, \Cake\I18n\Date を使用でき、データベースの時刻系のフィールドはこれらのクラスへマッピングされます。
また、Chronosにはテストを容易にするためにsetTestNow()というメソッドがあり、各クラスの現在時刻を指定の時間へ固定することができます。
use \Cake\I18n\FrozenTime;
FrozenTime::setTestNow('1975-08-08 11:22:33');
$time = new FrozenTime('1 hour ago'); // 1975-08-08 12:22:33
$now = FrozenTime::now();
FrozenTime::setTestNow($now);
sleep(10); // 10秒待つ
$currentTime = FrozenTime::now(); // 固定されているので $currentTime == $now
Chronos 1.1 までの問題
しかし、Chronos 1.1 までは、setTestNow()は各クラスごとにセットしなければなりませんでした。
以下のようにFrozenTime::setTestNowはTimeクラスへ影響しません。
use \Cake\I18n\FrozenTime;
use \Cake\I18n\Time;
FrozenTime::setTestNow('1975-08-08 11:22:33');
$time = new FrozenTime('1 hour ago'); // 1975-08-08 12:22:33
$time = new Time('1 hour ago'); // 現在時刻の1時間後
CakePHP 3.2以降のデフォルトでは、時刻系のフィールドは FrozenTime で処理されるようになっています。
通常は FrozenTime::setTestNow で時刻セットを行えばよいのですが、1ヶ所だけ Time が使われる部分があります。
TimestampBehavior の内部では、Timeが使用されています。ですので、テスト時に FrozenTime のみ時刻を固定した状態で created などの TimestampBehavior によりセットされる時刻フィールドをみると、固定した時刻ではなく実行時の時刻が入っていることになります。
これに対処するには、FrozenTime, Time の双方にテスト時刻をセットすればよいのですが、冗長な記述となっていました。
Chronos 1.2 ではこの問題が改善された
Chronos 1.2以降、setTestNowでの設定はすべてのChronosのサブクラスへ伝播するようになりました。
use \Cake\I18n\FrozenTime;
use \Cake\I18n\Time;
FrozenTime::setTestNow('1975-08-08 11:22:33');
$time = new FrozenTime('1 hour ago'); // 1975-08-08 12:22:33
$time = new Time('1 hour ago'); // 1975-08-08 12:22:33
これにより、各時刻クラスへのsetTestNowの漏れがなくなり重複した記述を減らせるので、今すぐChronos 1.2以降へバージョンアップすることをお勧めします。