Laravel8 CRUD＋検索機能のテストコード実装

##初めに
こちらの記事では、僕がPF作成で実装したCRUD+検索機能のテストコードについてご紹介します。
正直テストコードはかなり苦戦し、現役エンジニアの方に質問させてもらいながらやっとの思いで実装しました。。
公式ドキュメントや紹介されている記事でやり方がなんとなく分かったとしても、自分の実装したロジックに対してどのようなテストを書くかは自分で考えなくてはいけません。
現役エンジニアの方に言われたのですが、この辺りはテストを書く経験を積むことで養われるので、最初のうちはかなり時間がかかるようです。
人によってロジックが違う分テストの内容も変わってくるので、この記事が少しでも自身のテストを書く際に参考になってもらえると幸いです！

##SeederとFactoryの設定
先ずは、テスト時にダミーデータを作成でするようにする為にSeederとFactoryを修正します。
自分のPFではイベントを使って在庫とユーザーを紐つけ、在庫を登録した時にログインユーザーのIDを在庫テーブルのuser_idカラムに登録されるようにしているので、Seederで`\Event::fakeFor(function () `を使い、seedコマンド実行時はイベントを実行しないように設定しておきます。

```php
/database/sedders/StocksTableSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Stock;

class StocksTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        \Event::fakeFor(function () {
            Stock::factory()->count(20)->create();
        });
    }
}
```

```php
/database/factories/StockFactory

<?php

namespace Database\Factories;

use App\Models\Stock;
use Illuminate\Database\Eloquent\Factories\Factory;

class StockFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */

    protected $model = Stock::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'shop' => $this->faker->word,
            'purchase_date' => $this->faker->dateTimeBetween('-20 days', now()),
            'deadline' => $this->faker->dateTimeBetween(now(), '+1 week'),
            'name' => $this->faker->word,
            'price' => $this->faker->numberBetween(10,100),
            'number' => $this->faker->numberBetween(10,100),
            'user_id' => $this->faker->numberBetween(1,3),
            'image' => $this->faker->word,
            'created_at' => $this->faker->datetime($max = 'now', $timezone = date_default_timezone_get()),
            'updated_at' => $this->faker->datetime($max = 'now', $timezone = date_default_timezone_get())
        ];
    }
}

```


##HTTPリクエスト
###ステータスコード
今回のメインはデータベースのテストになりますが、簡単にHTTPリクエストのテストもご紹介しておきます。
テスト内容：`設定したルートにアクセスした際に想定のステータスコードが返ってくるか`
ログイン者しか使用できない機能に関しては`302`が返ってくるかで、誰でもアクセスできる場合は`200`が返ってくるか確認します。

リクエストとレスポンス、ステータスコード一覧についてこちらの記事が詳しく紹介されているので、ご覧になってみてください。
https://www.itmanage.co.jp/column/http-www-request-response-statuscode/

```php
    public function test_admin_list_screen_can_be_rendered()
    {
        $response = $this->get('/admin/list');
        $response->assertStatus(302);
    }

    //パスにidを付属している場合
　　　　　　　　public function test_edit_screen_can_be_rendered()
    {
        $response = $this->get('/list/edit/{id}');
        $response->assertStatus(302);
    }
```

###ログイン認証
テスト内容：`ログインして想定通りのパスにリダイレクトされるか`と`ユーザーが認証されているか`
Assertメソッドについて詳しく知りたい方はこちらを参考にしてみてください。
https://qiita.com/taku-0728/items/8cc6bb2ce9ec54168686

```php
    public function test_Can_Login(): void
    {
        //ログインするアカウント作成
        $user = User::factory(User::class)->create([
            'password' => bcrypt('password'),
            'role' => 'admin',
        ]);

        //　/loginからログイン情報をpost
        $response = $this->from('/login')->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);

        //ログイン後に想定通りのパスにリダイレクトされるか
        $response->assertRedirect('/admin/list');

        //ログインしたアカウントが認証されているか
        $this->assertAuthenticatedAs($user);
    }
```

##CRUD機能のデータベーステスト
###レコード追加と件数取得
テスト内容：`ログインして想定通りのパスにリダイレクトされるか`と`ユーザーが認証されているか`
`$this->actingAs($user)->get('/list')`とすることでログイン状態で指定のパスにアクセスできます。

```php
    public function testInsertFactoryTest()
    {
        // ユーザー認証して画面遷移
        $user = User::factory(User::class)->create([
            'password' => bcrypt('password'),
        ]);
        $response = $this->actingAs($user)->get('/list');

　　　　　　　　　　　　　　　　//在庫レコードを3つ作成
        $stocks = Stock::factory(Stock::class)->count(3)->create();

        //作成した在庫の数をカウント
        $count = count($stocks);
         
        //作成した在庫の数とカウントされた数が同じか確認
        $this->assertEquals(3, $count);
    }
```
###レコード一覧表示
テスト内容：`登録したレコードが一覧表示できているか`
ここのポイントは、`ログインするアカウントを作成する時にユーザーidとロールを指定していること`です。

今回僕のPFは下記のようなデータベース構造になっています。
・ユーザーをロールで管理し、アカウント作成時はデフォルトでuserロールで作成される。
・usersテーブルのidとstocksテーブルのuser_idを紐付けている。

idもプライマリキーなので、実際に利用者アカウントを作成するときはわざわざ指定する必要はないのですが、テストの場合はちゃんと指定してあげないと`ログインした際にロールで判別されない`のと、`レコードを作成した際にuser_idカラムがnullであるとエラーが出てしまう`ので注意してください！

また、一覧表示ではstocksテーブルの3つのカラムを表示させて、詳細画面で全てのカラムを表示するようにしている為、このテストでは3つのカラムのみ値の確認をしています。

```php
    public function testListFactoryTest()
    {
        $user = User::factory(User::class)->create([
            'id' => 2,
            'password' => bcrypt('password'),
            'role' => 'user',
        ]);
        $response = $this->actingAs($user)->get('/list');

        //在庫レコードを作成
        $stock = Stock::factory(Stock::class)->create([
            'shop' => 'セブン',
            'purchase_date' => '2021-04-12',
            'deadline' => '2021-06-12',
            'name' => 'サンプル',
            'price' => 200,
            'number' => 10,
        ]);

        // /listにアクセスして想定通りのタイトルが返ってくるか確認
        $response->assertSee('在庫一覧');

　　　　　　　　　　　　　　　　// /listで在庫情報の3つのカラムが表示されているか確認
        $this->assertEquals('2021-06-12', $stock['deadline']);
        $this->assertEquals('サンプル', $stock['name']);
        $this->assertEquals(10, $stock['number']);
    }
```
###レコード情報の詳細表示
テスト内容：`詳細画面にアクセスした際に詳細情報が想定通りに表示できているか`
このテストは一覧表示とほとんど変わりませんが、変わった点とポイントとしては、`idを付属させたパスへのアクセス`になります！

```php
    public function testShowFactoryTest()
    {
        $user = User::factory(User::class)->create([
            'id' => 3,
            'password' => bcrypt('password'),
            'role' => 'user',
        ]);
        $response = $this->actingAs($user)->get('/list');

        $stock = Stock::factory(Stock::class)->create([
            'shop' => 'セブン',
            'purchase_date' => '2021-04-12',
            'deadline' => '2021-06-12',
            'name' => 'サンプル',
            'price' => 200,
            'number' => 10,
        ]);

        // /listからログイン状態で詳細画面に遷移
        $response = $this->actingAs($user)->get('/list/show/'.$stock['id']);

        $response->assertSee('在庫詳細');
        $this->assertEquals('セブン', $stock['shop']);
        $this->assertEquals('2021-04-12', $stock['purchase_date']);
        $this->assertEquals('2021-06-12', $stock['deadline']);
        $this->assertEquals('サンプル', $stock['name']);
        $this->assertEquals(200, $stock['price']);
        $this->assertEquals(10, $stock['number']);
    }
```
レコード作成時に値を指定しない場合、下記のようにして確認することもできます。
ポイントは、`日付の値を確認する際にフォーマットを指定すること`です！

```php
$stock = Stock::factory(Stock::class)->create();
$response->assertSee($stock['name']);
$response->assertSee($stock['deadline']->format('Y-m-d'));
```
###レコード情報の更新
テスト内容：`想定通りにレコード情報の更新ができるか`
このテストでは、更新した情報がデータベースに反映されることが確認できれば良いので、先程と違ってログインアカウント作成時にidやロールを作成しなくてもエラーは出ませんでした！
また、レコードの更新は`updateメソッド`を使って行います。
なので、下記のように書けば大丈夫かと思います。

```php
    public function testUpdateFactoryTest()
    {
        $user = User::factory(User::class)->create([
            'password' => bcrypt('password'),
        ]);
        $response = $this->actingAs($user)->get('/list');

        $stock = Stock::factory(Stock::class)->create();

        //作成したレコードの情報を更新
        $stock->update([
            'shop' => 'サンプル',
            'purchase_date' => '2021-04-12',
            'deadline' => '2021-06-12',
            'name' => 'サンプル',
            'price' => 200,
            'number' => 10
        ]);

　　　　　　　　　　　　　　　　//一覧表示画面で更新した内容が表示されているか確認
        $this->assertEquals('サンプル', $stock['shop']);
        $this->assertEquals('2021-04-12', $stock['purchase_date']);
        $this->assertEquals('2021-06-12', $stock['deadline']);
        $this->assertEquals('サンプル', $stock['name']);
        $this->assertEquals(200, $stock['price']);
        $this->assertEquals(10, $stock['number']);
    }
```
###レコード削除
テスト内容：`想定通りレコードがデータベースから削除されるか`
このテストでは、`deleteメソッド`でレコードを削除し、`assertDeletedメソッド`で削除できているか確認しています！
ここで気になった点があるのですが、公式ドキュメントを見ると下記のように記載されています。
`assertSoftDeletedメソッドは、指定したEloquentモデルが「ソフト削除」されたことをアサートします。`
ここで`「ソフト削除」`という言葉が使われているのですが、他にも削除の種類があるのか分からないので、是非この件について理解できている方がいれば教えていただけると嬉しいです！

```php
    public function testDeleteFactoryTest()
    {
        $user = User::factory(User::class)->create([
            'password' => bcrypt('password'),
        ]);
        $response = $this->actingAs($user)->get('/list');

        $stock = Stock::factory(Stock::class)->create();

        //レコードを削除
        $stock->delete();

　　　　　　　　　　　　　　　　//レコードが削除されているか確認
        $this->assertDeleted($stock);
    }
```

##検索機能のテスト
###レコード検索
テスト内容：`検索ワードをpostしてレコードの検索結果が想定通り表示されるか`
こちらも書き方はこれまでとあまり変わりませんが、ポイントとしては検索ワードのpostの仕方ですね！

```php
    public function testSeachFactoryTest()
    {
        $user = User::factory(User::class)->create([
            'id' => 4,
            'password' => bcrypt('password'),
            'role' => 'user',
        ]);
        $response = $this->actingAs($user)->get('/list');

        $stock = Stock::factory(Stock::class)->create([
            'shop' => 'セブン',
            'purchase_date' => '2021-04-12',
            'deadline' => '2021-06-12',
            'name' => 'サンプル',
            'price' => 200,
            'number' => 10,
        ]);

        //ログイン状態で検索結果画面に検索ワードをpostして遷移
        $response = $this->actingAs($user)->post('/list/search', [
            'search' => 'サンプル',
        ]);

        $response->assertSee('在庫検索');
        $response->assertSee('該当商品がありました');
        $this->assertEquals('2021-06-12', $stock['deadline']);
        $this->assertEquals('サンプル', $stock['name']);
        $this->assertEquals(10, $stock['number']);
    }
```

##画像アップロードのテスト
上記のテストでは画像アップロードのテストを行っていませんでした。
画像のテストを組み込む場合は下記のように実装することができます！

```php
//レコード詳細表示
    public function testShowFactoryTest()
    {
        //ログインユーザーを作成して画面遷移
        $user = User::factory(User::class)->create([
            'id' => 3,
            'password' => bcrypt('password'),
            'role' => 'user',
        ]);
        $response = $this->actingAs($user)->get('/list');

        //フェイクディスクの作成
        //storage/framework/testing/disks/stocksに保存用ディスクが作成される
        Storage::fake('stocks');

        //UploadedFileクラス用意
        $file = UploadedFile::fake()->image('stock.jpg');

        //作成した画像を移動
        $file->move('storage/framework/testing/disks/stocks');

        //在庫作成
        $stock = Stock::factory(Stock::class)->create([
            'image' => $file,
        ]);

        // /listからログイン状態で詳細画面に遷移
        $response = $this->actingAs($user)->get('/list/show/'.$stock['id']);

        //画像データ保存確認
        Storage::disk('stocks')->assertExists($file->getFileName());
        //タイトル表示確認
        $response->assertSee('在庫詳細');
        //在庫情報表示確認
        $response->assertSee($stock['shop']);
        $response->assertSee($stock['purchase_date']->format('Y-m-d'));
        $response->assertSee($stock['deadline']->format('Y-m-d'));
        $response->assertSee($stock['name']);
        $response->assertSee($stock['price']);
        $response->assertSee($stock['number']);
        $response->assertSee($stock['image']);
    }

//レコード検索
    public function testSeachFactoryTest()
    {
        $user = User::factory(User::class)->create([
            'id' => 4,
            'password' => bcrypt('password'),
            'role' => 'user',
        ]);
        $response = $this->actingAs($user)->get('/list');

        //フェイクディスクの作成
        //storage/framework/testing/disks/stocksに保存用ディスクが作成される
        Storage::fake('stocks');

        //UploadedFileクラス用意
        $file = UploadedFile::fake()->image('stock.jpg');

        //作成した画像を移動
        $file->move('storage/framework/testing/disks/stocks');

        //在庫作成
        $stock = Stock::factory(Stock::class)->create([
            'shop' => 'セブン',
            'purchase_date' => '2021-04-12',
            'deadline' => '2021-06-12',
            'name' => 'サンプル',
            'price' => 200,
            'number' => 10,
            'image' => $file,
        ]);

        //ログイン状態で検索結果画面に検索ワードをpostして遷移
        $response = $this->actingAs($user)->post('/list/search', [
            'search' => 'サンプル',
        ]);

        //画像データ保存確認
        Storage::disk('stocks')->assertExists($file->getFileName());
        //タイトルとメッセージ表示確認
        $response->assertSee('在庫検索');
        $response->assertSee('該当商品がありました');
        //検索結果表示確認
        $this->assertEquals('2021-06-12', $stock['deadline']);
        $this->assertEquals('サンプル', $stock['name']);
        $this->assertEquals(10, $stock['number']);
    }
```

CRUD機能と検索機能のテストについては以上になります！
自身で作成されたロジックによってテストの書き方も変わってくるので、上記を参考にカスタマイズしてもらえたらと思います！

##利用者ログインと管理者ログイン両方実行する場合
最後に、一つのテストの中で利用者ログインと管理者ログイン両方を実行するテストの場合の書き方についてご紹介します！
これは、`管理者画面で利用者の登録したレコードやアカウントを管理するとき`などに必要になってきます。

例えば、利用者画面でレコードを作成し、その登録したレコードを管理者画面で表示できるか確認するような時は下記のように書いていきます。
ポイントは、`利用者でログインしてレコードを作成した後に必ずログアウトを実行すること`です！

```php
    public function testListFactoryTest()
    {
        //利用者アカウントでログインし利用者用の在庫一覧画面へ遷移
        $user = User::factory(User::class)->create([
            'id' => 9,
            'password' => bcrypt('password'),
            'role' => 'user',
        ]);
        $response = $this->actingAs($user)->get('/list');

        //在庫を作成
        $stock = Stock::factory(Stock::class)->create();

        //利用者アカウントログアウト
        $this->post('logout');

        //管理者アカウントでログインし管理者用の在庫一覧画面へ遷移
        $adminUser = User::factory(User::class)->create([
            'password' => bcrypt('password'),
            'role' => 'admin',
        ]);
        $response = $this->actingAs($adminUser)->get('/admin/list');

        // /admin/listで在庫情報の2つのカラムが表示されているか確認
        $response->assertSee('在庫一覧');
        $response->assertSee($stock['name']);
        $response->assertSee($stock['user_id']);
    }
```

以上になります！
実務ではもっと細かくテストを書くことになると思いますが、未経験でPFを作成されている方であれば、この辺りを参考にしてもらえればある程度のところまでは書けるのかなと思うのでぜひ参考にしてください！

##参考サイト
テストコード実装にあたり、参考にしたサイトを下記にまとめて記載させてもらいます。

https://readouble.com/laravel/8.x/ja/database-testing.html

https://readouble.com/laravel/8.x/ja/http-tests.html

https://re-engines.com/2020/04/13/laravel-test/

##追記
画像アップロードをS3で行うよう機能修正した為、画像保存テストコの内容を一部修正しました。
修正内容についてはこちらの記事で別途紹介していますので是非ご覧になってみてください。

https://qiita.com/yyy752/items/9dc3d8ec2797d8173212