4
3

More than 3 years have passed since last update.

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

Posted at

はじめに

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

TL;DR

  • モデルのプロパティをDatetimeへキャストするときは
<?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

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_fromDatetimeへキャストする設定を書いています。

<?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

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());
    }
}

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に値をセットする時に呼ばれる関数のようです。

    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()を見ます。

    /**
     * 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アクセスを行うことができないので、エラーが発生してしまいます。

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

どうするのか

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

<?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());
    }
}

テストが通る!

PHPUnit 9.5.5 by Sebastian Bergmann and contributors.



Time: 00:00.054, Memory: 10.00 MB

OK (1 test, 1 assertion)
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3