はじめに
前回の続きで、今回はLaravel Duskを使用して、非同期なテストを実装したいと思います。以前調査した際に、重い処理がある場合には、待機する必要があることが分かったので、Laravel Duskで非同期で重い処理を待機する方法を紹介します。
JavaScriptの式で待機する
Ajax通信を行った後の画面を検証したり、操作したりしたい場合は、JavaScriptの式を実行し、結果がtrueになるまで待機することができます。
テスト対象となるコード
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 | <div id="result"></div> <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> <script> var books = []; $.ajax({ url: 'http://localhost/api/books', success: function(response) { books = response; console.log(books); var bookList = '<table>' + ' <tr>' + ' <th>ID</th>' + ' <th>タイトル</th>' + ' <th>著者</th>' + ' <th colspan="2"></th>' + ' </tr>'; books.forEach(book => { bookList += '<tr>' + '<td>' + book.id + '</td>' + '<td>' + book.title + '</td>' + '<td>' + book.author + '</td>' + '<td><a href="/books/' + book.id + '">詳細</a></td>' + '</tr>'; }); bookList += '</table>'; $('#result').html(bookList); } }); </script> |
Ajax通信では次のJSONがレスポンスされます。
1 2 3 4 5 6 7 8 9 10 | [ { "id": 3, "title": "タイトルテスト", "author": "著者テスト", "is_check_out": false, "created_at": "2020-10-23T22:50:47.000000Z", "updated_at": "2020-10-24T01:26:17.000000Z" } ] |
Duskのテストコード
今回は、ページが読み込まれて、書籍の一覧表示をテストします。
1 2 3 4 5 6 7 8 9 10 11 12 | class BookTest extends DuskTestCase { public function testIndex() { $this->browse(function (Browser $browser) { $browser->visit('/books') ->waitUntil('books.length > 0') ->assertSee('タイトルテスト'); }); } } |
waitUntil()
メソッドで、JavaScriptの式を渡し、その結果がtrueになると、後続が実行されます。
waitUntil('books.length > 0', 10)
というように、第二引数を設定すると、10秒でタイムアウトするようになります。
では、この式がどのような仕組みで評価されているか理解するために、
waitUntil()
メソッドのコードを読んでいきたいと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /** * Wait until the given script returns true. * * @param string $script * @param int $seconds * @param string $message * @return $this * * @throws \Facebook\WebDriver\Exception\TimeOutException */ public function waitUntil($script, $seconds = null, $message = null) { if (! Str::startsWith($script, 'return ')) { $script = 'return '.$script; } if (! Str::endsWith($script, ';')) { $script = $script.';'; } return $this->waitUsing($seconds, 100, function () use ($script) { return $this->driver->executeScript($script); }, $message); } |
まず、コードの整形が行われ、式に
return
がない場合は、先頭に着けます。セミコロンも同様に付けます。
その後、
waitUsing()
メソッドを使って、100msごとに、WebDriverにJavaScriptを実行させます。
waitUsing()
メソッドは、クロージャの中で
true
が返却されるか、タイムアウトすると終了します。
ちなみに、タイムアウトまでの秒数が指定されていない場合は、
waitUsing()
メソッドのデフォルト値である5秒でタイムアウトします。
DOM要素の表示を待つ
DOMの表示を待ち、その結果を検証することもできます。
テスト対象となるコード
先ほどのbladeファイルを変更します。
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 | <div id="result"></div> <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> <script> var books = []; function index() { $.ajax({ url: '/api/books', success: function(response) { books = response; console.log(books); var bookList = '<table>' + ' <tr>' + ' <th>ID</th>' + ' <th>タイトル</th>' + ' <th>著者</th>' + ' <th colspan="2"></th>' + ' </tr>'; books.forEach(book => { bookList += '<tr>' + '<td>' + book.id + '</td>' + '<td>' + book.title + '</td>' + '<td>' + book.author + '</td>' + '<td><a href="/books/' + book.id + '">詳細</a></td>'; if (book.is_check_out) { bookList += '<td><button type="button" class="return" data-id="'+book.id+'">返却する</button></td>'; } else { bookList += '<td><button type="button" class="checkout" data-id="'+book.id+'">借りる</button></td>' } bookList += '</td>'; }); bookList += '</table>'; $('#result').html(bookList); } }); } // 借りるボタンをクリック $(document).on('click', '.checkout', function() { const id = $(this).data('id'); $.ajax({ url: '/api/books/' + id + '/check_out', type: 'POST', success: function(response) { index(); } }); }); // 返却ボタンをクリック $(document).on('click', '.return', function() { const id = $(this).data('id'); $.ajax({ url: '/api/books/' + id + '/return', type: 'POST', success: function(response) { index(); } }); }); index(); </script> |
本を借りたり、返却したりすることができるようになりました。この借りる・返却するボタンのテストを実装したいと思います。
Duskテストコードの実装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class BookTest extends DuskTestCase { public function testCheckOutBook() { $this->browse(function (Browser $browser) { $browser->visit('/books') ->waitUntil('books.length > 0') ->click('.checkout') ->waitForText('返却する') ->assertSee('返却する'); }); } public function testReturnBook() { $this->browse(function (Browser $browser) { $browser->visit('/books') ->waitUntil('books.length > 0') ->click('.return') ->waitForText('借りる') ->assertSee('借りる'); }); } |
まず、借りる/返却するボタンが表示されるのを待つため、一覧表示のテストと同じように待機します。
ボタン動作の検証ですが、今回は、ボタンの文言が借りた後は「返却する」、返却した後は「借りる」に変わっていることを検証するため、
waitForText
を使用して待機させています。
waitForText('返却する', 10)
とすると、10秒でタイムアウトになります。
この他にも、
waitFor('.return')
のように、CSSセレクタを使用して待機させることも可能です。
waitForTextのコードも見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /** * Wait for the given text to be visible. * * @param array|string $text * @param int $seconds * @return $this * * @throws \Facebook\WebDriver\Exception\TimeOutException */ public function waitForText($text, $seconds = null) { $text = Arr::wrap($text); $message = $this->formatTimeOutMessage('Waited %s seconds for text', implode("', '", $text)); return $this->waitUsing($seconds, 100, function () use ($text) { return Str::contains($this->resolver->findOrFail('')->getText(), $text); }, $message); } |
先ほどと同じように
waitUsing()
メソッドを使って、100msおきに
Str::conatins()
関数により評価され、テキストが存在してtrueが返却されるか、タイムアウトでfalseが返却されます。
さいごに
いかがでしたか。Laravel Duskで、シンプルな非同期処理の待機を実装することができます。ぜひ試してみてください。