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が紹介されています。

/ja/documentation/test_practices/encouraged/page_object_models/

(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をコピーしました