Laravel 【初級】 Featureテスト (index, create, store, show, edit, update, destroy)

# はじめに
テスト用のDB作成やテスト用のファイル作成が済んでいる前提で解説します。
また、業務でFeatureテストを書く事がありますが、果たしてこの書き方でいいのか確信が持てない為、ご意見を頂けると嬉しいです。

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [テスト対象のコード](#テスト対象のコード)
2. [テスト用のダミーデータ作成](#テスト用のダミーデータ作成)
3. [Featureテスト( index )](#indexアクションテスト)
4. [Featureテスト( create )](#createアクションテスト)
5. [Featureテスト( store )](#storeアクションテスト) 
6. [Featureテスト( show )](#showアクションテスト)
7. [Featureテスト( edit )](#editアクションテスト)
8. [Featureテスト( update )](#updateアクションテスト)
9. [Featureテスト( destroy )](#destroyアクションテスト)
10. [参考文献](#reference)

<!-- 各チャプター -->
# テスト対象のコード
こんな感じのクソショボい処理のFeatureテストを書いていきます。
Qiitaみたいに記事一覧を閲覧したり、投稿、更新、削除等の機能がある処理のテストコードを例に書いていきます。

__ルーティング__
routes/user.php (ルーティングファイル)

```
Route::resource('post', PostController::class);
```
__コントローラー__
PostController.php

```
public function index()
{
    $posts = Post::get();
    return view('user.post.index', ['posts' => $posts]);
}

 〜　間の処理は省略します(create, store, show, edit, update)　〜

public function destroy(Post $post)
{
    $post->delete();
    return redirect()->action([PostController::class,'index']);
}
```


# テスト用のダミーデータ作成
コマンドでテスト用のファイルを作成後、こちらにテストで使用するダミーデータを用意します。


```
// tests/Feature/User/PostControllerTest.phpファイル

// RefreshDatabaseをuse宣言
use Illuminate\Foundation\Testing\RefreshDatabase;

// 使用するモデルを追記
use App\Models\User;
use App\Models\Project;

class PostControllerTest extends TestCase
{
    // RefreshDatabaseにて、DBのデータを各テスト毎にリセット
    use RefreshDatabase;

    Public function setUp(): void
    {
        parent::setUp();

        // UserFactory()で定義したユーザーのダミーデータを1件作成
        $this->user = User::factory()->create();

        // PostFactory()で定義したユーザーのダミーデータを1件作成
        $this->post = Post::factory()->create();

        // store, update処理で使用するRequestパラメーター
　　　　　　　　　　　　　　　　// パラメータの値としては上記で作成したダミーデータを使用する。
        $this->data =
            [
                // 上記で作成した$this->userのid
                'user_id' => $this->user->id,
                'title' => $this->post->title,
                'content' => $this->post->content,
            ];
    }

```
__【各ヘルパメソッド】__
__・setUp__
詳しくは下の参考文献を参照ください。

__・RefreshDatabase__
毎回テストメソッドを実行する度にテスト用DBをリセットして、マイグレーションを実行しています。
※マイグレーションのfreshをやってます。
IDのオートインクリメントはリセットされない為、ダミーデータのidはテストを繰り返す毎に増えていきます。


# indexアクションテスト

__テスト対象のindexアクション__
TOP画面から投稿一覧画面('user.index')から、投稿一覧画面('user.post.index')に遷移する処理。

```
public function index()
{
    $posts = Post::get();
    return view('user.post.index', ['posts' => $posts]);
}
```
<br>
__indexアクションテスト__

・先程のダミーデータの下にテストを書いていきます。
・testIndex( )と書いていますが、<font color="blue">__テストメソッドの先頭に'test'を付けて、その後の文字は任意で設定__</font>して下さい。
※アノテーション(@test)をつける方法もありますので、こちらの方法で命名をしたい方はこちらを参照ください。
https://qiita.com/apricotcomic/items/76a5c4cf63dbd0e1b39c

```
// tests/Feature/User/PostControllerTest.phpファイル

class ReportControllerTest extends TestCase
{
    public function testIndex()
    {
        // エラー内容を分かりやすくしてくれる(Laravel8.51以降では不要・・・らしいです)
        $this->withoutExceptionHandling();

　　　　                  // TOPページから画面遷移
        $response = $this->from(route('user.index'))
　　　　　　　　　　　       // 投稿一覧ページに画面遷移
                         ->get(route('user.post.index'));
　
　　　　　　　　　　　　　　　 　        // テスト成功 (HTTPレスポンス200)
        $response->assertOk()
                 // ルートによって指定されたビューページが返されているか確認
                 ->assertViewIs('user.post.index')
　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　// ビューに渡すデータが合っているか確認
                 ->assertViewHas(['posts']);
    }
```

__【各ヘルパメソッド】__
__・withoutExceptionHandling()__
Laravel8.51以前までは、テストで失敗した際にエラーメッセージを分かりやすく表示してくれていましたが、Laravel8.51以降からは別に付けなくてもエラー原因が表示されるみたいです。
【参考記事】
https://zenn.dev/nshiro/articles/a00dea5f688cf9
特定の例外が投げられているかテストしたい時には使えるらしいです。

__・from( )__
どのページから遷移するのか設定できます。引数に遷移元のルートを記載します。

__・get( )__
<font color="blue">__リクエストパラメーターのgetです。storeではpost、updateではput、destroyではdeleteを使用__</font>します。
今回はindexメソッドで投稿一覧にアクセスするのでgetメソッドを使用します。

__【assertメソッド】__
__・assertOk( )__
<font color="blue">__HTTPレスポンスで200が返ってきた場合にテスト成功__</font>となります。
逆に200以外が返ってきた場合はテスト失敗となります。
※assertStatus(200)と同様の意味になります。

__・assertViewIs( )__
遷移先のページがviews/posts/index.blade.phpである事を確認します。
違うページに遷移していたら、テスト失敗となります。

__・assertViewHas( )__
ビューに$postsのデータが渡されているかチェックします。




#createアクションテスト

__テスト対象のcreateアクション__
ログインしたユーザーのみ投稿一覧画面('user.post.index')から、新規投稿画面('user.post.create')に遷移する処理。
<font color="blue">__※index以外の処理はログインしていないと操作できない仕様とします。__<font>

```
public function create()
{
   return view('user.post.create');
}
```
<br>
__createアクションテスト__

```
public function testCreate()
{
    $this->withoutExceptionHandling();

　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　     　// ログインユーザーで画面遷移
    $response = $this->actingAs($this->user)
                     ->from(route('user.post.index'))
                     ->get(route('user.post.create'));
　
    $response->assertOk()
             ->assertViewIs('user.post.create');
}
```

__【各ヘルパメソッド】__
__・actingAs( )__
<font color="blue">__ログイン中のユーザーしか閲覧できないページがある場合に使用__</font>します。
引数に先程作成したダミーデータのユーザーを入れる事で、擬似的にそのユーザーがログインしている状況を作ります。

#storeアクションテスト

__テスト対象のstoreアクション__
新規投稿画面('user.post.create')から投稿後、投稿一覧画面('user.post.index')に遷移する処理。

```
public function store(Request $request)
{
    $post = new Post;

    $post->title = request('title');
    $post->content = request('content');
    $post->save();

    return redirect()->action([PostController::class,'index']);
}
```
<br>
__storeアクションテスト__
・リクエストパラメーターはpostの点に注意して下さい。
・$this->dataのダミーデータを用いてDB登録処理を行う為、記述して下さい。

```
public function testStore()
{
    $this->withoutExceptionHandling();

    $response = $this->actingAs($this->user)
                     ->from(route('user.post.create'))
                     // リクエストパラメーターpost
　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　// $this->data　=> storeの$requestのデータにあたる部分
                     ->post(route('user.post.store'), $this->data);
　        
               // 指定したURIへリダイレクトする。
    $response->assertRedirect(route('user.post.index', ['posts' => $this->posts]));
}
```
__【各ヘルパメソッド】__
__・post( )__
<font color="blue">__リクエストパラメーターのpost__</font>です。store処理で使用される事が多いです。

__【assertメソッド】__
__・assertRedirect( )__
引数に指定したURI通りにリダイレクトしているか確認します。

#showアクションテスト

__テスト対象のshowアクション__
投稿一覧画面('user.post.index')から、投稿詳細画面(user.post.show)に画面遷移する処理。

```
public function show(Post $post)
{
    return view('user.post.show', ['post' => $post]);
}
```
<br>
__showアクションテスト__
ルートモデルバインディングから受け取った$postをviewに渡す為、書き忘れないように注意して下さい。

```
public function testShow()
{
    $this->withoutExceptionHandling();

    $response = $this->actingAs($this->user)
                     ->from(route('user.post.index'))
                     // ルートモデルバインディングから受け取った$postをviewに渡す
                     ->get(route('user.post.show'), ['post' => $this->post]);
　
    $response->assertOk()
             ->assertViewIs('user.post.show')
             ->assertViewHas('post');
}
```

#editアクションテスト

__テスト対象のeditアクション__
投稿一覧画面('user.post.index')から、投稿更新画面(user.post.edit)に画面遷移する処理。

```
public function edit(Post $post)
{
    return view('user.post.edit', ['post' => $post]);
}
```
<br>
__editアクションテスト__
showアクションのテストとほぼ同じ処理を書きます。

```
public function testEdit()
{
    $this->withoutExceptionHandling();

    $response = $this->actingAs($this->user)
                     ->from(route('user.post.index'))
                     ->get(route('user.post.edit'), ['post' => $this->post]);
　
    $response->assertOk()
             ->assertViewIs('user.post.edit')
             ->assertViewHas('post');
}
```

#updateアクションテスト

__テスト対象のupdateアクション__
投稿更新画面('user.post.edit')から投稿を更新後、投稿一覧画面('user.post.index')に遷移する処理。

```
public function update(Request $request, Post $post)
{
    $post = Post::find($request->id);
    $post->title = request('title');
    $post->content = request('content');
    $post->save();

    return redirect()->action([PostController::class,'index']);
}
```
<br>
__updateアクションテスト__
・リクエストパラメーターがputである点に注意が必要です。
・store処理同様リクエストデータ($this->data)を書き忘れないでください。

```
public function testUpdate()
{
    $this->withoutExceptionHandling();

    $response = 
     $this->actingAs($this->user)
          // editからなので、ルートモデルバンディングのパラメータを忘れずに！
          ->from(route('user.post.edit'),['post' => $this->post]) 
          // リクエストパラメーターput
          // $this->dataを書き忘れないように！                     
          ->put(route('user.post.update') ,['post' => $this->post]), $this->data);
　
    $response
     ->assertRedirect(route('user.post.index', ['posts' => $this->posts]));
}
```
__【各ヘルパメソッド】__
__・put( )__
<font color="blue">__リクエストパラメーターのput__</font>です。update処理で使用されます。

#destroyアクションテスト

__テスト対象のdestroyアクション__
投稿一覧画面('user.post.index')で投稿を削除('user.post.destroy')し、投稿一覧画面('user.post.index')にリダイレクトする処理。

```
public function destroy(Post $post)
{
    $post->delete();
    return redirect()->action([PostController::class,'index']);
}
```
<br>
__destroyアクションテスト__
・リクエストパラメーターはdelete
・今回はソフトデリートを想定している為、assertSoftDeleted( )やassertEquals( )を使用しています。
ソフトデリートでなければ、assertDeleted( )やassertDatabaseMissing( )を使用するかと思います。

```
public function testDestroy()
{
    $this->withoutExceptionHandling();

    $response = 
     $this->actingAs($this->user)
           ->from(route('user.post.index'))
           // リクエストパラメーターは delete
           ->delete(route('user.post.destroy'),['post' => $this->post]);
　
     $response->assertRedirect(route('user.post.index', ['posts' => $this->posts]));

    // レコードが削除されたことを確認
    // 注意！！　以下ソフトデリート用の処理。
    $this->assertSoftDeleted($this->post);
         ->assertEquals(0, Post::count());
}
```
__【各ヘルパメソッド】__
__・delete( )__
<font color="blue">__リクエストパラメーターのdelete__</font>です。delete処理で使用されます。

__【assertメソッド】__
__・assertSoftDeleted( )__
引数に設定しているレコードがソフトデリートされたか確認します。

__・assertEquals( )__
第一引数と第二引数の値が同じであるか確認する。
今回の場合はDBにあるデータが空になっていれば、テスト成功です。

__・assertDeleted( )__
引数に設定しているレコードがデリートされたか確認します。
※ソフトデリートの場合assertSoftDeletedを使用します。

__・assertDatabaseMissing( )__
DBに該当するレコードが無いかどうかを確認します。
第一引数にテーブル名、第二引数に該当するレコードを設定します。
<font color="blue">__※こちらはソフトデリートの場合使用できません。__</font>
下記記事を参考にして下さい。
https://techfree.jp/devmemo/devmemo-2696-3

<a id="#reference"></a>
# 参考文献
- [setUpとは](https://tektektech.com/laravel-setup-test/#:~:text=laravel%E3%81%A7%E3%81%AFsetUp%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%81%84%E3%81%86,%E5%A0%B4%E5%90%88%E3%81%AA%E3%81%A9%E3%81%AB%E4%BD%BF%E3%81%84%E3%81%BE%E3%81%99%E3%80%82&text=%E3%81%93%E3%81%86%E3%81%84%E3%81%A3%E3%81%9F%E5%85%B1%E9%80%9A%E3%81%AE,%E3%81%8CsetUp%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AA%E3%81%AE%E3%81%A7%E3%81%99%EF%BC%81)