【Laravel】DBに依存させずにEloquentモデルをテストする

  • 2019年4月4日
  • 2019年4月4日
  • Laravel

 EloquentはLaravelに備わっているORM(Object-relational mapping)です。
Eloquent:利用の開始 5.7 Laravel
 オブジェクト関連マッピングとはオブジェクトとDB(データベース)の関連のマッピングのことであり、その都合上オブジェクトはデータベースと密接な関係にあります。

<?php
class Post extends Model
{
    // これだけでpostsテーブル中の各レコードを呼べる。
}


 この密接な関係のため、モデルをテストする際にDBが付きまといがちです。何を呼び出すにしても、DBの中を見に行きます。しかしモデルのメソッドのテストはDBから切り離されるべきです。これはモデルのテストがそれ以上掘り下げるべき最小単位のユニットテストであり、テスト失敗の原因がテストやソースコードのエラーかDBのエラーか考える手間を省くべきでもあるからです。
 次の様なコードを記述することでDBからEloquentモデルを切り離してテストできます。

<?php

namespace Tests\Unit\App\Models;

use App\Models\User;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
use Tests\TestCase;

class UserTest extends TestCase
{
    public function testScopeFindOrFailPort()
    {
        self::assertInstanceOf(User::class, UserMock::scopeFindOrFailPort(UserMock::query(), '20100'));
        try {
            UserMock::scopeFindOrFailPort(UserMock::query(), 'hoge');
            self::assertTrue(false, 'not foundでなかった');
        } catch (ModelNotFoundException $e) {
            self::assertTrue(true);
        }
    }
}

/**
 * モデルUserのモック。データベースに関わらず一定のものを返すようにしている。
 */
class UserMock extends User
{
    public $mock_data;
    public $mock_return_data;

    /**
     * {@inheritdoc}
     * UserMock constructor.
     * @param array $attributes
     */
    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
// テストデータ定義
        $mocks = [];
        for ($i = 0; $i < 5; ++$i) {
            $mocks[] = (new User())->forceFill([
                'id' => $i,
                'group_key' => $i % 3,
                'hash' => 'hash_'.$i,
                'port' => ['1234', '12345', '20100'][$i % 2],
                'created_at' => now(),
                'updated_at' => now(),
                'deleted_at' => null,
            ]);
        }
        $this->mock_data = new Collection($mocks);
    }

// DBにアクセスするLaravel内部に隠れている部分を引っ張り出して置き換え

    /**
     * {@inheritdoc}
     * @return User|Builder|UserMock
     */
    public static function query()
    {
        return new self();
    }

    /**
     * {@inheritdoc}
     * @param  array                    $columns
     * @return User|Model|object|null
     */
    /** @noinspection PhpHierarchyChecksInspection */
    public function first($columns = [])
    {
        return $this->mock_data->first();
    }

    /**
     * {@inheritdoc}
     * @param  array|Closure|string $column
     * @param  string|null          $operator
     * @param  null                 $value
     * @param  string               $boolean
     * @return User|UserMock
     */
    /** @noinspection PhpHierarchyChecksInspection */
    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        if (func_num_args() === 2) {
            $value = $operator;

            $operator = '=';
        }
        $this->mock_data = $this->mock_data->where($column, $operator, $value);

        return $this;
    }
}

 重要なのは、テスト対象のモデルを継承しているがテスト対象のモデル内で定義されたメソッドを変更していないクラス、をテストコードに記述することです。このUserMockクラスは、UserクラスのDBにアクセスしてデータを取ってくる部分を、テストコード中で定義する値を取ってくる様に上書きしたクラスです。テスト対象そのものを置き換えている様にも見えますが、この置き換えている部分は大過ないことが担保されているLaravel内部のテスト済みコードのModelクラスの部分です。

 テスト用クラスUserMockはテスト対象クラスUserで定義されたコードを変更せず、Userの更に奥のModelから継承したコードを変更しています。これによりUserクラスを破壊することなく、DBのアクセスの代わりにローカル変数へのアクセスを行うUserクラスであるUserMockクラスが作れます。
 このようにするとデータベースに依存することなくEloquentのテストが出来ます。こういったコードはローカルのPHPのみで完結しており、実行速度が速い、他のデータリソースを汚染しない、と便利です。ここまで小さいテストならばモデル全体を通しても10秒とかからずにテストが終わります。おかげでGitにコミットする度にローカルで走らせても苦になりません。

DBへのアクセスメソッドは大体\Illuminate\Database\Eloquent\Builder以下にそろっています。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG