テストファーストで編集画面を作成します。CRUD(Create, Read, Update and Delete)と呼ばれる 4 つの基本機能の Update に該当します。
画面レイアウトと仕様
/tasks/{$taskid}
で下記画面を表示します。
以下の項目にテキストを入力して登録をクリックすることで 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
から流用できるものはコピーします。
以下では追加や修正したものを紹介します。
選択したタスクの概要が表示されること(追加)
編集画面が表示するまでの流れは、下記になります。
- タスク一覧画面の各タスクをクリックする
- リクエストとしてサーバ側に /task/{$id} が送られる
- 送られた id を key にデータベースからデータを取得
- 画面レイアウトに取得したデータを流し込んで HTML を描画。
- クライアントにレスポンスを返す
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
テスト毎にデータベースを初期化したいので、EditTaskTest
にuse 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 という 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');
コントローラ
TaskController
にedit
(編集画面の表示)と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.php
をcreate-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)
完成です。