はじめに
Laravelでは標準で単体テストを実行できますが、公式パッケージ
Laravel Dusk
を使用すると、ブラウザテスト作成・実行ができるようになります。今回は、CRUDを例に、セットアップと基本的なアクション・アサーションを紹介します。
Laravel Duskのセットアップ
まずは、composerでインストールします。
1 | $ composer require --dev laravel/dusk |
次に、artisanコマンドで必要なファイルを配置します。
1 | $ php artisan dusk |
tests/Browswer
ディレクトリが作成され、ブラウザテストに必要なファイルが配置されます。構造は次のようになっています。
1 2 3 4 5 | . ├── Components // コンポーネント定義 ├── Pages // ページ定義 ├── console // コンソール出力結果 └── screenshots // スクリーンショット出力結果 |
今回は触れませんが、ページやコンポーネントを使用すると、テストをより構造的に実装することができ、同じUIパーツやページをテストする際に、コードを使いまわせるようになります。
また、consoleはブラウザコンソールの内容、screenshotsにはテスト失敗時や、任意のタイミングでスクリーンショットを撮影したときの画像が入ります。
新規登録のテスト
テスト対象となるコード
前提となる機能を実装します。今回は、書籍を登録・更新・削除できるサンプルコードになります。まずは、一覧と新規登録を作成します。
web.php
1 2 3 4 5 6 | <?php use Illuminate\Support\Facades\Route; Route::resource('books', \App\Http\Controllers\BookController::class); |
BookController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | <?php namespace App\Http\Controllers; use App\Http\Requests\BookRequest; use App\Models\Book; use Illuminate\Http\Request; class BookController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $books = Book::all(); return view('books.index', compact('books')); } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { $book = new Book(); return view('books.create', compact('book')); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(BookRequest $request) { $book = new Book(); $book->title = $request->title; $book->author = $request->author; $book->save(); return response()->redirectToRoute('books.index'); } } |
BookRequest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class BookRequest extends FormRequest { /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'title' => 'required', 'author' => 'required' ]; } public function attributes() { return [ 'title' => 'タイトル', 'author' => '著者' ]; } } |
index.blade
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <h1>図書一覧</h1> <a href="{{ route('books.create') }}">新規作成</a> <table> <tr> <th>ID</th> <th>タイトル</th> <th>著者</th> <th colspan="2"></th> </tr> @foreach($books as $book) <tr> <td>{{ $book->id }}</td> <td>{{ $book->title }}</td> <td>{{ $book->author }}</td> </tr> @endforeach </table> |
edit.blade
1 2 3 4 5 6 7 8 | <h1>図書の編集</h1> <form method="post" action="{{ route('books.update', $book) }}"> @include('books.form') @method('put') <br> <button type="submit" id="register">更新する</button> </form> |
form.blade
1 2 3 4 5 6 7 8 9 10 11 12 | @csrf タイトル: <input type="text" name="title" value="{{ old('title', optional($book)->title) }}"> @error('title') <br> {{ $message }} @enderror <br> 著者: <input type="text" name="author" value="{{ old('author', optional($book)->author) }}"> @error('author') <br> {{ $message }} @enderror |
Duskのテストコード
まずは、テスト用のクラスを作成します。
1 | $ php artisan dusk:make BookCreateTest |
すると、
tests/Browser/BookCreateTest.php
が生成されます。ここに、テストコードを書いていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <?php namespace Tests\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; use Laravel\Dusk\Browser; use Tests\DuskTestCase; class BookCreateTest extends DuskTestCase { public function testCreateBookOK() { $this->browse(function (Browser $browser) { $browser->visit('/books') // 一覧画面に遷移 ->clickLink('新規作成') // 一覧画面で新規作成リンクをクリック ->type('title', 'タイトルテスト') // タイトルを入力する ->type('author', '著者テスト') // 著者を入力する ->click('button[type="submit"]') // 送信ボタンをクリック ->assertPathIs('/books') // 一覧画面に遷移を確認 ->assertSee('タイトルテスト') // 「タイトルテスト」というテキストが含まれていること ->assertSee('著者テスト'); // 「著者テスト」というテキストが含まれていること }); } public function testCreateBookValidationError() { $this->browse(function (Browser $browser) { $browser->visit('/books') // 一覧画面に遷移 ->clickLink('新規作成') // 一覧画面で新規作成リンクをクリック ->click('button') // 何も入力せずにクリック ->assertPathIs('/books/create') ->assertSee('タイトルは必ず指定してください。') // バリデーションエラーの文言が表示されていること ->assertSee('著者は必ず指定してください。');// バリデーションエラーの文言が表示されていること }); } } |
PHPUnitを使用しているので、メソッド名のプレフィックスに
test
を付けるか、アノテーションを設定します。
基本的には、
$browser
に続けて、動作とアサーションを記述していきます。最初の例では、新規作成画面に遷移し、必要なパラメータを入力します。
2つめの例では、入力せずに送信し、バリデーションエラーの文言をチェックしています。
ここで使用しているメソッドを紹介します。
-
type(テキストボックスのname, 入力内容文字列)
テキストボックスに入力します。 -
click(セレクタ)
ボタンをクリックします。セレクタにはCSSセレクタが使用できるので、例えばid等でclick('#register')
と指定することもできます。 -
clickLink(テキスト)
テキストリンクをクリックします。 -
assertPathIs(パス)
表示されているページのパスが、指定のものと一致するか検証します。読み込みに時間のかかるページの場合は、waitする必要はありませんでした。一方で、JavaScriptで重い処理の直後にこの関数を実行すると、アサーションに失敗しました。(JavaScriptに絡むものは次回以降紹介したいと思います。) -
assertSee(文字列)
表示されているページに指定の文字列があるか確認します。
テストの実行
テストを実行するには
$ php artisan dusk
コマンドを使用します。実際にテストを動作させると、次のように出力されます。
1 2 3 4 5 6 7 8 | $ php artisan dusk PHPUnit 9.3.11 by Sebastian Bergmann and contributors. ....... 7 / 7 (100%) Time: 00:03.209, Memory: 20.00 MB OK (7 tests, 20 assertions) |
失敗すると、エラー内容が表示されます。また、
tests/Browser/screenshots
配下に、エラー時のスクリーンショットが保存されます。
更新のテスト
更新のテストでは、更新前の値が正しく入っていること、正しく更新できることをテストします。
テスト対象となるコード
新規作成のテストで作ったコードに追記します。
BookController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <?php namespace App\Http\Controllers; use App\Http\Requests\BookRequest; use App\Models\Book; use Illuminate\Http\Request; class BookController extends Controller { ・・・中略・・・ /** * Show the form for editing the specified resource. * * @param \App\Models\Book $book * @return \Illuminate\Http\Response */ public function edit(Book $book) { return view('books.edit', compact('book')); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param \App\Models\Book $book * @return \Illuminate\Http\Response */ public function update(BookRequest $request, Book $book) { $book->title = $request->title; $book->author = $request->author; $book->save(); return response()->redirectToRoute('books.index'); } } |
index.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <h1>図書一覧</h1> <a href="{{ route('books.create') }}">新規作成</a> <table> <tr> <th>ID</th> <th>タイトル</th> <th>著者</th> <th colspan="2"></th> </tr> @foreach($books as $book) <tr> <td>{{ $book->id }}</td> <td>{{ $book->title }}</td> <td>{{ $book->author }}</td> <!-- 追加を追記 --> <td><a href="{{ route('books.edit', $book) }}">詳細</a></td> </tr> @endforeach </table> |
edit.blade.php (新規追加)
1 2 3 4 5 6 7 8 | <h1>図書の編集</h1> <form method="post" action="{{ route('books.update', $book) }}"> @include('books.form') @method('put') <br> <button type="submit">更新する</button> </form> |
Duskのテストコード
新規作成のテストの応用で実装していきます。先ほどの
BookCreateTest.php
に追記します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?php namespace Tests\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; use Laravel\Dusk\Browser; use Tests\DuskTestCase; class BookCreateTest extends DuskTestCase { ・・・中略・・・ public function testEditBook() { $this->browse(function (Browser $browser) { $browser->visit('/books') // 一覧画面に遷移 ->click('detail-link:first-of-type') // 一覧画面で1番始めの詳細リンクをクリック ->assertValue('input[name="title"]', 'タイトルテスト') // テキストボックスの値をチェック ->assertValue('input[name="author"]', '著者テスト'); }); } public function testUpdateBook() { $this->browse(function (Browser $browser) { $browser->visit('/books') // 一覧画面に遷移 ->click('detail-link:first-of-type') // 一覧画面で1番始めの詳細リンクをクリック ->type('title', 'タイトルテスト2') // テキストボックスの値を書き換え ->type('author', '著者テスト2') ->click('button') ->assertPathIs('/books') ->assertSee('タイトルテスト2') // 一覧画面で、書き換え後のテキストになっているか確認する ->assertSee('著者テスト2'); }); } } |
ここで使用しているメソッドを紹介します。
-
assertValue(セレクタ)
セレクタのinput
タグに、値を指定します。click()
と同じように、CSSセレクタで指定します。 -
click('detail-link:first-of-type')
先ほども紹介したメソッドですが、CSSセレクタの使い方が異なるので紹介します。一覧画面では複数の詳細リンクがあります。そこで、CSS擬似クラス:first-of-type
を使用して、1つ目の詳細リンクをクリックするようにしています。
JavaScriptを組み合わせた例
ブラウザテストなので、JavaScriptの動作も検証することができます。削除のテストを例にとって、説明します。
テスト対象となるコード
新規作成のテストで作ったコードに追記します。
BookController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php namespace App\Http\Controllers; use App\Http\Requests\BookRequest; use App\Models\Book; use Illuminate\Http\Request; class BookController extends Controller { ・・・中略・・・ /** * Remove the specified resource from storage. * * @param \App\Models\Book $book * @return \Illuminate\Http\Response */ public function destroy(Book $book) { $book->delete(); return response()->redirectToRoute('books.index'); } } |
index.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <h1>図書一覧</h1> <a href="{{ route('books.create') }}">新規作成</a> <table> <tr> <th>ID</th> <th>タイトル</th> <th>著者</th> <th colspan="2"></th> </tr> @foreach($books as $book) <tr> <td>{{ $book->id }}</td> <td>{{ $book->title }}</td> <td>{{ $book->author }}</td> <td><a href="{{ route('books.edit', $book) }}">詳細</a></td> <!-- ここから追記 --> <td> <form method="post" action="{{ route('books.destroy', $book) }}" onsubmit="return destroy()"> @csrf @method('delete') <button type="submit">削除</button> </form> </td> <!-- ここまで追記 --> </tr> @endforeach </table> <!-- ここから追記 --> <script> function destroy() { if (!confirm('本当に削除しますか?')) { return false; } } </script> <!-- ここまで追記 --> |
Duskのテストコード
このサンプルでは、削除ボタンを押すと「本当に削除しますか?」というダイアログが表示されてから、削除を行います。その動作をLaravel Duskで実装します。
それでは、先ほどの
BookCreateTest.php
に追記します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <?php namespace Tests\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; use Laravel\Dusk\Browser; use Tests\DuskTestCase; class BookCreateTest extends DuskTestCase { ・・・中略・・・ public function testDestroyCancel() { $this->browse(function (Browser $browser) { $browser->visit('/books') // 一覧画面を表示 ->click('button') // 削除ボタンをクリック ->assertDialogOpened('本当に削除しますか?') // ダイアログの文言確認 ->dismissDialog() // キャンセルボタンをクリック ->assertPathIs('/books') // 一覧画面のパス確認 ->assertSee('タイトルテスト2') // 削除されていないことを表示で確認 ->assertSee('著者テスト2'); // 削除されていないことを表示で確認 }); } public function testDestroyBook() { $this->browse(function (Browser $browser) { $browser->visit('/books') // 一覧画面を表示 ->click('button') // 削除ボタンをクリック ->assertDialogOpened('本当に削除しますか?') // ダイアログの文言確認 ->acceptDialog() // 削除ボタンをクリック ->assertPathIs('/books') // 一覧画面のパス確認 ->assertDontSee('タイトルテスト2') // 削除した本のタイトルが表示されていないことを確認 ->assertDontSee('著者テスト2'); // 削除した本の著者が表示されていないことを確認 }); } } |
ここで使用しているメソッドを紹介します。
-
assertDialogOpened(テキスト)
ダイアログ(JSのalert/confirm/prompt)のメッセージが、指定のものになっているか検証します。 -
acceptDialog()
ダイアログのOKボタンを押下します。 -
dismissDialog()
ダイアログを閉じます。(confirm, promptではキャンセルボタン押下と同じになります。) -
assertDontSee(テキスト)
指定したテキストが表示されていないことを検証します。
さいごに
今回のテストコードの全体をここに載せておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | <?php namespace Tests\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; use Laravel\Dusk\Browser; use Tests\DuskTestCase; class BookCreateTest extends DuskTestCase { public function testCreateBookOK() { $this->browse(function (Browser $browser) { $browser->visit('/books/create') // 新規作成画面に遷移 ->type('title', 'タイトルテスト') // タイトルを入力する ->type('author', '著者テスト') // 著者を入力する ->click('button') // 送信ボタンをクリック ->assertPathIs('/books') // 一覧画面に遷移を確認 ->assertSee('タイトルテスト') // 「タイトルテスト」というテキストが含まれていること ->assertSee('著者テスト'); // 「著者テスト」というテキストが含まれていること }); } public function testEditBook() { $this->browse(function (Browser $browser) { $browser->visit('/books') ->clickLink('詳細') ->assertValue('input[name="title"]', 'タイトルテスト') ->assertValue('input[name="author"]', '著者テスト'); }); } public function testUpdateBook() { $this->browse(function (Browser $browser) { $browser->visit('/books') ->clickLink('詳細') ->type('title', 'タイトルテスト2') ->type('author', '著者テスト2') ->click('button') ->assertPathIs('/books') ->assertSee('タイトルテスト2') ->assertSee('著者テスト2'); }); } public function testCreateBookValidationError() { $this->browse(function (Browser $browser) { $browser->visit('/books/create') ->click('button') ->assertPathIs('/books/create') ->assertSee('タイトルは必ず指定してください。') ->assertSee('著者は必ず指定してください。'); }); } public function testDestroyCancel() { $this->browse(function (Browser $browser) { $browser->visit('/books') ->click('button') ->assertDialogOpened('本当に削除しますか?') ->dismissDialog() ->assertPathIs('/books') ->assertSee('タイトルテスト2') ->assertSee('著者テスト2'); }); } public function testDestroyBook() { $this->browse(function (Browser $browser) { $browser->visit('/books') ->click('button') ->assertDialogOpened('本当に削除しますか?') ->acceptDialog() ->assertPathIs('/books') ->assertDontSee('タイトルテスト2') ->assertDontSee('著者テスト2'); }); } } |
今回紹介した以外にも、様々なアクション・アサーションがあります。詳しくは公式ドキュメントを参照してください。
Laravel Dusk: https://laravel.com/docs/master/dusk