はじめに
前回の続きで、今回は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> varbooks=[]; $.ajax({ url:'http://localhost/api/books', success:function(response){ books=response; console.log(books); varbookList='<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 | classBookTestextendsDuskTestCase { publicfunctiontestIndex() { $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 */ publicfunctionwaitUntil($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> varbooks=[]; functionindex(){ $.ajax({ url:'/api/books', success:function(response){ books=response; console.log(books); varbookList='<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(){ constid=$(this).data('id'); $.ajax({ url:'/api/books/'+id+'/check_out', type:'POST', success:function(response){ index(); } }); }); // 返却ボタンをクリック $(document).on('click','.return',function(){ constid=$(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 | classBookTestextendsDuskTestCase { publicfunctiontestCheckOutBook() { $this->browse(function(Browser$browser){ $browser->visit('/books') ->waitUntil('books.length > 0') ->click('.checkout') ->waitForText('返却する') ->assertSee('返却する'); }); } publicfunctiontestReturnBook() { $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 */ publicfunctionwaitForText($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){ returnStr::contains($this->resolver->findOrFail('')->getText(),$text); },$message); } |
先ほどと同じように waitUsing()
メソッドを使って、100msおきに Str::conatins()
関数により評価され、テキストが存在してtrueが返却されるか、タイムアウトでfalseが返却されます。
さいごに
いかがでしたか。Laravel Duskで、シンプルな非同期処理の待機を実装することができます。ぜひ試してみてください。