日付関連のテストを書く時は絶対にスタブ化しろ

## サンプルコード

スタブ化しないとまずいケースを紹介します。

```ProductCode.php
<?php
class Sample{
    /**
     * @var string キャンセル日
     */
    private $cancelDate;

    /**
     * @param string キャンセル日
     */
    function __construct($cancelDate){
        $this->cancelDate = $cancelDate;
    }

    /**
     * @param string キャンセル日
     */
    public function setCancelDate($cancelDate){
        $this->cancelDate = $cancelDate;
    }

    /**
     * 有効なユーザーか無効なユーザーかを文字列で返す
     *
     * @return string
     */
    public function isActiveUser(){
        if($this->isActive(date("Y/m/d"), $this->cancelDate)){
            return '有効なユーザーです';
        } else {
            return '無効なユーザーです';
        }
    }

    /**
     * 有効な日付かどうか判定する関数
     *
     * @param string $date 比較する日
     * @param string $cancelDate キャンセル日
     * @return bool キャンセル日を過ぎていなければtrue, 過ぎていればfalse
     */
    public function isActive($date, $cancelDate){
        return strtotime($date) < strtotime($cancelDate);
    }

}

//$sample = new Sample('2000/01/01');
//// 無効なユーザーです
//var_dump($sample->isActiveUser());
//// 有効なユーザーです
//$sample->setCancelDate('2100/01/01');
//var_dump($sample->isActiveUser());
```

```ProductCodeTest.php
<?php
/**
 * 実行方法
 * brew install phpunit
 * phpunit ProductCodeTest.php
 */
use PHPUnit\Framework\TestCase;
require_once 'ProductCode.php';

class ProductCodeTest extends TestCase
{
    /* isActiveのテスト */
    // 省略

    /* isActiveUserのテスト */
    public function test_isActiveUser_有効なユーザーの場合は有効である旨のメッセージを返すこと()
    {
        $sample = new Sample('2100/01/01');
        $result = $sample->isActiveUser();
        $this->assertSame('有効なユーザーです', $result);
    }

    public function test_isActiveUser_無効なユーザーの場合は無効である旨のメッセージを返すこと()
    {
        $sample = new Sample('2000/01/01');
        $result = $sample->isActiveUser();
        $this->assertSame('無効なユーザーです', $result);
    }
}
```

```bash:結果
$ phpunit ProductCodeTest.php
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 00:00, Memory: 18.00 MB

OK (2 tests, 2 assertions)
```

## 問題点

テストは通っていますがこのテストにはいくつか問題点があります。

* 現在時刻と比較しているため「有効である」状態にするために2100年を指定していますが2100年以降は失敗します。
    * 100年後のことは知らん。
    * じゃあ1000年後を指定しよう。
    * という人はこれ以降の話は読まなくて大丈夫です。
* cancelDateという状態を持っており、例えば後続で同じインスタンスを使い回すようにした場合常にcacelDateの状態を気にしないといけないです。
    * 今回は簡易的にプロパティにしましたがキャンセル日をDBに保持しており、都度updateをして状態を整えないといけない、と思うと大変ですよね？

## 改善策

```ProductCodeFixTest.php
<?php
/**
 * 実行方法
 * brew install phpunit
 * phpunit ProductCodeFixTest.php
 */
use PHPUnit\Framework\TestCase;
require_once 'ProductCode.php';

class ProductCodeFixTest extends TestCase
{
    /* isActiveのテスト */
    // 省略

    /* isActiveUserのテスト */
    public function test_isActiveUser_有効なユーザーの場合は有効である旨のメッセージを返すこと()
    {
        // スタブを作成します
        $stub = $this->getMockBuilder(Sample::class)
                     ->disableOriginalConstructor()
                     ->setMethodsExcept(array('isActiveUser'))
                     ->getMock();
        // スタブの設定を行います
        // isActiveメソッドが常にtrueを返すようにする
        $stub->method('isActive')
             ->willReturn(true);

        $result = $stub->isActiveUser();
        $this->assertSame('有効なユーザーです', $result);
    }

    public function test_isActiveUser_無効なユーザーの場合は無効である旨のメッセージを返すこと()
    {
        // スタブを作成します
        $stub = $this->getMockBuilder(Sample::class)
                     ->disableOriginalConstructor()
                     ->setMethodsExcept(array('isActiveUser'))
                     ->getMock();
        // スタブの設定を行います
        // isActiveメソッドが常にfalseを返すようにする
        $stub->method('isActive')
             ->willReturn(false);

        $result = $stub->isActiveUser();
        $this->assertSame('無効なユーザーです', $result);
    }
}
```

```bash:結果
$ phpunit ProductCodeTest.php
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 00:00, Memory: 18.00 MB

OK (2 tests, 2 assertions)
```

スタブ化することでisActiveがどんな状況であれtrue/falseを返すようになっているため、100年後でも1000年後でも失敗することはありません。
また、状態を変えている訳ではなく1つのテストケース内での振る舞いを変えているだけなので他のテストへの影響がありません。

## まとめ

* 以下の理由から日付関連はスタブ化することをオススメします。
    * 100年後とかに設定した場合100年後にはコケてしまう時限式トラップになってしまう。
        * いつ実行しても通るテストを目指すべき。
    * 状態を変えてしまうと後続のテストにも影響する可能性がある。
        * そもそもテスト単体で独立しているべきではあるが実際そうでないことが多い。