Laravel

Laravelコントローラテストのテンプレまとめ

Laravelコントローラテストのテンプレまとめ

今回は、LaravelのControllerテストのコードをテンプレとしてまとめておきます。(ほとんど車輪の再発明をしないための自分用メモです)

テストコードの書き方の一例として参考になれば幸いです。

※Laravelはバージョン6系を使用しています

Laravelコントローラテストのテンプレまとめ

今回は以下の3つのControllerについてテストを書いてみました。

  • PostControllerTest
  • RegisterControllerTest
  • LoginControllerTest

 

それぞれ多くのテストケースを書いていますが、基本的な考え方はどれも同じです。

要は、Controllerのアクションに対してRequestを投げて、正しい(予想通りの)Responseが返ってくるかを判定しているだけです。難しく考える必要はありません。

 

それでは、それぞれのControllerについてテストケースの例を掲載します。

PostControllerTestのテストケース

PostControlerTestでは、一般的な投稿のCRUD処理に関するテストを書いています。

何も説明しないのもあれなので、簡単に概要だけ解説しますね。

 

今回、テスト用データはファクトリで生成しました。

 

また、各テストケースごとにDBを初期化するため、DatabaseMigrationsトレイトを設置しています。

最初はRefreshDatabaseトレイトを使っていたのですが、RefreshDatabaseを使った場合、オートインクリメントがリセットされないことに気付いたため、変更しました。

 

具体的なテストの流れは、どのテストケースも似ています。こんな感じです。

  1. ファクトリでテストユーザーを作成
  2. actingAsでユーザーを認証させる
  3. Controllerに対してHTTPリクエストを投げ、Response(結果)を受け取る
  4. それぞれのテストケースに対して適切なアサーションを利用して、Response結果の成否を判定する

 

ちなみに、上記はコントローラテストの場合ですが、より一般的なテストの場合に拡張するとこんな感じになります。

  1. テストを実行するための条件(ユーザ認証、データをインサートなど)を設定
  2. テスト対象を実行(HTTPリクエストを送る、クラスのメソッドを実行するなど)
  3. 実行結果を検証(アサーションを利用)

 

全てのテストの前に書かれてある/** @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のコントローラテストを書く際の参考になっていれば幸いです。

参考資料

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA