PuPHPeteerでE2Eテスト(4) ページオブジェクトモデル

前回のJpostalで郵便番号から住所を自動入力するテストプログラムをページオブジェクトモデルへリファクタします。

PuPHPeteerとは

WebアプリケーションのE2Eテストフレームワークの一つに、Puppeteer(パペッティア)があります。Puppeteerはブラウザを起動し、ブラウザを操作してE2Eテストをします。

PuPHPeteerは、PHPからPuppeteerを使うためのライブラリです。

前回

Jpostalを使った郵便番号から住所を自動入力するページのテストプログラムを作りました。

前回のテストメソッド

前回のコードです。前回の記事を読むか、コードを読み込まないと何をしているのかわからないですね。

public function test_1000001は東京都千代田区千代田(): void { $this->page->goto('https://jpostal-1006.appspot.com/sample_1.html'); $this->page->type('#postcode1', '100'); sleep(2); $this->screenShot(); $el = $this->page->querySelector('#address1'); $value = $el->getProperty('value')->jsonValue(); $this->assertEquals('東京都', $value); $el = $this->page->querySelector('#address2'); $value = $el->getProperty('value')->jsonValue(); $this->assertEquals('千代田区', $value); $this->page->type('#postcode2', '0001'); usleep(100000); $this->screenShot(); $el = $this->page->querySelector('#address3'); $value = $el->getProperty('value')->jsonValue(); $this->assertEquals('千代田', $value); $this->pageType('#postcode2', '0002'); usleep(100000); $this->screenShot(); $el = $this->page->querySelector('#address3'); $value = $el->getProperty('value')->jsonValue(); $this->assertEquals('皇居外苑', $value); }
Code language: PHP (php)

次のように「郵便番号の上3桁に100を入力する」とコメントをつけるのも一つの方法ですが、

// 郵便番号の上3桁に '100'を入力する $this->page->type('#postcode1', '100'); sleep(2); $this->screenShot();
Code language: PHP (php)

メソッドに小分けして、メソッド名を「郵便番号の上3桁に入力する」としたほうが、よりわかりやすいです。

ここは、内部で$this->page->typeメソッドを呼んでいるので「type郵便番号の上3桁」にしました。

$this->type郵便番号の上3桁'100'); sleep(2); $this->screenShot(); ... } private function type郵便番号の上3桁(string $text): void { $this->pageType('#postcode1', $text); }
Code language: PHP (php)

各項目のtypeやvalue取得をメソッドにすると、テストコードは次のようになりました。

<?php namespace Ninton\Test; class Jpostal100v2Test extends BasePuppeteerTestCase { public function test_1000001は東京都千代田区千代田(): void { $this->page->goto('https://jpostal-1006.appspot.com/sample_1.html'); $this->type郵便番号の上3桁('100'); sleep(2); $this->screenShot(); $address1 = $this->get都道府県value(); $this->assertEquals('東京都', $address1); $address2 = $this->get市区町村value(); $this->assertEquals('千代田区', $address2); $this->type郵便番号の下4桁('0001'); usleep(100000); $this->screenShot(); $address3 = $this->get町域value(); $this->assertEquals('千代田', $address3); $this->type郵便番号の下4桁('0002'); usleep(100000); $this->screenShot(); $address3 = $this->get町域value(); $this->assertEquals('皇居外苑', $address3); } private function type郵便番号の上3桁(string $text): void { $this->pageType('#postcode1', $text); } private function type郵便番号の下4桁(string $text): void { $this->pageType('#postcode2', $text); usleep(100000); } private function get都道府県value(): string { $el = $this->page->querySelector('#address1'); $value = $el->getProperty('value')->jsonValue(); return $value; } private function get市区町村value(): string { $el = $this->page->querySelector('#address2'); $value = $el->getProperty('value')->jsonValue(); return $value; } private function get町域value(): string { $el = $this->page->querySelector('#address3'); $value = $el->getProperty('value')->jsonValue(); return $value; } }
Code language: plaintext (plaintext)

だいぶよくなりましたが、この方法には欠点があります。

この方法の欠点は、せっかく小分けした type郵便番号の上3桁()や、get都道府県value()などのメソッドを、他のテストケースクラスから使えないことです。

かといって、BaseJpostalTestCaseクラスを作って、そのクラスにメソッドを移動するのは悪いアイデアです。例えば、前回のGoogle検索の操作は、BaseGoogleTestCaseクラスにメソッドを作るといったことです。

実際のE2Eテストは、複数サイトや複数ページを遷移するようなテストプログラムを作ります。例えば、タブ0でJpostalを表示し、タブ1でGoogleを表示するといった場合です。複数のクラスからextendsできないので、無理やりな親子関係を作るしかありません。

// バッドパターン class BaseJpostalTestCase extends TestCase { protected function type郵便番号の上3桁(string $text) protected function type郵便番号の上4桁(string $text) } class BaseGoogleTestCase extends BaseJpostalTestCase { protected function type検索ワード(string $text) protected function click検索() } class JpostaAndGoogleTest extends BaseGoogleTestCase { // タブ0 でJpostalを表示し // タブ1 でGoogleを表示する }
Code language: PHP (php)

複数サイトや複数ページを操作しようとすると、この作り方は破綻します。

ページオブジェクトモデル

E2Eテストの元祖であるSeleniumのサイトに、Page Object Modelが紹介されています。

ページオブジェクトモデル :: Seleniumドキュメント
Documentation for Selenium

(PuppeteerのPageクラスではなく)表示するページごとにクラスを用意して、typeやclick、getメソッドを実装します。

CSSセレクターがあちこちのファイルに散らばることがありません。

study_puphpeteer ├── composer.json ├── composer.lock ├── node_modules ├── package-lock.json ├── package.json ├── phpunit.sh ├── phpunit.xml ├── tests │   ├── BasePuppeteerTestCase.php │   ├── GoogleSearchTest.php │   ├── Jpostal100Test.php │   ├── Jpostal100v2Test.php │   ├── Jpostal100v3Test.php // ★コレ │   └── Libs │   ├── BasePage.php // ★コレ │   └── Jpostal │   └── Sample1Page.php // ★コレ ├── tmp └── vendor
Code language: plaintext (plaintext)

tests/Jpostal100v3Test.php

Jpostal100v3Test.phpを新規作成します。Jpostal100v2Testクラスとほとんど同じです。

<?php namespace Ninton\Test; use Ninton\Test\Libs\Jpostal\Sample1Page; class Jpostal100v3Test extends BasePuppeteerTestCase { public function test_1000001は東京都千代田区千代田(): void { $sample1Page = Sample1Page::goto($this->page); $sample1Page->type郵便番号の上3桁('100'); sleep(2); $this->screenShot(); $address1 = $sample1Page->get都道府県value(); $this->assertEquals('東京都', $address1); $address2 = $sample1Page->get市区町村value(); $this->assertEquals('千代田区', $address2); $sample1Page->type郵便番号の下4桁('0001'); usleep(100000); $this->screenShot(); $address3 = $sample1Page->get町域value(); $this->assertEquals('千代田', $address3); $sample1Page->type郵便番号の下4桁('0002'); usleep(100000); $this->screenShot(); $address3 = $sample1Page->get町域value(); $this->assertEquals('皇居外苑', $address3); } }
Code language: plaintext (plaintext)

tests/Libs/BasePage.php

BasePage.phpを新規作成します。

BasePageクラスを extendsして、それぞれのページのクラスを作ります。

どのページでも使いたい共通のメソッドをBasePageクラスにまとめます。pageTypeメソッド(クリアしてからテキスト入力する)、waitForPageLoadメソッド(ページロードを待つ)をBasePuppeteerTestCaseクラスからBasePageクラスへコピペします。

スクリーンショットの保存先は、実際に合わせて修正してください。

<?php namespace Ninton\Test\Libs; use Nesk\Puphpeteer\Resources\Page; use Nesk\Rialto\Data\JsFunction; abstract class BasePage { /** * @var */ protected $page; /** * BasePage constructor. * @param Page $page */ public function __construct(Page $page) { $this->page = $page; } public function waitForPageLoad(): void { $this->page->waitForNavigation("{waitUntil: ['load', 'networkidle2']}"); } public function screenShot(): void { $path = __DIR__ . '/../../tmp/' . strftime('%Y-%m-%d-%H-%M-%S.jpg'); $this->page->screenshot([ 'path' => $path, 'fullPage' => true, ]); } /** * @param string $selector */ public function pageTypeClear(string $selector): void { $jsFunc = JsFunction::createWithBody("document.querySelector('${selector}').value = '';"); $this->page->evaluate($jsFunc); usleep(100000); } /** * @param string $selector * @param string $text */ public function pageType(string $selector, string $text): void { $this->pageTypeClear($selector); $this->page->type($selector, $text); } }
Code language: PHP (php)

tests/Libs/Jpostal/Sample1Page.php

Sample1Page.phpは、https://jpostal-1006.appspot.com/sample_1.htmlのページを操作するためのクラスです。

表示するページごとに作成するクラスで、BasePageクラスをextendsします。

ディレクトリ構成は、サイト/Foo/Bar/ページクラス.phpです。複数サイトにまたがったテストをできるように、サイトごとにディレクトリを作ります。その下はURLのパスに合わせて、ディレクトリを作ります。

サイトの例ディレクトリの例
https://www.example.jp/xxx/yyyWww/Xxx/YyyPage.php
https://admin.example.jp/aaa/bbbAdmin/Aaa/BbbPage.php
https://foo.example.com/bar/bazFoo/Bar/BazPage.php

Jpostal100v2Testクラスのtype郵便番号の上3桁()メソッドなどをコピペします。スコープをpublicに置換します。メソッドの内容は同じです。

<?php namespace Ninton\Test\Libs\Jpostal; use Nesk\Puphpeteer\Resources\Page; use Ninton\Test\Libs\BasePage; class Sample1Page extends BasePage { /** * @param Page $page * @return Sample1Page */ public static function goto(Page $page): Sample1Page { $page->goto('https://jpostal-1006.appspot.com/sample_1.html'); return new Sample1Page($page); } public function type郵便番号の上3桁(string $text): void { $this->pageType('#postcode1', $text); } public function type郵便番号の下4桁(string $text): void { $this->pageType('#postcode2', $text); usleep(100000); } public function get都道府県value(): string { $el = $this->page->querySelector('#address1'); $value = $el->getProperty('value')->jsonValue(); return $value; } public function get市区町村value(): string { $el = $this->page->querySelector('#address2'); $value = $el->getProperty('value')->jsonValue(); return $value; } public function get町域value(): string { $el = $this->page->querySelector('#address3'); $value = $el->getProperty('value')->jsonValue(); return $value; } }
Code language: PHP (php)

仮にsample_1.htmlのDOM構造が変わってしまって、CSSセレクターを修正することになったとき、Sample1Page.phpだけを修正すればよくなりました。

$el = $this->page->querySelector('#address3');
$value = $el->getProperty('value')->jsonValue();

は、似たようなコードが多いからメソッドに小分けできそうだよ

そのとおりですね。BasePageクラスに、pageGetValueメソッドを追加して、リファクタしました。

/** * @param string $selector * @return string */ public function pageGetValue(string $selector): string { $el = $this->page->querySelector($selector); $value = $el->getProperty('value')->jsonValue(); return $value; } }
Code language: PHP (php)

Sample1Page.phpは次のようになりました。

public function get都道府県value(): string { return $this->pageGetValue('#address1'); } public function get市区町村value(): string { return $this->pageGetValue('#address2'); } public function get町域value(): string { return $this->pageGetValue('#address3'); }
Code language: PHP (php)

まとめ

Jspotal100v3Test.php
https://github.com/ninton/study_puphpeteer/blob/main/tests/Jpostal100v3Test.php

Sample1Page.php
https://github.com/ninton/study_puphpeteer/blob/main/tests/Libs/Jpostal/Sample1Page.php

BasePage.php
https://github.com/ninton/study_puphpeteer/blob/main/tests/Libs/BasePage.php

つづく

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