LaravelでモデルのUnitテストを書く時の注意

# はじめに

以下の記事にあるように、インスタンス化したモデルに自分で値をセットすることで、Unitテストを行うことができます。
ですが、この時プロパティを`date`へキャストする設定を行うとエラーが発生します。その時の解決法です。

https://qiita.com/ggg-mzkr/items/507c2921e442ad3ccab9

# TL;DR
* モデルのプロパティを`Datetime`へキャストするときは

```php
<?php
...
class User extends Authenticatable
{
...
    protected $casts = [
        'active_from' => 'datetime' // active_fromカラムの値をDatetimeへキャストする
    ];

    public function isActive(): bool
    {
        return $this->active_from < Carbon::now();
    }
}

```
* `setDateFormat()`を呼ぶ

```php
<?php

namespace Tests\Unit;

use App\Models\User;
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    public function test_isAdmin()
    {
        $user = new User();
        $user->setDateFormat('Y-m-d H:i:s'); // これ
        $user->active_from = Carbon::yesterday();

        self::assertTrue($user->isActive());
    }
}

```


# キャストの設定

次のようなモデルを想定します。`$casts`を使って、`active_from`を`Datetime`へキャストする設定を書いています。

```php
<?php

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'password',
        'active_from'
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'active_from' => 'datetime' // active_fromカラムの値をDatetimeへキャストする
    ];

    public function isActive(): bool
    {
        return $this->active_from < Carbon::now();
    }
}

```

## Unitテストを書いてみる

Unitテストは以下のようになります。一見動きそうですが、実行するとエラーになります。

```php
<?php

namespace Tests\Unit;

use App\Models\User;
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    public function test_isActive()
    {
        $user = new User();
        $user->active_from = Carbon::yesterday();

        self::assertTrue($user->isActive());
    }
}

```

```shell
PHPUnit 9.5.5 by Sebastian Bergmann and contributors.


Error : Call to a member function connection() on null
 /xxxxx/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1569
 /xxxxx/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1535
 /xxxxx/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:1139
 /xxxxx/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:1087
 /xxxxx/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:740
 /xxxxx/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1904
 /xxxxx/tests/Unit/UserTest.php:16
 
Time: 00:00.027, Memory: 12.00 MB

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.
```


## 何故なのか
スタックトレースを追ってみます。

まず、`HasAttributes.php:740`を見ます。（とんでもない行数だ...)
この`setAttribute()`関数はUserモデルの`active_from`に値をセットする時に呼ばれる関数のようです。

```php
    public function setAttribute($key, $value)
    {
        // First we will check for the presence of a mutator for the set operation
        // which simply lets the developers tweak the attribute as it is set on
        // this model, such as "json_encoding" a listing of data for storage.
        if ($this->hasSetMutator($key)) {
            return $this->setMutatedAttributeValue($key, $value);
        }

        // If an attribute is listed as a "date", we'll convert it from a DateTime
        // instance into a form proper for storage on the database tables using
        // the connection grammar's date format. We will auto set the values.
        elseif ($value && $this->isDateAttribute($key)) {
            $value = $this->fromDateTime($value); // ここ！
        }

        if ($this->isClassCastable($key)) {
            $this->setClassCastableAttribute($key, $value);

            return $this;
        }

        if (! is_null($value) && $this->isJsonCastable($key)) {
            $value = $this->castAttributeAsJson($key, $value);
        }

        // If this attribute contains a JSON ->, we'll set the proper value in the
        // attribute's underlying array. This takes care of properly nesting an
        // attribute in the array's value in the case of deeply nested items.
        if (Str::contains($key, '->')) {
            return $this->fillJsonAttribute($key, $value);
        }

        if (! is_null($value) && $this->isEncryptedCastable($key)) {
            $value = $this->castAttributeAsEncryptedString($key, $value);
        }

        $this->attributes[$key] = $value;

        return $this;
    }
```

続いて`HasAttributes.php:740`から呼ばれる`HasAttributes.php:1087`を見ます。`fromDateTime()`関数はモデルにセットされた値をDB用へ変換する関数のようです。続いて`fromDateTime()`から呼ばれる`getDateFormat()`を見ます。

```php
    /**
     * Convert a DateTime to a storable string.
     *
     * @param  mixed  $value
     * @return string|null
     */
    public function fromDateTime($value)
    {
        return empty($value) ? $value : $this->asDateTime($value)->format(
            $this->getDateFormat()
        );
    }
```

DBにDatetime型の値を保存するときの変換フォーマットを取得するメソッドです。ここで、`$this->dateFormat`が取得できなかったときに`$this->getConnection()`としてDBからフォーマットを取得しようとします。上の記事にあるように、UnitテストではDBアクセスを行うことができないので、エラーが発生してしまいます。

```php
    /**
     * Get the format for database stored dates.
     *
     * @return string
     */
    public function getDateFormat()
    {
        return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat();
    }
```

## どうするのか

答えは単純で、`$this->dateFormat`にフォーマットを自分でセットしてしまえばOKです。そのための`setDateFormat()`というメソッドがありますのでこちらを使います。

```php
<?php

namespace Tests\Unit;

use App\Models\User;
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    public function test_isAdmin()
    {
        $user = new User();
        $user->setDateFormat('Y-m-d H:i:s'); // これ
        $user->active_from = Carbon::yesterday();

        self::assertTrue($user->isActive());
    }
}

```

テストが通る!

```shell
PHPUnit 9.5.5 by Sebastian Bergmann and contributors.



Time: 00:00.054, Memory: 10.00 MB

OK (1 test, 1 assertion)
```