【Laravel/Breeze】HTTPテストでCRUD操作をテストする方法

Laravel9.x系。公式ドキュメントにはHTTPテストにおけるpost()とかの説明がほとんど無くて困ったのでメモ。

やりたいこと

顧客を新規登録したら顧客の一覧ページにリダイレクトされて,登録された顧客が index に表示されているかどうかテストしたい。

その他の操作も index に反映されるかどうかテストする。

ここではコードを理解しやすくするためにデータを配列で用意しているが,通常は factory を用いることになる。

storeメソッドのテスト

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;   <--入れ忘れに注意

class StudentTest extends TestCase
{
    use RefreshDatabase;

    protected $seed = true;  //ユーザー認証を突破するためシードしておく

    //テストが成功するデータを用意
    $data = [
        'name' => '田中太郎',
        'birthdate' => '2022-07-04',
        'email' => 'test@gmail.com',
    ];

    /** @test */
    public function 顧客を新規登録したら一覧にリダイレクトされる()
    {
        $response = $this->actingAs(User::find(1))
            ->post(route('customer.store'), $this->data)   //postでデータを送る
            ->assertRedirect(route('customer.index')); //リダイレクト先を確認
    }

    /** @test */
    public function 新規登録した顧客が一覧に反映される()
    {
        $response = $this->actingAs(User::find(1))
            ->post(route('customer.store'), $this->data)
        $response = $this
            ->get(route('customer.index'))     //indexページにアクセスする
            ->assertSee('田中太郎');  //登録したデータが表示されているかどうか確認
    }
}

大抵はユーザー認証が必要なはずなので protected $seed = true; でユーザーをシーダーで登録しておく(シーダーには factory などを用いてユーザーの情報を登録するコードを書いておく)。id が 1 のユーザーでログインした状態を作りたいなら,actingAs(User::find(1)) とする。

次に post()store メソッドにデータを送る。通常なら create メソッドのビューで <input> を使ってデータを入力すると思うので,そこで入力するデータを配列として用意すると良い。コードに書いているように,メソッドの外で書いた変数や配列をメソッド内で使うときには,$this->data のような形で書く。

コントローラー側の store メソッドでは,return redirect()->route('customer.index'); のような感じでリダイレクトするように書かれているはずなので,assertRedirect(route('customer.index')) でリダイレクトが返ってきているかどうかを判定する。

ちなみに post()を実行してもページが実際にリダイレクトされるわけではなく,「このページにリダイレクトしてね」という指示文がレスポンスとして返ってくる。assertRedirect はその指示文に指定したルートが含まれているかどうかを判定する。

データがちゃんと登録され,index に反映されるかを確認するには,get()を用いて,改めて index ページにアクセスすると良い。

バリデーションエラーのテスト

バリデーションで name が空白だとエラーになるように設定されているとする。まずは失敗するテストを書いてみる。

//テストに成功するデータ
$data = [
    'name' => '田中太郎',
    'birthdate' => '2022-07-04',
    'email' => 'test@gmail.com',
];

/** @test */
public function 名前が空白は不可()
{
    $data = $this->data;   //メソッドの外で定義した配列を取り込む
    $data['name'] = '';    //nameを空白にする

    $response = $this->actingAs(User::find(1))
        ->post(route('customer.store'), $data)   //postでデータを送る
        ->assertValid(); //バリデーションが成功しているか確認
}

このテストはバリデーションでエラーが発生するので失敗する。

Response has unexpected validation errors: 
  
  {
      "name": [
          "nameは、必ず指定してください。"
      ]
  }

のような結果が返ってくるはず。

今度は成功するテストを書いてみる。

assertValid()assertInValid(['name' => 'nameは、必ず指定してください。']) に書き換える。

これは assertInValid() とすることもでき,この場合はエラーの内容は考慮されない。

今度はテストが成功するはず。

updateメソッドのテスト

updateメソッドのルートが /customer/{customer} となっていて,id が 1 の customer を編集するテストを書いてみる。storeメソッドのときは post() を用いたが,updateメソッドのときは put() を用いる。

//テストに成功するデータ
$data = [
    'name' => '田中太郎',
    'birthdate' => '2022-07-04',
    'email' => 'test@gmail.com',
];

/** @test */
public function 顧客を編集したら一覧にリダイレクトされる()
{
    $response = $this->actingAs(User::find(1))
        ->post(route('customer.store'), $this->data);   //postで新規登録

    $data = $this->data;
    $data['name'] = '田中次郎';      //nameを変更

    $response = $this->actingAs(User::find(1))
        ->put(route('customer.update', ['customer' => 1]), $data)   //putでデータを送る
        ->assertRedirect(route('customer.index')); //リダイレクト先を確認
}

/** @test */
public function 顧客を編集したら一覧に反映される()
{
    //最後のアサーション以外は↑と同じ
    $response = $this->actingAs(User::find(1))
        ->post(route('customer.store'), $this->data);

    $data = $this->data;
    $data['name'] = '田中次郎';

    $response = $this
        ->put(route('customer.update', ['customer' => 1]), $data);

    $response = $this->actingAs(User::find(1))
        ->get(route('customer.index'))
        ->assertSee('田中次郎');  //登録したデータが表示されているかどうか確認
}

/** @test */
public function 名前が空白は不可()
{
    $response = $this->actingAs(User::find(1))
        ->post(route('customer.store'), $this->data);

    $data = $this->data;
    $data['name'] = '';

    $response = $this
        ->put(route('customer.update', ['customer' => 1]), $data)
        //バリデーションエラーを確認
        ->assertInValid(['name' => 'nameは、必ず指定してください。']);
}

流れとしてはいったん成功するデータを post() で登録しておいて,データを変更したあと put() で更新している。

destroyメソッドのテスト

データをいったん登録したあと,そのデータを delete() で削除している。

use App\Models\Customer;   <--customerのモデルを書いておく

//テストに成功するデータ
$data = [
    'name' => '田中太郎',
    'birthdate' => '2022-07-04',
    'email' => 'test@gmail.com',
];

/** @test */
public function 顧客を削除したら一覧にリダイレクトされる()
{
    $response = $this->actingAs(User::find(1))
        ->post(route('customer.store'), $this->data);   //postで新規登録

    //登録したデータを取得
    $customer = Customer::where('name', '田中太郎')->first();

    $response = $this
        //deleteでデータを削除
        ->delete(route('customer.destroy', ['customer' => $customer->id]))
        ->assertRedirect(route('customer.index')); //リダイレクト先を確認
}

/** @test */
public function 顧客を削除したら一覧に反映される()
{
    //最後のアサーション以外は↑と同じ
    $response = $this->actingAs(User::find(1))
        ->post(route('customer.store'), $this->data);

    $customer = Customer::where('name', '田中太郎')->first();

    $response = $this
        ->delete(route('customer.destroy', ['customer' => $customer->id]))
        //削除したデータが表示されていないことを確認
        ->assertDontSee('田中太郎');
}

factoryを使う

上記をfactoryを用いたテストに書き換えてみる。factoryの作り方は省略。

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;
use App\Models\Customer;

class StudentTest extends TestCase
{
    use RefreshDatabase;
    protected $seed = true;

    /** @test */
    public function 顧客を新規登録したら一覧にリダイレクトされる()
    {
        //factoryでデータを作って配列に変換する
        $data = Customer::factory()->make()->toArray();

        $response = $this->actingAs(User::find(1))
            ->post(route('customer.store'), $data)   //postでデータを送る
            ->assertRedirect(route('customer.index')); //リダイレクト先を確認
    }
}

post()はリクエストを配列として送る。factoryで作られるデータはコレクションなのでtoArray()で配列に変換しておくと良い。

/** @test */
public function 顧客を編集したら一覧にリダイレクトされる()
{
    //factoryでデータを作って配列に変換する
    $data = Customer::factory()->make()->toArray();

    $response = $this->actingAs(User::find(1)); //ログイン状態を作る

    //データを登録
    //$customerには登録したデータのコレクションが格納される
    //updateするときにidが必要になる
    $customer = Auth::user()->customers()->create($data);

    $data['name'] = '田中次郎';      //nameを変更

    $response = $this
        ->put(route('customer.update', ['customer' => $customer->id]), $data)
        ->assertRedirect(route('customer.index')); //リダイレクト先を確認
}

updateメソッドのテスト。user と customer がリレーションしているとする。いったんcustomerを作って,そのデータを編集しupdateするという流れ。

customerを作るときstoreメソッドを使っても良いが,create()を用いた方がスマートに書ける。

/** @test */
public function 顧客を削除したら一覧にリダイレクトされる()
{
    $data = Customer::factory()->make()->toArray();
    $response = $this->actingAs(User::find(1));
    $customer = Auth::user()->customers()->create($data);
    $response = $this
        ->delete(route('customer.destroy', ['customer' => $customer->id]))
        ->assertRedirect(route('customer.index'));
}

destroyメソッドのテストは上のようになる。