テストファーストで新規登録画面を作成します。CRUD(Create, Read, Update and Delete)と呼ばれる4つの基本機能の Create に該当します。加えて、Laravel での validation 処理と Unicode のコードポイントを学びます。
テストファースト
テストファーストとは、プログラミングを行う前にテストケースを作成することです。
メリットは下記の通りです。
- コーディング前に仕様の考慮漏れ、整合性の不備などを見つけ、品質を向上させることができる
- テストケースに合格することでコーディングの完了が明確に判断できる
- 作業再開示にどこから再開すべきかがすぐにわかる
先に画面レイアウトと仕様を確認し、その後にテストケースを作成していきます。
画面レイアウトと仕様
/task/create で 下記画面を表示します。
以下の項目にテキストを入力して登録をクリックすることで"未着手"の ToDo を登録できます。
title
description
各項目の制限は下記表になります。
項目名 | 必須 | 最大長 | 備考 |
---|---|---|---|
title |
○ | 40 | |
description |
200 |
title
は必須で、40 文字以内、description
は、200 文字以内のテキストを入力できます。
エラーチェック(バリデーション)時のメッセージは下記になります。
- タイトルは必ず指定してください。
- タイトルは、40 文字以下で指定してください。
- 概要は、200 文字以下で指定してください。
イベントは下記の通りとなります。
イベント | URL | 備考 |
---|---|---|
登録ボタン | /task |
post |
キャンセルボタン | / |
get |
登録ボタンをクリックすると、入力内容をサーバ側に送信します。その送信先が、/task
で、送信方法が post です。
キャンセルボタンをクリックすると、トップページに戻ります。
テストケースとテストコード実装
冒頭で記載した通り、プログラミング前にテストケースを作成しテストを行います。 考えられる大まかなテストケースとしては下記になります。
- タスク名が未入力時にエラーが表示されること
- submit クリック時にタスク名が 40 文字を超えたエラーが表示されること
- submit クリック時に概要が 200 文字を超えた場合にエラーが表示されること
- submit で正しく登録できること
- cancel で一覧画面に戻ること
それぞれのテストケースをコーディングしていきますが、まずは、新たにテストケースクラスを作成します。
php artisan make:test TaskSubmitTest
そして、前回作成したテストクラスから、下記をコピーします。
use App\\Task;
use RefreshDatabase;
結果、TaskSubmitTest.php
は下記になります。
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Task;
class TaskSubmitTest extends TestCase
{
use RefreshDatabase;
}
それぞれのテストケースをコーディングしていきます。
タスク名が未入力時にエラーが表示されること
未入力という状態として、以下のようないくつかの場合が想定できます。
- null
- 未入力
- 空白入力
- タブ
空白は、半角空白(U+0020
)、全角空白(U+3000
)に加えて、いくつかのものが存在します。
https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9
ここでは半角空白(U+0020
)、全角空白(
U+3000`)だけをテストケースにします。
そして、データパターンが複数ある場合は、dataProvider
を使うと、テストケースを読みやすく記述できます。
TaskSubmitTest.php
に下記のメソッドを追加します。
これは、テストケースからdataProvider
として呼ばれるデータパターンを定義するメソッドになります。
return の箇所に、連動配列でデータパターンを定義しています。
//テストデータを渡すデータプロバイダ用のファンクション
public function provideTitleTestParams()
{
$whiteTexts = \IntlChar::chr("\u{3000}") . \IntlChar::chr("\u{2007}");
return [
'タイトル必須OK' => ["title", true],
'タイトル必須NULL' => [null,false],
'タイトル必須未入力' => ["", false],
'タイトル必須半角空白' => [" ", false],
'タイトル必須全角空白' => [" ", false],
'タイトル必須空文字' => [ "{$whiteTexts}", false],
];
}
データパターンの形式は下記になります。
データパターン名(任意の名称) => [titleデータ、期待される結果]
全部で6つのデータパターンを定義しています。
このデータパターンを使用するテストケースでは、コメント行にて紐付けを行います。
下記のコメント1行目のように@dataProvider データパターンのメソッド名
の形式で記述します。
テストケースの引数$title
,$expected
は、データパターンの連想配列に対応しています。
/**
* @dataProvider provideTitleTestParams
*
* @param mixed $data
* @param mixed $expected
*/
public function testPutTaskTitle_failed($title, $expected): void
{
$data = [
'status' => 1,
'title' => $title,
'description' => "description",
];
$response = $this->from('/tasks/create')->post('/tasks', $data);
if ($expected) {
$response->assertSessionDoesntHaveErrors('title');
$response->assertRedirect('/');
} else {
$response->assertSessionHasErrors('title');
//画面が指定先にリダイレクトされていることを確認
$response->assertRedirect('/tasks/create');
}
}
テストコードを解説していきます。
$data = [
'status' => 1,
'title' => $title,
'description' => "wゑエ😀𩸽"
];
入力フォームで送るデータを連想配列にまとめています。
$response = $this->from('/tasks/create')->post('/tasks', $data);
新規登録画面にて登録ボタンをクリックしサーバに入力フォームの値を送信しまています。 fromにて新規登録画面に指定をして、POSTにて送信先とデータを指定しています。
処理結果は戻り値$response
として受け取ります。
if ($expected) {
$response->assertSessionDoesntHaveErrors('title');
$response->assertRedirect('/');
} else {
$response->assertSessionHasErrors('title');
//画面が指定先にリダイレクトされていることを確認
$response->assertRedirect('/tasks/create');
}
$expected が True のときは、セッションにTitleに関するエラーメッセージが含まれていないこと。 そして、リダイレクト先が一覧画面であることを確認します。
$expected が True のときは、セッションにTitleに関するエラーメッセージが存在すること。 そして、リダイレクト先が新規登録画面であることを確認します。
(セッションについては、別記事で紹介しています)
では、テストを実行してみましょう。
$ ./vendor/bin/phpunit tests/Feature/TaskSubmitTest.php --testdox
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
Task Submit (Tests\Feature\TaskSubmit)
✘ PutTaskTitle failed with タイトル必須NULL
┐
├ Failed asserting that an array contains 'タイトルは必須です。'.
│
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1059
╵ /var/www/html/todo/tests/Feature/TaskSubmitTest.php:52
┴
✘ PutTaskTitle failed with タイトル必須未入力
┐
├ Failed asserting that an array contains 'タイトルは必須です。'.
│
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1059
╵ /var/www/html/todo/tests/Feature/TaskSubmitTest.php:52
┴
✘ PutTaskTitle failed with タイトル必須半角空白
┐
├ Failed asserting that an array contains 'タイトルは必須です。'.
│
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1059
╵ /var/www/html/todo/tests/Feature/TaskSubmitTest.php:52
┴
✘ PutTaskTitle failed with タイトル必須全角空白
┐
├ Failed asserting that an array contains 'タイトルは必須です。'.
│
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1059
╵ /var/www/html/todo/tests/Feature/TaskSubmitTest.php:52
┴
✘ PutTaskTitle failed with タイトル必須タブ
┐
├ Failed asserting that an array contains 'タイトルは必須です。'.
│
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1059
╵ /var/www/html/todo/tests/Feature/TaskSubmitTest.php:52
┴
Time: 1.48 seconds, Memory: 26.00 MB
すべてのテストケースがエラーとなりました。
submitクリック時に40文字を超えたタスク名の場合エラーが表示されること
これは境界値分析のテストケースになります。 こういう場合は、下記の 3 パターンでのテストケースが考えられます。
- 21 文字(40 文字超過 NG)
- 40 文字(40 文字以内 OK)
- 19 文字(40 文字以内 OK)
また、文字数チェックは、前後の空白文字を trim してからチェックするのが定番です。
- 空白文字+40 文字
- 40 文字+空白文字
- 空白 2 文字+40 文字
- 40 文字+空白 2 文字
- 10 文字+空白文字+10 文字
- さまざまな空白文字+40 文字
よって前後の空白を除いて 40 文字以内ならエラーにならないことを確認します。
UNIODE での空白文字の種類は下記になります。 https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9
//テストデータを渡すデータプロバイダ用のファンクション
public function provideTitleLengthTestParams()
{
// 半角空白 \IntlChar::chr("\u{0020}")
// 全角空白 \IntlChar::chr("\u{3000}")
// 図形間隔 \IntlChar::chr("\u{2007}")
// タブ文字 \IntlChar::chr("\u{0009}")
$whiteChar = \IntlChar::chr("\u{0020}");
$whiteText = \IntlChar::chr("\u{3000}"). \IntlChar::chr("\u{2007}").\IntlChar::chr("\u{0009}");
//半角、UTF16のサロゲート文字、UTF8の4バイト文字、半角カタカナ、絵文字などを含めた文字列
$text = str_repeat("wゑエ😀𩸽", 8);
$halftext = str_repeat("wゑエ😀𩸽", 4);
echo "{$text}{$whiteText}{$whiteText}";
return [
'41文字(40文字超過NG)' => ["{$text}a", false],
'40文字(40文字以内OK)' => ["{$text}", true],
'39文字(40文字以内OK)' => ["{$halftext}123456789", true],
'空白文字+40文字' => ["{$whiteChar}{$text}", true],
'40文字+空白文字' => ["{$text}{$whiteChar}", true],
'空白2文字+40文字' => ["{$whiteChar}{$whiteChar}{$text}", true],
'40文字+空白2文字' => ["{$text}{$whiteChar}{$whiteChar}", true],
'20文字+空白文字+20文字' => ["{$halftext}{$whiteText}{$halftext}", false],
'様々な空白文字+40文字' => ["{$whiteText}{$text}", true],
];
}
/**
* @dataProvider provideTitleLengthTestParams
*
* @param mixed $title
* @param boolean $expected
*/
public function testPutTaskTitle_length($title, $expected): void
{
$data = [
'status' => 1,
'title' => $title,
'description' => "description",
];
//post で 送信先とdata を指定し、レスポンス(処理結果)を取得します。
$response = $this->from('/tasks/create')->post('/tasks', $data);
if ($expected) {
$response->assertSessionDoesntHaveErrors('title');
$response->assertRedirect('/');
} else {
$response->assertSessionHasErrors('title');
$response->assertRedirect('/tasks/create');
}
}
submitクリック時に概要が200文字を超えた場合にエラーが表示されること
概要欄のテストですが、title
と文字数が異なるのと、改行文字が含まれてきます。
それを考慮してテストケースを実装してみてください。
ちなみに、改行コードの Unicode ポイントは、下記になります。
コードポイント、エスケープ文字、名称
- U+000D \r : Carriage return.
- U+000A \n : Line feed.
- U+000B \v : Vertical tab
- U+000C \f : Form feed
- U+0085 : Next line
- U+2028 : Line separator
- U+2029 : Paragraph separator
OS 毎に改行コードが異なります。Linux と Mac では\n
となり、Windows では、\r\n
となります。
submitで正しく登録できること
Laravel では、テーブルにcreated_at
とupdated_at
のフィールドがあると
登録時にサーバ側のマシン日付が設定されます。
テスト時には、登録されたデータと想定したデータが同じであるかを確認します。
そのときに日付がマシン日付だと事前に想定が不可能です。
そこで、Carbon::setTestNow
を使用し、日時を固定します。
使用するには、use にて Carbonライブラリーをインポートします。
use \Carbon\Carbon;
を追加してください。
テストケースの実装は下記になります。
public function testStore(): void
{
//作成日付と更新日付を特定日にするために設定
$testdate = new Carbon('2022-01-01 23:59:59');
Carbon::setTestNow($testdate);
$data = [
'status' => 1,
'title' => "title",
'description' => "wゑエ😀𩸽"
];
//登録前に同じデータがないことを確認する
$this->assertDatabaseMissing('tasks', $data);
$response = $this->post('/tasks', $data);
//更新日付と作成日付が登録されていることを確認するために配列を追加
$expected = $data + array('created_at'=>$testdate,'updated_at'=>$testdate);
//登録が正しくされたことを確認する
$this->assertDatabaseHas('tasks', $expected);
//画面が指定先にリダイレクトされていることを確認
$response->assertRedirect('/');
//テスト日付をリセット
Carbon::setTestNow();
}
cancelで一覧画面に戻ること
画面上の cancel ボタンをクリックした際に、呼び出し元であるタスク一覧画面に戻ることを確認します。
ブラウザテストで行います。
php artisan dusk:make SubmitTaskTest
<?php
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use App\Task;
class SubmitTaskTest extends DuskTestCase
{
use DatabaseMigrations;
/**
* A Dusk test example.
*
* @return void
*/
public function testCancel()
{
$this->browse(function (Browser $browser) {
$browser->visit(route('task.new'))
->clickLink('Cancel')
->assertPathIs('/');
});
}
}
テスト実行
下記のコマンドでテストを実行します。
$ ./vendor/bin/phpunit tests/Feature/TaskSubmitTest.php --testdox
laravel7の場合は、下記でも実行できます。
$ php artisan test tests/Feature/TaskSubmitTest --testdox
ブラウザテストは下記のようにすることでクラス別にテストできます。
$ php artisan dusk tests/Browser/SubmitTaskTest.php
フューチャテスト、ブラウザテストともに すべてのテストケースがエラーとなっていることを確認します。
実装
新規登録画面の実装をしていきます。
一つの画面を作るには、以下の 4 つが必要になってきます。
- ルート
- コントローラ
- ビュー
- モデル
モデルは作成済みですので、残りルート、コントローラー、ビューを実装していきます。
ルート
web.php
にて、ルートを追加します。
コントローラの名称とメソッドが決まれば簡単ですね。
Route::get('/tasks/create', 'TaskController@create')->name('task.new');
Route::post('/tasks', 'TaskController@store')->name('task.submit');
VIEW などから、ルートを名前で呼べるように名付けしておきます。
コントローラ
TaskController
にcreate
メソッドを追加します。
単に新規登録用の VIEW を呼び出すだけのものです。
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('create-task');
}
html(フォーム)
index.blade.php
をコピーしてcreate-task.blade.php
を作ります。
$ cp resources/views/index.blade.php resources/views/create-task.blade.php
create-task.blade.php
の@section('content')
と@endsection
で囲まれた部分を下記のように書き換えます。
<form method="POST" action="{{route('task.submit')}}">
@csrf
<div class="mb-3">
<label class="form-label">task</label>
<input name="title" class="form-control" type="text"
placeholder="Text input" value="">
</div>
<div class="mb-5">
<label class="form-label">description</label>
<input name="description" class="form-control" type="text"
placeholder="Text input" value="">
</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>
DB登録
TaskController
にstore
メソッドを追加します。
フォームの値を DB に登録する処理を記述します。
public function store(Request $request)
{
$task = new Task();
$task->fill($request->all());//注1)
$task->status = 1;
$task->save();
return redirect(route('home'));
}
注1)$request->all()を使用する場合の問題は後のチュートリアルで取り上げます。
validation
TaskController
のstore
メソッドにvalidation
処理を追加します。
public function store(Request $request)
{
$request->validate([
'title' => ['max:40','required'],
'description' => ['max:200']
]);
$task = new Task();
$task->fill($request->all());
$task->status = 1;
$task->save();
return redirect(route('home'));
}
テスト実施
$ ./vendor/bin/phpunit tests/Feature/TaskSubmitTest.php --testdox
実行すると、出力されるレポートの最後の方にエラーとなったものがまとめて表示されます。
Summary of non-successful tests:
Task Submit (Tests\Feature\TaskSubmit)
✘ PutTaskTitle failed with タイトル必須全角空白
┐
├ Session is missing expected key [errors].
├ Failed asserting that false is true.
│
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:974
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1049
╵ /var/www/html/todo/tests/Feature/TaskSubmitTest.php:59
┴
✘ PutTaskTitle failed with タイトル必須空文字
┐
├ Session is missing expected key [errors].
├ Failed asserting that false is true.
│
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:974
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1049
╵ /var/www/html/todo/tests/Feature/TaskSubmitTest.php:59
┴
✘ PutTaskTitle length with 様々な空白文字+40文字
┐
├ Session has unexpected error: title
├ Failed asserting that true is false.
│
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1092
╵ /var/www/html/todo/tests/Feature/TaskSubmitTest.php:117
┴
✘ PutTaskDescription length with 様々な空白文字+200文字
┐
├ Session has unexpected error: description
├ Failed asserting that true is false.
│
╵ /var/www/html/todo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1092
╵ /var/www/html/todo/tests/Feature/TaskSubmitTest.php:177
┴
FAILURES!
Tests: 25, Assertions: 75, Failures: 4.
テストで失敗したケースを見てみると、空白文字のトリムに失敗しているようです。 トリムの実装が抜けていました。
しかし、トリムが実行されているものもあります。
実は、Laravel はミドルウェアのapp/Http/Middleware/TrimStrings.php
にて、トリムが自動で行われます。
ただ、Laravelのトリムでは、除去できないパターンがあるようです。
マルチバイト文字のtrimの問題解決
app/Http/Middleware/TrimStrings.php
では、Illuminate\Foundation\Http\Middleware\TrimStrings
を extend
しています。
下記のソースをみてください
vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php
/**
* Transform the given value.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function transform($key, $value)
{
if (in_array($key, $this->except, true)) {
return $value;
}
return is_string($value) ? trim($value) : $value;
}
文字列ならトリムをするようになっています。
このtrim
、実は以下の文字しか削除してくれません。
- " " (ASCII 32 (0x20)), 通常の空白。
- "\t" (ASCII 9 (0x09)), タブ。
- "\n" (ASCII 10 (0x0A)), リターン。
- "\r" (ASCII 13 (0x0D)), 改行。
- "\0" (ASCII 0 (0x00)), NULバイト
- "\v" (ASCII 11 (0x0B)), 垂直タブ
そこで、app/Http/Middleware/TrimStrings.php
を以下を追加します。
/**
* Transform the given value.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function transform($key, $value)
{
if (in_array($key, $this->except, true)) {
return $value;
}
//return is_string($value) ? trim($value) : $value;
return is_string($value) ? $this->mbTrim($value) : $value;
}
/**
* trim マルチバイト対応
*
* @param string $text
* @return string
*/
private function mbTrim($text)
{
return preg_replace('/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $text);
}
mbTrim
というメソッドを定義して、trim
の代わりにmbTrim
を呼ぶようにします。
mbTrim
は、正規表現で文字列の前後の空白文字を’’(文字なし)に置き換えています。
ポイントは、正規表現の記述の中の \p{C}
と \p{Z}
です。
\p{C}
と \p{Z
} は、Unicode 文字プロパティというものです。
https://www.php.net/manual/ja/regexp.reference.unicode.php
\p{C}
は、コントロール系文字類で、Cc Cf Cn Co Cs というカテゴリに属する文字すべてを指します。
\p{Z}
は、区切り文字、空白文字系の文字類で、Zl Zp Zs というカテゴリに属する文字すべてを指します。
それぞれにどのような種類の文字があるかは下記で簡単に調べることができます。
https://www.compart.com/en/unicode/category
トリムすべき文字を自分で調査して、一文字ずつ指定するのは、とても時間のかかる作業です。 しかし、Unicode 文字プロパティを使用すれば、カテゴリ単位に指定できるため時間がからず漏れもなくなります。
そのほか、正規表現の記号の説明は下記になります。
- 「[]」は、この括弧の中の文字はいずれかという意味です。
- 「|」は、OR です。
- 「\A」は始端を、「\z」は終端を表しています。
- 「/u」は文字コードを UTF-8 として扱うという指定になります。
- 「++」は、
絶対最大量指定子(possessive quantifier)
と呼ばれるもので、1 文字前のものが 1 文字以上続くというものを表しています。 同じ意味である「+」との違いは、バックトラックの有無です。「++」は、バックトラックが行われません。
正規表現の確認は下記が便利です。実行時間も表示してくれます。 https://regex101.com/
テストをします。
$ ./vendor/bin/phpunit tests/Feature/TaskSubmitTest.php --testdox
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
Warning: Invocation with class name is deprecated
Task Submit (Tests\Feature\TaskSubmit)
✔ PutTaskTitle failed with タイトル必須OK
✔ PutTaskTitle failed with タイトル必須NULL
✔ PutTaskTitle failed with タイトル必須未入力
✔ PutTaskTitle failed with タイトル必須半角空白
✔ PutTaskTitle failed with タイトル必須全角空白
✔ PutTaskTitle failed with タイトル必須空文字
✔ PutTaskTitle length with 21文字(40文字超過NG)
✔ PutTaskTitle length with 40文字(40文字以内OK)
✔ PutTaskTitle length with 19文字(40文字以内OK)
✔ PutTaskTitle length with 空白文字+40文字
✔ PutTaskTitle length with 40文字+空白文字
✔ PutTaskTitle length with 空白2文字+40文字
✔ PutTaskTitle length with 40文字+空白2文字
✔ PutTaskTitle length with 10文字+空白文字+10文字
✔ 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文字
✔ Store
Time: 3.51 seconds, Memory: 26.00 MB
OK (25 tests, 85 assertions)
ちなみにテストクラスのtestPutTaskDescription_length
メソッドのみテストを行う場合はfilter
オプションを使います。
下記のコマンドになります。
$ ./vendor/bin/phpunit --filter "testPutTaskDescription_length" tests/Feature/TaskSubmitTest.php
ブラウザテスト
$ php artisan dusk tests/Browser/SubmitTaskTest.php
OK となります。
手動テストで確認
念のため新規登録を手動で確認してみます。
次の確認をします。
- 正常に登録できることを確認する
- 入力内容に誤りがあるときにエラーとなることを確認する
確認によって、以下の問題があるとわかりました。
- エラー時のメッセージが表示されない
- エラー時に入力内容が空になってしまう。
次回への申し送り事項
次回は今回、見つかった問題を解決します。
- エラメッセージの表示
- エラー時の前回入力値の表示