今回はページオブジェクトモデルで、amazonで「テスト駆動開発」をカートに入れるテストプログラムを作ります。(ログインはせず、購入手続きまで進みませんので、心配はいりません)
PuPHPeteerとは
WebアプリケーションのE2Eテストフレームワークの一つに、Puppeteer(パペッティア)があります。Puppeteerはブラウザを起動し、ブラウザを操作してE2Eテストをします。
PuPHPeteerは、PHPからPuppeteerを使うためのライブラリです。
前回
前々回のJpostalを使った郵便番号から住所を自動入力するページのテストプログラムをページオブジェクトモデルでリファクタしました。
今回のテスト
今回は、amazon.co.jpで「テスト駆動開発」を検索し、カートに入れて、3080円であることをテストします。
まず、手でブラウザを操作します。ねんのため、ブラウザをシークレットモードで開くか、amazonからログアウトしてください。
テストしたいシナリオです。
- amazon.co.jpトップページを表示します(https://www.amazon.co.jp/)
- 検索ワード欄で「テスト駆動開発」を入力します。
- 検索ボタンをクリックます。
- 検索結果ページで、1番目のサムネイル画像をクリックします。
- 商品詳細ページで、「カートに入れる」ボタンをクリックします。
- カートの小計に「3080」と表示されているはずです。
amazon.co.jpトップを表示します
tests/AmazonTest.phpを新規作成します。
<?php
namespace Ninton\Test;
use Ninton\Test\Libs\Amazon\TopPage;
class AmazonTest extends BasePuppeteerTestCase
{
public function test_テスト駆動開発を購入(): void
{
$topPage = TopPage::goto($this->page);
$this->screenShot();
$this->assertTrue(true);
}
}
Code language: PHP (php)
tests/Libs/Amazon/TopPage.phpを新規作成します。
<?php
namespace Ninton\Test\Libs\Amazon;
use Nesk\Puphpeteer\Resources\Page;
use Ninton\Test\Libs\BasePage;
class TopPage extends BasePage
{
public static function goto(Page $page): TopPage
{
$page->goto('https://www.amazon.co.jp');
return new TopPage($page);
}
}
Code language: PHP (php)
ここでテストを実行します。
$ ./phpunit.sh tests/AmazonTest.php
+ ./vendor/bin/phpunit --configuration=phpunit.xml tests/AmazonTest.php
PHPUnit 8.5.14 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 8.46 seconds, Memory: 6.00 MB
OK (1 test, 1 assertion)
Code language: Bash (bash)
スクリーンショット画像も保存されました。
上部の検索欄で「テスト駆動開発」を入力します。
tests/AmazonTest.phpのテストメソッド内に追加します。
$topPage->type検索ワード('テスト駆動開発');
Code language: plaintext (plaintext)
検索ワード欄のHTMLソースを見ると、id="twotabsearchtextbox" が設定されていました。CSSセレクターは"#twotabsearchtextbox"です。
<input type="text" id="twotabsearchtextbox" value="" name="field-keywords" autocomplete="off" placeholder="" class="nav-input nav-progressive-attribute" dir="auto" tabindex="0" aria-label="検索">
Code language: HTML, XML (xml)
tests/Lib/Amazon/TopPage.phpに、type検索ワード()メソッドを追加します。
public function type検索ワード(string $text): void
{
$this->pageType('#twotabsearchtextbox', $text);
}
Code language: PHP (php)
ここでテストを実行すると、検索ワード欄に「テスト駆動開発」と入力されたのが、一瞬見えました。
検索ボタンをクリックます。
tests/AmazonTest.phpの テストメソッド内に追加します。
$topPage->click検索();
$this->waitForPageLoad();
Code language: plaintext (plaintext)
検索ボタンのHTMLソースを見ると、id="nav-search-submit-button" が設定されていました。CSSセレクターは、"#nav-search-submit-button" です。
<input id="nav-search-submit-button" type="submit" class="nav-input nav-progressive-attribute" value="検索" tabindex="0">
Code language: HTML, XML (xml)
tests/Lib/Amazon/TopPage.phpに、click検索()メソッドを追加します。
public function click検索(): void
{
$this->page->click('#nav-search-submit-button');
}
Code language: PHP (php)
ここでテストを実行すると、検索結果のページが表示されました。
検索結果ページです。1番目のサムネイル画像をクリックします。
tests/AmazonTest.phpに追加します。
use Ninton\Test\Libs\Amazon\SearchResultPage;
...
public function test_テスト駆動開発を購入(): void
{
...
$searchResultPage = new SearchResultPage($this->page);
$searchResultPage->click最初のサムネイル画像();
$this->waitForPageLoad();
Code language: plaintext (plaintext)
検索結果のサムネイル画像まわりのHTMLソースを見ると、DOM idは設定されていません。
まず、サムネイル画像を囲むAタグを探して、右クリック>Copy>Copy SelectorでコピーしたCSSセレクターを試してみます。
tests/Lib/Amazon/SearchResultPage.phpを新規作成します。
<?php
namespace Ninton\Test\Libs\Amazon;
use Ninton\Test\Libs\BasePage;
class SearchResultPage extends BasePage
{
public function click最初のサムネイル画像(): void
{
$selector = 'div.sg-col-4-of-12:nth-child(1) > div:nth-child(1) > span:nth-child(1) > div:nth-child(1) > div:nth-child(1) > span:nth-child(2) > a:nth-child(1)';
$this->page->click($selector);
}
}
Code language: PHP (php)
ここでテストを実行します。
検索結果に「テスト駆動開発」は表示されますが、検索結果の手前にPR領域が表示されて、別の本をクリックしてしまいました。何回か実行してみると、PR領域に表示される本の個数が変化したり、PR領域自体が表示されないこともありました。
最初のバージョンのCSSセレクターは「検索結果の1番目」を特定できないようです。DOM idではないCSSセレクターは、DOM構造やDOM順序の変化に弱いのが難点です。
HTMLソースを調べていると、次のようなDOM構造がわかりました。
<div class="s-search-results">
<!-- 1冊目 -->
<div class="s-result-item">
<a href=""><img src="" /></a>
</div>
<!-- 2冊目 -->
<div class="s-result-item">
<a href=""><img src="" /></a>
</div>
</div>
Code language: HTML, XML (xml)
CSSセレクターとして使えそうな箇所を取り出すと、
.s-search-results
.s-result-item
a
img
Code language: HTML, XML (xml)
そこで、CSSセレクターを次のようにしたところ、1冊目をクリックすることができました。
public function click最初のサムネイル画像(): void
{
$selector = '.s-search-results .s-result-item a img';
$this->page->click($selector);
}
Code language: PHP (php)
ところが、新たな問題です。
サムネイル画像をクリックすると、新しいタブに商品詳細ページが表示されてしまいました。$this->pageは現在のタブを表していて、新しいタブを操作できません。PuPHPeteerから、target="_blank"で開いた新しいタブにアクセスする方法がわかりませんでした。
レビューの★の下の「単行本(ソフトカバー)」をクリックしてみると、現在のタブに商品詳細ページが表示されました。サムネイル画像ではなく、これをクリックすることにします。
レビューの★の下の「単行本(ソフトカバー)」のHTMLソースを見ると、次のDOM構造でした。
<div class="s-search-results">
<!-- 1冊目 -->
<div class="s-result-item">
<a class="a-text-bold" href="">単行本(ソフトカバー)</a>
</div>
<!-- 2冊目 -->
<div class="s-result-item">
<a class="a-text-bold" href="">大型本</a>
</div>
</div>
Code language: HTML, XML (xml)
CSSセレクターとして使えそうな箇所を取り出すと、
.s-search-results
.s-result-item
a.a-text-bold
Code language: HTML, XML (xml)
そこで、CSSセレクターを次のようにしました。
public function click最初のサムネイル画像(): void
{
$selector = '.s-search-results .s-result-item a.a-text-bold';
$this->page->click($selector);
}
Code language: PHP (php)
テストを実行してみると、現在のタブに商品詳細ページを表示できるようになりました。
検索結果に10冊表示されていたとしてさ、このCSSセレクターをクリックしたら、10冊すべてのAタグをクリックしないのかな?
Puppeteerのpage.clickのドキュメントに書いてあったわ
https://pptr.dev/#?product=Puppeteer&version=v8.0.0&show=api-pageclickselector-options
page.click(selector[, opions])
selector
: A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.selector
: クリックする要素を検索するためのセレクター。セレクターを満たす要素が複数ある場合は、最初の要素がクリックされます。
page.clickは見つかった最初の要素をクリックするから、心配ないわね
ところが、別の問題です。
click後のwaitForPageLoad()でタイムアウトしてしまいます。
$searchResultPage->click最初のサムネイル画像();
$this->waitForPageLoad(); // ここでタイムアウトしてしまう...
$this->screenShot();
Code language: plaintext (plaintext)
waitForPageLoad()で待つのはあきらめて、別の方法でページ表示の完了を待つことにします。
waitForPageLoad()を削除します。
$searchResultPage->click最初のサムネイル画像();
// $this->waitForPageLoad();
// $this->screenShot();
Code language: plaintext (plaintext)
商品詳細ページです。「カートに入れる」ボタンをクリックします。
tests/AmazonTest.phpのテストメソッドに次のコードを追加します。
use Ninton\Test\Libs\Amazon\DetailPage;
...
public function test_テスト駆動開発を購入(): void
{
...
$detailPage = new DetailPage($this->page);
$detailPage->waitForカートに入れるボタン();
$this->screenShot();
Code language: plaintext (plaintext)
tests/Libs/Amazon/DetailPage.phpを新規作成します。
click後のwaitForPageLoadでタイムアウトしてしまうので、その代わりに、「カートに入れる」ボタンが表示されたら、ページ表示が完了したと判断することにしました。
<?php
namespace Ninton\Test\Libs\Amazon;
use Ninton\Test\Libs\BasePage;
class DetailPage extends BasePage
{
private const ADD_TO_CART_BUTTON = '#add-to-cart-button';
public function waitForカートに入れるボタン(): void
{
$this->page->waitForSelector(self::ADD_TO_CART_BUTTON);
}
}
Code language: PHP (php)
「カートに入れる」ボタンのHTMLソースを見ると、id="add-to-cart-button"なので、CSSセレクターは"#add-to-cart-button"を設定しました。
<input id="add-to-cart-button" name="submit.add-to-cart" title="カートに入れる" data-hover="左から<b>__dims__</b>を選択して<br>ショッピングカートに追加" class="a-button-input" type="submit" value="カートに入れる" aria-labelledby="submit.add-to-cart-announce">
Code language: HTML, XML (xml)
ここで、テストを実行すると、無事OKでした。
つづけて、tests/AmazonTest.phpのテストメソッドに次のコードを追加します。click後、すぐにブラウザを閉じてしまうので、sleep(5); で待つことにします。
$detailPage->clickカートに入れるボタン();
sleep(5);
Code language: plaintext (plaintext)
tests/Libs/Amazon/DetailPage.phpに「カートに入れる」ボタンをクリックするメソッドを追加します。
public function clickカートに入れるボタン(): void
{
$this->page->click(self::ADD_TO_CART_BUTTON);
}
Code language: PHP (php)
常にwaitForSelectorでページ表示を待てばいいんじゃないの?
ブラウザ表示を見ていると、そのDOM要素が表示されていても、ときどきwaitForSelectorもタイムアウトしてしまうことがあるの
あとね、このページにしかないDOM要素だ!と思っていると、条件によっては表示されないことがあったりするのよ
難しいんだね
sleep、waitForNavigation、watiForSelectorを試したり、組み合わせてみてね
カートの小計に「3080」と表示されているはずです。
tests/AmazonTest.phpのテストメソッドから、さきほどのsleep(5); を削除して、次のコードを追加します。
use Ninton\Test\Libs\Amazon\AddedToCartPage;
...
public function test_テスト駆動開発を購入(): void
{
...
// sleep(5);
$addedToCartPage = new AddedToCartPage($this->page);
$addedToCartPage->waitForレジに進むボタン();
$this->screenShot();
Code language: plaintext (plaintext)
tests/Libs/Amazon/AddedToCart.phpを新規作成します。
「レジに進む」ボタンが表示されたら、ページ表示が完了したと判断することにします。
<?php
namespace Ninton\Test\Libs\Amazon;
use Ninton\Test\Libs\BasePage;
class AddedToCartPage extends BasePage
{
private const CHECKOUT_BUTTON = '#hlb-ptc-btn-native';
public function waitForレジに進むボタン(): void
{
$this->page->waitForSelector(self::CHECKOUT_BUTTON);
}
}
Code language: PHP (php)
「レジに進む」ボタンのHTMLソースを見ると、id="hlb-ptc-btn-native"なので、CSSセレクターは"#hlb-ptc-btn-native"を設定しました。
<a id="hlb-ptc-btn-native" href="https://www.amazon.co.jp/ap/signin?_encoding=UTF8..." class="a-button-text a-text-center" role="button">
レジに進む
</a>
Code language: HTML, XML (xml)
ここで、テストを実行すると、無事OKでした。
最後に、カートの小計に、"3080"が表示されていることを検証します。
tests/AmazonTest.phpのテストメソッドに、次のコードを追加します。
$subTotal = $addedToCartPage->getカートの小計();
$this->assertEquals('3080', $subTotal);
Code language: plaintext (plaintext)
tests/Libs/Amazon/AddedToCartPage.php に次のメソッドを追加します。
取り出した文字列の先頭と末尾のスペースや改行は、ltrim関数、rtrim関数で削除します。
数字とマイナス以外を削除して、「¥3,080」を「3080」にします。
public function getカートの小計(): string
{
$selector = '#hlb-subcart span.hlb-price';
$text = $this->pageGetText($selector);
$text = ltrim(rtrim($text));
$text = preg_replace('/[^-0-9]/', '', $text);
return $text;
}
Code language: plaintext (plaintext)
Libs/BasePage.phpに、次のメソッドを追加します。
pageGetValueメソッドとほとんど同じですが、getProperty('value')ではなく、getProperty('textContent')とします。
public function pageGetText(string $selector): string
{
$el = $this->page->querySelector($selector);
$value = $el->getProperty('textContent')->jsonValue();
return $value;
}
Code language: plaintext (plaintext)
テストを実行すると、無事OKです。約20秒かかりました。
$ ./phpunit.sh tests/AmazonTest.php
+ ./vendor/bin/phpunit --configuration=phpunit.xml tests/AmazonTest.php
PHPUnit 8.5.14 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 20.14 seconds, Memory: 6.00 MB
Code language: Bash (bash)
まとめ
tests/AmazonTest.php
<?php
namespace Ninton\Test;
use Ninton\Test\Libs\Amazon\AddedToCartPage;
use Ninton\Test\Libs\Amazon\DetailPage;
use Ninton\Test\Libs\Amazon\SearchResultPage;
use Ninton\Test\Libs\Amazon\TopPage;
class AmazonTest extends BasePuppeteerTestCase
{
public function test_テスト駆動開発を購入(): void
{
$topPage = TopPage::goto($this->page);
$this->screenShot();
$topPage->type検索ワード('テスト駆動開発');
$topPage->click検索();
$this->waitForPageLoad();
$this->screenShot();
$searchResultPage = new SearchResultPage($this->page);
$searchResultPage->click最初のサムネイル画像();
$detailPage = new DetailPage($this->page);
$detailPage->waitForカートに入れるボタン();
$this->screenShot();
$detailPage->clickカートに入れるボタン();
$addedToCartPage = new AddedToCartPage($this->page);
$addedToCartPage->waitForレジに進むボタン();
$this->screenShot();
$subTotal = $addedToCartPage->getカートの小計();
$this->assertEquals('3080', $subTotal);
}
}
Code language: plaintext (plaintext)
githubへのリンク
- tests/AmazonTest.php
- tests/Libs/BasePage.php
- tests/Libs/Amazon/TopPage.php
- tests/Libs/Amazon/SearchResultPage.php
- tests/Libs/Amazon/DetailPage.php
- tests/Libs/Amazon/AddedToCartPage.php
最初のテストを作るのは手間がかかるね
そうね、ページを表示して、HTMLソースを探して、CSSセレクターを決めるところが時間がかかるわね。