Laravel入門 テストファーストでのToDo編集画面の作成(第14回)

2021-01-01
10 min read

テストファーストで編集画面を作成します。CRUD(Create, Read, Update and Delete)と呼ばれる4つの基本機能の Update に該当します。

画面レイアウトと仕様

/tasks/{$taskid}で下記画面を表示します。

task編集画面

以下の項目にテキストを入力して登録をクリックすることで ToDo を修正できます。

  • title
  • status
  • description

追加する各ボタンの仕様は下記表になります。

イベント URL 備考
登録ボタン /tasks/{$taskid} put
キャンセルボタン / get

登録ボタンをクリックすると、入力内容をサーバ側に送信します。その送信先が、/tasks/{$taskid}で、送信方法が put です。 $taskidで指定されたタスクを更新をします。

キャンセルボタンをクリックした際には、一覧画面を表示するようにサーバー側に要求します。

この画面を表示するには、タスク一覧画面の各タスクをクリックします。

テストケースとテストコード実装

プログラミング前にテストケースを作成しテストを行います。 考えられる大まかなテストケースは下記になります。

  • 選択したタスクの概要が表示されること(追加)
  • タスク名が未入力時にエラーとなること
  • submit クリック時に 40 文字を超えたタスク名の場合にエラーとなること
  • submit クリック時に概要が 200 文字を超えた場合にエラーとなること
  • タスク名が未入力時にエラーが表示されること
  • submit クリック時に 40 文字を超えたタスク名の場合エラーが表示されること
  • submit クリック時に概要が 200 文字を超えた場合にエラーが表示されること
  • submit で正しく修正登録できること(修正)
  • エラー時に前回入力内容が表示されること(ブラウザテスト)
  • cancel で元の画面に戻ること(ブラウザテスト)

それぞれのテストケースをコーディングしていきます。

フューチャーテスト

編集画面用のフューチャーテストのテストケースクラスを作成します。

php artisan make:test TaskEditTest

編集画面のテストケースは、新規登録画面のテストケースと似通っているものが多数あります。 新規登録画面のテストクラスであるTaskSubmitTestから流用できるものはコピーします。

以下では追加や修正したものを紹介します。

選択したタスクの概要が表示されること(追加)

編集画面が表示するまでの流れは、下記になります。

  1. タスク一覧画面の各タスクをクリックする
  2. リクエストとしてサーバ側に /task/{$id} が送られる
  3. 送られた id を key にデータベースからデータを取得
  4. 画面レイアウトに取得したデータを流し込んで HTML を描画。
  5. クライアントにレスポンスを返す

feature テストは、2 から 5 までが対象です。HTTP リクエスト単位でのテストになります。 1 についてはブラウザテスト(Laravel Dusk)でテストすることになります。

public function testShow(): void
{

    $readytask = factory(Task::class)->state('Ready')->create();
    factory(Task::class,1)->state('Doing')->create();
    factory(Task::class,1)->state('Done')->create();
    factory(Task::class,1)->state('notReady')->create();

    $id = $readytask->id;
    $response = $this->get("/tasks/{$id}");
    $response->assertStatus(200);
    $response->assertViewHas('task', $readytask);
}

submitで正しく修正登録できること(修正)

タイムスタンプ系は下記を確認します。

  • 作成日は更新されないこと
  • 更新日は新しい日付になっていること
public function testUpdate(): void
{
       //1件のタスクを作って取得
        $editTask = factory(Task::class)->state('Ready')->create();
        factory(Task::class,1)->state('Doing')->create();
        factory(Task::class,1)->state('Done')->create();
        factory(Task::class,1)->state('notReady')->create();

        //更新データを作成
        $data = [
            'id' => $editTask->id,
            'status' => 2,
            'title' => "update title",
            'description' => "update description"
        ];

        //PUTでリクエストを送信
        $response = $this->from("/tasks/$editTask->id")->put("/tasks/$editTask->id", $data);

        //更新データと同じものがデータベースにあることを確認
        $this->assertDatabaseHas('tasks', $data);

        //更新したタスクを取得(タイムスタンプ系の照合をするため)
        $updatedTask = Task::find($editTask->id);

        //作成日が変更されてないことを確認
        $this->assertEquals(
            $editTask->created_at,
            $updatedTask->created_at,
        );

        //更新日が更新されていることを確認
        $this->assertGreaterThan(
            $editTask->updated_at,
            $updatedTask->updated_at,
        );

        //画面が指定先にリダイレクトされていることを確認
        $response->assertRedirect(route('home'));
}

テストを実施

テストを実施します。

$ ./vendor/bin/phpunit tests/Feature/TaskEditTest.php --testdox

テストはすべてエラーになることを確認します。

ERRORS!
Tests: 26, Assertions: 70, Errors: 26.

上記のようにtests の件数とErrorsの件数が同じになっているはずです。

テストコードに間違いがある場合は修正します。

フューチャーテスト(エラーメッセージ用のテストケース)

エラーメッセージ用のテストケースクラスを作成します。

php artisan make:test TaskEditErrorMessageTest

下記がエラーメッセージのテストケースになります。

  • submit クリック時に 40 文字を超えたタスク名の場合エラーが表示されること
  • submit クリック時に概要が 200 文字を超えた場合にエラーが表示されること

これらは、新規登録画面のテストクラスであるTaskSubmitErrorMessageTestから流用できるのでコピーします。

テストを実施

$ ./vendor/bin/phpunit tests/Feature/TaskEditErrorMessageTest.php --testdox

全件がエラーであること、エラーコードがないかを確認してください。

ブラウザテスト

テストクラスを作成します。

php artisan dusk:make EditTaskTest

テスト毎にデータベースを初期化したいので、EditTaskTestuse DatabaseMigrationsを追加します。 フューチャーテストのときのuse RefreshDatabaseは使用できません。

namespace Tests\Browser;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use App\Task;//追加

class EditTaskTest extends DuskTestCase
{

   use DatabaseMigrations;//追加

}

下記のケースをテストメソッドとして追加していきます。

  • 選択したタスクの概要が表示されること(追加)
  • エラー時に前回入力内容が表示されること(ブラウザテスト)
  • cancel で元の画面に戻ること(ブラウザテスト)

選択したタスクの内容が表示されること(追加)

始めにテストのためのデータを追加します。 次に、ヘッドレスブラウザで、タスク一覧画面から該当のものをクリックします。 最後に、編集画面に飛びことを確認します。

public function testTransition()
{


    $editTask = factory(Task::class)->state('Ready')->create();
    factory(Task::class, 1)->state('Doing')->create();
    factory(Task::class, 1)->state('Done')->create();
    factory(Task::class, 1)->state('notReady')->create();


    $this->browse(function ($browser) use ($editTask) {
        $browser->visit(route('home'))
                ->click('@edit-' . $editTask->id)
                ->assertPathIs('/tasks/1');
    });
}

click('@edit-' . $editTask->id)というのは、複数並ぶタスクからdusk="edit-(タスクID)"が付与されているタスクを特定してクリックするという意味です。 実装時に、タスクのアンカータグの属性にdusk="edit-(タスクID)"を追加するようにします。

エラー時に前回入力内容が表示されること(ブラウザテスト)

テストデータを追加した後に以下の流れを辿るようにします。

  • 一覧画面から特定のタスクの編集ボタンをクリック
  • 編集画面にて入力フォームに入力(タイトルが文字数オーバー)
  • 登録ボタンをクリック

上記の流れの結果、入力フォームに入力したものが再表示されることを確認します。

public function testOldValueDisp()
{

    $editTask = factory(Task::class)->state('Ready')->create();
    factory(Task::class, 1)->state('Doing')->create();
    factory(Task::class, 1)->state('Done')->create();
    factory(Task::class, 1)->state('notReady')->create();

    $this->browse(function ($browser) use ($editTask) {
        $browser->visit(route('home'))
                ->click('@edit-' . $editTask->id)
                //フォームの入力
                ->type('title', 'タイトル1タイトル1タイトル1タイトル1タイトル1タイトル1タイトル1タイトル12')
                ->type('description', '本文1')
                ->select('status', 4)
                ->press('Submit')
                //エラー後の再表示の確認
                ->assertPathIs('/tasks/' . $editTask->id)
                ->assertInputValue('title', 'タイトル1タイトル1タイトル1タイトル1タイトル1タイトル1タイトル1タイトル12')
                ->assertSelected('status', 4)
                ->assertInputValue('description', '本文1');
    });
}

cancelで元の画面に戻ること(ブラウザテスト)

テストデータを追加した後に以下の流れを辿るようにします。

  • 一覧画面から特定のタスクの編集ボタンをクリック
  • キャンセルボタンをクリック

結果、一覧画面が再表示されることを確認します。

public function testCancel()
{


  $editTask = factory(Task::class)->state('Ready')->create();
  factory(Task::class, 1)->state('Doing')->create();
  factory(Task::class, 1)->state('Done')->create();
  factory(Task::class, 1)->state('notReady')->create();

   $this->browse(function ($browser) use ($editTask) {
      $browser->visit(route('task.edit', ['id' => $editTask->id]))
              ->clickLink('Cancel')
              ->assertPathIs('/');
   });
}

テストを実施

テストを実施します。

php artisan dusk tests/Browser/EditTaskTest.php

失敗します。

補足 : Laravel TestToolsの問題

ブラウザを操作する動作をいちいちコーディングしていくのが面倒ということで、 Laravel TestTools という chorme の拡張ツールを導入することがあります。

https://chrome.google.com/webstore/detail/laravel-testtools/ddieaepnbjhgcbddafciempnibnfnakl

しかし、これを利用すると、クリック部分が下記のコードとなり意図が異なってきます。

$this->visit('/tasks/2');

これは、クリックをエミュレートせずに編集画面を呼び出してしまっています。 一方、上記のテストコードでは下記になります。

->click('@edit-' . $editTask->id)

ブラウザテストの意味がなくなるので注意してください。

実装

ルート

Web.php に、ルートを追加します。 一覧画面からタスクをクリックして編集画面を表示するルートと編集画面で SUBMIT したときのルートを指定します。

Route::get('/tasks/{id}', 'TaskController@edit')->where('id', '[0-9]+')->name('task.edit');
Route::put('/tasks/{id}', 'TaskController@update')->where('id', '[0-9]+')->name('task.update');

コントローラ

TaskControlleredit(編集画面の表示)とupdate(タスク更新)の 2 つのメソッドを追加します。


  /**
   * Display the specified resource.
   *
   * @param \App\Task $task
   * @return \Illuminate\Http\Response
   */
  public function edit(int $id)
  {
      $task = Task::find($id);
      return view('edit-task', compact('task'));
  }

  /**
   * Update the specified resource in storage.
   *
   * @param \Illuminate\Http\Request $request
   * @return \Illuminate\Http\Response
   */
  public function update(int $id,Request $request)
  {

      $request->validate([
          'title' => ['max:40','required'],
          'description' => ['max:200']
      ]);

      $task = Task::find($id);
      $task->fill($request->all());
      $task->save();
      return redirect(route('home'));
  }

saveメソッドは、新規登録のときも使用しました。 新規作成のときは、先にモデルを生成してからsaveメソッドを呼びました。 一方、更新の場合は、先にモデルをデータベースから取得し、saveメソッドを呼び出します。 また、モデルのフィールドが更新前と後で同じ場合は、更新処理はされません。

ビュー

画面表示の Blade を作っていきます。

edit-task.blade.php

edit-task.blade.phpcreate-task.blade.phpをコピーして作ります。

$ cp resources/views/create-task.blade.php resources/views/edit-task.blade.php

edit-task.blade.php@section('content')@endsectionで囲まれた部分を下記のように書き換えます。


<form method="post" action="{{route('task.update', ['id' => $task->id])}}">
    @method('PUT')
    @csrf

    <!-- タイトル -->
    <div class="mb-3">
        <label class="form-label">title</label>
        <input name="title" class="form-control" type="text"
            placeholder="Text input" value="{{ old('title') ?? $task->title }}">
        @error('title')
        <div class="alert alert-danger">{{ $message }}</div>
        @enderror
    </div>

    <!-- スタータス選択肢 -->
    <div class="mb-3">
        <label class="form-label">status</label>
        <select name="status" class="form-select">
          <option value="1" @if( (old('status') ?? $task->status ) == 1 ) selected @endif>未着手</option>
          <option value="2" @if( (old('status') ?? $task->status ) == 2 ) selected @endif>着手中</option>
          <option value="3" @if( (old('status') ?? $task->status ) == 3 ) selected @endif>完了</option>
          <option value="4" @if( (old('status') ?? $task->status ) == 4 ) selected @endif>延期</option>
        </select>
    </div>


    <!-- 概要 -->
    <div class="mb-5">
        <label class="form-label">description</label>
        <textarea name="description" class="form-control" type="text"
            placeholder="Text input">{{ old('description') ?? $task->description  }}</textarea>
        @error('description')
        <div class="alert alert-danger">{{ $message }}</div>
        @enderror
    </div>

    <!-- ボタン類 -->
    <div class="d-flex justify-content-end">
        <button class="btn btn-primary me-2">Submit</button>
        <a class="btn btn-secondary" href="{{route('home') }}">Cancel</a>
    </div>
</form>

form の method は、get と post しか指定できません。 PUT の場合は、form の methodにpost を指定し@method('PUT')を記載します。

そうすることで、hidden 属性で _method=put として送信されます。 Laravel は、_methodをみて、判断します。

form タグの action は、submit 時の飛び先を指定します。ここでは、/task/{id}になります。

ルートに名前をつけているので、route('task.update', ['id' => $task->id])と指定します。 しかし、更新対象のidが見えてしまうのが気になるところです。改善予定として控えておきます。

スタータス選択肢の部分は、$task->statusの値によって、表示する選択肢を決めています。 @if( (old('status') ?? $task->status ) == 1 ) selected @endifが該当のコードです。

( old('status') ?? $task->status ) の部分は、エラー後の再表示のことを考慮して、null 合体演算子でエラー前に選択した選択肢を表示できるようにしています。

index.blade.php

index.blade.phpの下記の行を修正します。

{{ $task->status_name }}-{{ $task->title }}

各リストに編集画面へ飛ぶためのアンカータグを追加します。

また追加するアンカータグには、dusk 属性を追加します。

ブラウザテスト時にヘッドレスブラウザが、どのタスクをクリックするかを識別するためのものになります。

下記になります。

<a href="{{ route('task.edit',['id' => $task->id]) }}" dusk="{{ 'edit-' . $task->id }}" >{{ $task->status_name }}-{{ $task->title }}</a>

テスト

フューチャーテスト

テストを実行して、下記のようにすべて OK になるまでデバッグします。

$ ./vendor/bin/phpunit tests/Feature/TaskEditTest.php --testdox
PHPUnit 9.5.1 by Sebastian Bergmann and contributors.

Task Edit (Tests\Feature\TaskEdit)
 ✔ PutTaskTitle failed with タイトル必須OK
 ✔ PutTaskTitle failed with タイトル必須NULL
 ✔ PutTaskTitle failed with タイトル必須未入力
 ✔ PutTaskTitle failed with タイトル必須半角空白
 ✔ PutTaskTitle failed with タイトル必須全角空白
 ✔ PutTaskTitle failed with タイトル必須空文字
 ✔ PutTaskTitle length with 41文字(40文字超過NG)
 ✔ PutTaskTitle length with 40文字(40文字以内OK)
 ✔ PutTaskTitle length with 39文字(40文字以内OK)
 ✔ PutTaskTitle length with 空白文字+40文字
 ✔ PutTaskTitle length with 40文字+空白文字
 ✔ PutTaskTitle length with 空白2文字+40文字
 ✔ PutTaskTitle length with 40文字+空白2文字
 ✔ PutTaskTitle length with 20文字+空白文字+20文字
 ✔ PutTaskTitle length with 様々な空白文字+40文字
 ✔ PutTaskDescription length with 201文字(200文字超過NG)
 ✔ PutTaskDescription length with 200文字(200文字以内OK)
 ✔ PutTaskDescription length with 199文字(200文字以内OK)
 ✔ PutTaskDescription length with 空白文字+200文字
 ✔ PutTaskDescription length with 200文字+空白文字
 ✔ PutTaskDescription length with 空白2文字+200文字
 ✔ PutTaskDescription length with 200文字+空白2文字
 ✔ PutTaskDescription length with 100文字+空白文字+100文字
 ✔ PutTaskDescription length with 様々な空白文字+200文字
 ✔ Show
 ✔ Update

Time: 00:02.002, Memory: 38.00 MB

OK (26 tests, 70 assertions)
$ ./vendor/bin/phpunit tests/Feature/TaskEditErrorMessageTest.php --testdox
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

Task Edit Error Message (Tests\Feature\TaskEditErrorMessage)
 ✔ Title long error
 ✔ Title no text error
 ✔ Description long error

Time: 00:00.435, Memory: 24.00 MB

OK (3 tests, 12 assertions)

ブラウザテスト

テストを実施します。

$ php artisan dusk tests/Browser/EditTaskTest.php
PHPUnit 9.5.1 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 00:01.831, Memory: 34.01 MB

OK (3 tests, 6 assertions)

完成です。