前回の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が紹介されています。
(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/yyy | Www/Xxx/YyyPage.php |
https://admin.example.jp/aaa/bbb | Admin/Aaa/BbbPage.php |
https://foo.example.com/bar/baz | Foo/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