【Laravel】Duskでのブラウザテスト実行例

Laravel

この記事のゴール

  • Laravel Dusk を使ったブラウザテストの仕方が理解できる。

前提条件

  • Laravel Dusk の実行環境構築が完了している。
  • ブラウザテスト実行対象のWEBアプリ構築が完了している。

まだの場合は、次の記事を参考にしてください。

ここでテストする対象のWEBアプリは、Vue.jsのテュートリアルで作成する

todo を拡張して、Breezeインストール後のDashboard内に実装したものです。

はじめに | Vue.js
Vue.js - The Progressive JavaScript Framework
Tutorial | Vue.js
Vue.js - The Progressive JavaScript Framework

Breezeの代わりにJetstreamでも問題ありません。

テスト対象WEBアプリの機能仕様概要

基本的なCRUD操作を実装しています。追加機能だけをざっくり書きます。

1.新規追加フォーム表示機能:「新規追加」リンク押下で入力フォームがそう入される。

  →「Cancel」ボタン押下でフォームは非表示になり「新規追加」リンクが表示される。

2.DB連携:

  ・TODOデータはDBに保存

  ・項目毎の編集確定時にDB更新

  ※Vue.jsテュートリアルのTODOはメモリ上にデータ保持しているだけです。

3.TODO削除直前の確認ダイアログ表示

  ・「いいえ」を選択した場合はTODOを削除しない。

  ・「はい」を選択した場合は該当TODOを削除する。

テストコード保存先

Dusk用のブラウザテストコードの保存先は

[プロジェクトトップ]/tests/Browser/

のディレクトリ内です。

テストコード例

ざっくりなテストしかしていません。

コードの右側にコメントを付けたので、何をしているのかは大体わかると思います。

<?php

namespace Tests\Browser;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use App\Models\User;
use App\Models\Todo;

class TodoTest extends DuskTestCase
{
    private $user;

    /**
     * 各メソッド実行前の処理
     * 参照: https://docs.phpunit.de/en/10.0/fixtures.html#
     */
    protected function setUp(): void
    {
        parent::setUp();
        // テスト用ダミーユーザーを1件だけ作成
        $this->user = User::factory()->create();
    }

    /**
     * TODO新規追加のテスト
     */
    public function test_can_add_todo(): void
    {
        // 全レコード削除
        Todo::truncate();
        // 事前データを10件作成
        $count = 10;
        $todos = Todo::factory()->count($count)->state(['user_id' => $this->user->id])->create();
        // ブラウザ操作
        $this->browse(function (Browser $browser) {
            // スクリーンショット画像ファイル名のベース
            $sc_basename = 'test_can_see_todo_';
            $browser->loginAs($this->user)  // setUp()で作成したユーザーでログイン
                    ->visitRoute('todo.index')  // TODOページへアクセス
                    ->screenshot($sc_basename . '000')  // スクリーンショット保存
                    ->assertSee(config('site.title'))   // サイトタイトル確認
                    ->assertSee(config('site.copyright'))   // コピーライト確認
                    ->waitFor('#todolist',10)   // 10秒間のレンダリング待ち(待ちすぎ?)
                    ->screenshot($sc_basename . '001')
                    ->click('.addText') // 「新規追加」リンクをクリック
                    ->screenshot($sc_basename . '002')
                    ->type('.newTodoTitle','newTodoTitle')  // 件名入力
                    ->type('.newTodoDescription','newTodoDescription')  // 内容入力
                    ->screenshot($sc_basename . '003')
                    ->press('.addTodoSubmit')   // 追加ボタン押下
                    ->pause(500)    // 500ミリ秒(0.5秒)処理待ち
                    ->with('#todolist', function (Browser $table) { // スコープ絞り込み
                        $table->assertSee('newTodoTitle')   // 新規件名の表示確認
                              ->assertSee('newTodoDescription');    // 新規内容の表示確認
                    })
                    ->screenshot($sc_basename . '004')
            ;
        });
    }

    public function test_can_set_done():void
    {
        Todo::truncate();
        $count = 10;
        $todos = Todo::factory()->count($count)->state(['user_id' => $this->user->id, 'done' => 0])->create();
        $this->browse(function (Browser $browser) {
            $sc_basename = 'test_can_set_done_';
            $browser->loginAs($this->user)
                    ->visitRoute('todo.index')
                    ->pause(500)
                    ->screenshot($sc_basename . '001')
                    ->check('.check-done:first-child')  // 最初のチェックボックスをチェック
                    ->check('.check-done:last-child')   // 最後のチェックボックスをチェック
                    ->screenshot($sc_basename . '002');
            $browser->loginAs($this->user)
                    ->visitRoute('todo.index')
                    ->pause(500)
                    ->screenshot($sc_basename . '003')
                    ->assertChecked('.check-done:first-child')  // チェックボックスのチェック確認
                    ->assertChecked('.check-done:last-child');
        });
    }

    public function test_can_edit_todo():void
    {
        Todo::truncate();
        $count = 10;
        $todos = Todo::factory()->count($count)->state(['user_id' => $this->user->id, 'done' => 0])->create();
        $this->browse(function (Browser $browser) {
            $sc_basename = 'test_can_edit_todo_';
            $browser->loginAs($this->user)
                    ->visitRoute('todo.index')
                    ->pause(500)
                    ->screenshot($sc_basename . '001')
                    ->doubleClick('.todo-title-text:first-child')   // 最初のTODO件名をダブルクリック
                    ->screenshot($sc_basename . '002')
                    //->type('todoTitle', 'duskhogehoge')   // This doesn't work.
                    //->type('.todo-input-title','duskhogehoge')   // This doesn't work.
                    //->value('.todo-input-title',"duskhogehoge")   // This doesn't work.
                    ->value('.todo-input-title','') // 件名入力欄のvalue属性値を''にする
                    ->keys('.todo-input-title','duskhogehoge','{ENTER}')    // 値を入力して[ENTER]押下
                    ->pause(1000)   // 1000ミリ秒(1秒)のDB更新待ち
                    ->screenshot($sc_basename . '003');
            $browser->loginAs($this->user)
                    ->visitRoute('todo.index')
                    ->pause(500)
                    ->screenshot($sc_basename . '004')
                    ->assertSeeIn('.todo-title-text:first-child','duskhogehoge')
                    ->doubleClick('.todo-description-text:first-child')
                    ->screenshot($sc_basename . '005')
                    ->clear('.todo-textarea-description')   // テキストエリア内消去
                    ->type('.todo-textarea-description','duskduskhogehoge')
                    ->screenshot($sc_basename . '006')
                    ->press('.cancel-edit-todo-description')
                    ->screenshot($sc_basename . '007')
                    ->assertDontSeeIn('.todo-description-text:first-child','duskduskhogehoge')  // 表示されてない確認
                    ->doubleClick('.todo-description-text:first-child')
                    ->clear('.todo-textarea-description')
                    ->type('.todo-textarea-description','duskduskhogehoge')
                    ->press('.save-todo-description')
                    ->pause(1000)
                    ->screenshot($sc_basename . '008');
            $browser->loginAs($this->user)
                    ->visitRoute('todo.index')
                    ->pause(500)
                    ->screenshot($sc_basename . '009')
                    ->assertSeeIn('.todo-description-text:first-child','duskduskhogehoge');
        });
    }

    public function test_can_delete_todo():void
    {
        Todo::truncate();
        $count = 10;
        $todos = Todo::factory()->count($count)->state(['user_id' => $this->user->id, 'done' => 0])->create();
        $this->browse(function (Browser $browser) {
            $sc_basename = 'test_can_delete_todo_';
            $browser->loginAs($this->user)
                    ->visitRoute('todo.index')
                    ->screenshot($sc_basename . '001')
                    ->doubleClick('.todo-title-text')
                    ->value('.todo-input-title','')
                    ->keys('.todo-input-title','duskhogehoge','{ENTER}')
                    ->pause(500)
                    ->screenshot($sc_basename . '002')
                    ->press('.remove-todo:first-child')
                    ->waitForDialog(3)  // ダイアログの表示待ち3秒
                    ->assertDialogOpened('「duskhogehoge」の削除を実行しますか?')  // ダイアログ内容確認
                    //->screenshot($sc_basename . '003')    // ここではExceptionスローされる
                    ->dismissDialog()   // ダイアログで「いいえ」を選択
                    ->assertSeeIn('.todo-title-text:first-child','duskhogehoge')
                    ->screenshot($sc_basename . '003')
                    ->press('.remove-todo:first-child')
                    ->waitForDialog(3)
                    ->acceptDialog()    // ダイアログで「はい」を選択
                    ->pause(1000)
                    ->screenshot($sc_basename . '004')
                    ->assertDontSeeIn('.todo-title-text:first-child','duskhogehoge');
        });
    }
}

テスト実行

プロジェクトトップディレクトリ内で、ターミナルのコマンドプロンプト上で、

次のコマンドを実行します。

./vendor/bin/sail dusk

筆者の実行環境が貧弱なので1分掛かってしまいましたが、

無事オールグリーンです。

スクリーンショット

保存先は次のディレクトリ内です。

[プロジェクトトップ]/tests/Browser/screenshots/

ここに保存される画像は、テスト実行毎に一度全削除されます。

スクリーンショットの例をいくつか見てみましょう。

▼TODO一覧表示

画面が幅広ですね。

縦方向のスクロールバーがついているので、

ページ全体のスクリーンショットではないことがわかります。

ページをスクロールさせるメソッドもあるので、

画面最下部に特定の要素を配置して、

scrollIntoView() メソッドなどを使うと良いかもしれません。

▼新規追加入力フォーム

▼内容テキストダブルクリック後の編集フォーム

▼全件削除後の表示

このような感じで、スクリーンショットが保存されます。

コメント

タイトルとURLをコピーしました