今回は、LaravelのControllerテストのコードをテンプレとしてまとめておきます。(ほとんど車輪の再発明をしないための自分用メモです)
テストコードの書き方の一例として参考になれば幸いです。
※Laravelはバージョン6系を使用しています
Laravelコントローラテストのテンプレまとめ
今回は以下の3つのControllerについてテストを書いてみました。
- PostControllerTest
- RegisterControllerTest
- LoginControllerTest
それぞれ多くのテストケースを書いていますが、基本的な考え方はどれも同じです。
要は、Controllerのアクションに対してRequestを投げて、正しい(予想通りの)Responseが返ってくるかを判定しているだけです。難しく考える必要はありません。
それでは、それぞれのControllerについてテストケースの例を掲載します。
PostControllerTestのテストケース
PostControlerTestでは、一般的な投稿のCRUD処理に関するテストを書いています。
何も説明しないのもあれなので、簡単に概要だけ解説しますね。
今回、テスト用データはファクトリで生成しました。
また、各テストケースごとにDBを初期化するため、DatabaseMigrationsトレイトを設置しています。
最初はRefreshDatabaseトレイトを使っていたのですが、RefreshDatabaseを使った場合、オートインクリメントがリセットされないことに気付いたため、変更しました。
具体的なテストの流れは、どのテストケースも似ています。こんな感じです。
- ファクトリでテストユーザーを作成
- actingAsでユーザーを認証させる
- Controllerに対してHTTPリクエストを投げ、Response(結果)を受け取る
- それぞれのテストケースに対して適切なアサーションを利用して、Response結果の成否を判定する
ちなみに、上記はコントローラテストの場合ですが、より一般的なテストの場合に拡張するとこんな感じになります。
- テストを実行するための条件(ユーザ認証、データをインサートなど)を設定
- テスト対象を実行(HTTPリクエストを送る、クラスのメソッドを実行するなど)
- 実行結果を検証(アサーションを利用)
全てのテストの前に書かれてある/** @test */はアノテーションと言い、これを書いておくことで、日本語でテストメソッドの名前を指定することができます。詳しくはこちらの記事を参考にして下さい。
概要は以上です。ここまで理解しておけば、おそらく以下のコードは読めると思います。
<?php namespace Tests\Http\Controllers; use Illuminate\Foundation\Testing\DatabaseMigrations; use Tests\TestCase; use App\User; use App\Post; class PostControllerTest extends TestCase { use DatabaseMigrations; /** @test */ public function 投稿一覧のURLにアクセスして画面が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $response = $this->get(action('PostController@index')); $response->assertStatus(200); } /** @test */ public function 投稿一覧のURLにアクセスして投稿一覧画面が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $response = $this->get(action('PostController@index')); $response->assertViewIs('layouts.home'); } /** @test */ public function ログインしていない場合は投稿一覧のURLにアクセスしてもログイン画面が表示される() { $response = $this->get(action('PostController@index')); $response->assertRedirect(route('login')); } /** @test */ public function 投稿一覧画面に全ての投稿データが表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $firstPost = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'firstPost' ]); $secondPost = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'secondPost' ]); $thirdPost = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'thirdPost' ]); $response = $this->get(action('PostController@index')); $response->assertSee($firstPost->content, $thirdPost->content, $secondPost->content); } /** @test */ public function 投稿一覧画面に作成日時の降順で投稿が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $firstPost = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'firstPost', 'created_at'=> '2020-06-03 07:29:56' ]); $secondPost = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'secondPost', 'created_at'=> '2020-06-03 07:30:59' ]); $thirdPost = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'thirdPost', 'created_at'=> '2020-06-03 07:34:59' ]); $response = $this->get(action('PostController@index')); $response->assertSeeInOrder($expects = [$thirdPost->created_at->diffForHumans(), $secondPost->created_at->diffForHumans(), $firstPost->created_at->diffForHumans()]); } /** @test */ public function 投稿一覧画面で投稿に紐付く投稿者名が表示される() { factory(User::class, 2)->create(); // ユーザーのうち片方を認証させる $user = User::findOrFail(1); $this->actingAs($user); // 認証しているユーザーが投稿を送信し保存 $data = ['content' => 'test']; $this->post(route('posts.store'), $data); // 投稿一覧画面で認証しているユーザーの名前が表示されているか確認 $userName = $user->name; $this->get(action('PostController@index'))->assertSee($userName); } /** @test */ public function 投稿作成のURLにアクセスして画面が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $response = $this->get(route('posts.create')); $response->assertStatus(200); } /** @test */ public function 投稿作成のURLにアクセスして投稿画面が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $response = $this->get(action('PostController@create')); $response->assertViewIs('layouts.post'); } /** @test */ public function データを投稿し保存する() { $user = factory(User::class)->create(); $this->actingAs($user); $data = ['content' => 'storeTest']; $response = $this->post(route('posts.store'), $data); $this->assertDatabaseHas('posts', [ 'content' => 'storetest' ]); } /** @test */ public function 保存処理完了後は投稿一覧画面に遷移する() { $data = ['content' => 'storeTest']; $user = factory(User::class)->create(); $this->actingAs($user); $response = $this->post(route('posts.store'), $data); $response->assertRedirect(action('PostController@index')); } /** @test */ public function 投稿フォームに何も投稿しなかった場合はバリデーションメッセージが表示される() { $data = ['content' => null]; $user = factory(User::class)->create(); $this->actingAs($user); $response = $this->post(route('posts.store'), $data); $validation = '投稿内容は必ず指定してください'; $this->get(route('posts.create'))->assertSee($validation); } /** @test */ public function 詳細画面のURLにアクセスして画面が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'showTest' ]); $response = $this->get(route('posts.show', $item->id)); $response->assertStatus(200); } /** @test */ public function 詳細画面のURLにアクセスして投稿の詳細画面が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'showTest' ]); $response = $this->get(route('posts.show', $item->id)); $response->assertViewIs('layouts.item'); } /** @test */ public function 存在しない投稿の詳細を表示させようとするとエラー画面が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'showTest' ]); $response = $this->get(route('posts.show', 10)); $response->assertStatus(404); } public function 詳細画面には特定の投稿が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'showTest' ]); $item = Post::find($item->id); $response = $this->get(route('posts.show', $item->id)); $response->assertViewHas('item', $item); } /** @test */ public function 詳細画面で投稿に紐付く投稿者名が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'showTest' ]); $UserName = $user->name; $item = Post::find($item->id); $response = $this->get(route('posts.show', $item->id)); $response->assertSee($UserName); } /** @test */ public function 編集画面のURLにアクセスして画面が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'edittest' ]); $response = $this->get(route('posts.edit', $item->user_id)); $response->assertStatus(200); } /** @test */ public function 編集画面のURLにアクセスして投稿編集画面が表示される() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'edittest' ]); $response = $this->get(route('posts.edit', $item->user_id)); $response->assertViewIs('layouts.edit'); } /** @test */ public function 投稿した内容の更新をする() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'editTest' ]); factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'NotEdit' ]); $data = ['content'=>'投稿を編集しました']; $response = $this->put(route('posts.update', $item->id), $data); $this->assertDatabaseHas('posts', [ 'content' => '投稿を編集しました' ]); } /** @test */ public function データを更新した後は投稿の詳細画面に遷移する() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'editTest' ]); $data = ['content'=>'testPost']; $response = $this->put(route('posts.update', $item->id), $data); $response->assertRedirect(action('PostController@show', $item->id)); } /** @test */ public function 投稿を削除する() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'deletepost' ]); $this->delete(route('posts.destroy', $item->id)); $this->assertSoftDeleted('posts', [ 'user_id' => $user->id, 'content' => 'deletepost' ]); } /** @test */ public function 投稿の削除処理完了後は投稿一覧画面に遷移する() { $user = factory(User::class)->create(); $this->actingAs($user); $item = factory(Post::class)->create([ 'user_id' => $user->id, 'content' => 'deletepost' ]); $response = $this->delete(route('posts.destroy', $item->id)); $response->assertRedirect(action('PostController@index')); } }
正直ここまで細かく書く必要はない気がします。(画面を確認すれば分かるのもあるし)
ただ、今回はあくまでテンプレ集なので、なるべく細かく書いてみました。
実際にテストを書くときは、ここからピックアップしたものを書くので十分かなと思います。(もちろん現場の方針にもよりますが)
RegisterControllerTestのテストケース
次に、ユーザー登録のControllerに対するテストです。
書き方としては、PostControllerTestの時とほとんど同じです。
しかし、この場面では、まだユーザー登録やログイン(認証処理)をしていないため、ユーザー生成や認証(actingAs)のコードは必要ありません。
<?php namespace Tests\Http\Controllers; use Illuminate\Foundation\Testing\DatabaseMigrations; use Tests\TestCase; use App\User; class RegisterControllerTest extends TestCase { use DatabaseMigrations; /** @test */ public function ユーザー登録画面のURLにアクセスしてユーザー登録画面が表示される() { $response = $this->get(route('register')); $response->assertViewIs('auth.register'); } /** @test */ public function ユーザー登録に成功した後は投稿一覧画面が表示される() { // ユーザー登録処理 $response = $this->post(route('register'), [ 'name' => 'testUser', 'email' => 'test@example.com', 'password' => 'registerPass', 'password_confirmation' => 'registerPass' ]); $response->assertRedirect(action('PostController@index')); } /** @test */ public function 名前を入力しないで登録しようとするとエラーメッセージが表示される() { $response = $this->post(route('register'), [ 'name' => '', 'email' => 'test@example.com', 'password' => 'password123' ]); $errorMessage = '名前は必ず指定してください'; $this->get(route('register'))->assertSee($errorMessage); } /** @test */ public function メールアドレスを入力しないで登録しようとするとエラーメッセージが表示される() { $response = $this->post(route('register'), [ 'name' => 'testuser', 'email' => '', 'password' => 'password123' ]); $errorMessage = 'メールアドレスは必ず指定してください'; $this->get(route('register'))->assertSee($errorMessage); } /** @test */ public function パスワードを入力しないで登録しようとするとエラーメッセージが表示される() { $response = $this->post(route('register'), [ 'name' => 'testuser', 'email' => 'test@example.com', 'password' => '' ]); $errorMessage = 'パスワードには正しい形式を指定してください'; $this->get(route('register'))->assertSee($errorMessage); } }
主に、きちんと画面が表示されるかと、エラーメッセージが正しく表示されるかをチェックしました。
LoginControllerTestのテストケース
ユーザーのログインに関するControllerのテストです。
先ほどのRegisterControllerTestと同様、この場面では、まだユーザー登録やログイン(認証処理)をしていないため、ユーザー生成や認証(actingAs)のコードは必要ありません。
その他の書き方は、他のControllerのテストとほとんど同じです。
HTTPリクエストをコントローラに対して投げて、予想通りの結果が返ってくるかを判定しています。
<?php namespace Tests\Http\Controllers; use Illuminate\Foundation\Testing\DatabaseMigrations; use Tests\TestCase; use App\User; class LoginControllerTest extends TestCase { use DatabaseMigrations; /** @test */ public function ログイン画面のURLにアクセスして画面が表示される() { $response = $this->get(route('login')); $response->assertStatus(200); } /** @test */ public function ログイン画面のURLにアクセスしてログイン画面が表示される() { $response = $this->get(route('login')); $response->assertViewIs('auth.login'); } /** @test */ public function 登録しておいたemailアドレスとパスワードでログインできる() { $user = factory(User::class)->create([ 'email' => 'pass@example.com', 'password' => bcrypt('loginPass') ]); // ログイン処理 $response = $this->post(route('login'), [ 'email' => 'pass@example.com', 'password' => 'loginPass' ]); $this->assertAuthenticatedAs($user); } /** @test */ public function ログインに成功した後は投稿一覧画面が表示される() { $user = factory(User::class)->create([ 'email' => 'pass@example.com', 'password' => bcrypt('loginPass') ]); // ログイン処理 $response = $this->post(route('login'), [ 'email' => 'pass@example.com', 'password' => 'loginPass' ]); $response->assertRedirect(action('PostController@index')); } /** @test */ public function 登録したのと違うメールアドレスでログインしようとしてもログインできない() { $user = factory(User::class)->create([ 'email' => 'pass@example.com', 'password' => bcrypt('loginPass') ]); // ログイン処理 $response = $this->post(route('login'), [ 'email' => 'pass@exae.com', 'password' => 'loginPass' ]); $this->assertGuest(); } /** @test */ public function 登録したのと違うパスワードでログインしようとしてもログインできない() { $user = factory(User::class)->create([ 'email' => 'pass@example.com', 'password' => bcrypt('loginpass') ]); // ログイン処理 $response = $this->post(route('login'), [ 'email' => 'pass@example.com', 'password' => 'liginpass' ]); $this->assertGuest(); } /** @test */ public function 異なるアドレスでログインしようとするとエラーメッセージが表示される() { $user = factory(User::class)->create([ 'email' => 'pass@example.com', 'password' => bcrypt('loginPass') ]); $response = $this->post(route('login'), [ 'email' => 'pass@exae.com', 'password' => 'loginPass' ]); $errorMessage = 'メールアドレスまたはパスワードが間違っています'; $this->get(route('login'))->assertSee($errorMessage); } /** @test */ public function 異なるパスワードでログインしようとするとエラーメッセージが表示される() { $user = factory(User::class)->create([ 'email' => 'pass@example.com', 'password' => bcrypt('loginpass') ]); $response = $this->post(route('login'), [ 'email' => 'pass@example.com', 'password' => 'lss' ]); $errorMessage = 'メールアドレスまたはパスワードが間違っています'; $this->get(route('login'))->assertSee($errorMessage); } /** @test */ public function ログアウトすると認証状態が解除される() { $user = factory(User::class)->create(); $this->actingAs($user); // ログアウトリクエスト $response = $this->post(route('logout')); // ユーザーが認証されていない $this->assertGuest(); } /** @test */ public function ログアウトをすると投稿一覧画面を表示しようとする() { $user = factory(User::class)->create(); $this->actingAs($user); // ログアウトリクエスト $response = $this->post(route('logout')); $response->assertRedirect(action('PostController@index')); } /** @test */ public function ログアウト後はログイン画面に遷移する() { $user = factory(User::class)->create(); $this->actingAs($user); // ログアウトリクエスト $this->post(route('logout')); $response = $this->get(action('PostController@index')); $response->assertRedirect(route('login')); } }
ログイン(ログアウト)時に考えられる様々なシチュエーション(ログインできるか、ログインできなかった場合に画面はどうなるかなど)でテストを行いました。
おわりに
今回は以上となります。
(未来の自分も含めて)少しでもLaravelのコントローラテストを書く際の参考になっていれば幸いです。