PuPHPeteerでE2Eテスト(2) Google検索

Googleで「PuPHPeteer」を検索するテストプログラムを作ります。

PuPHPeteerとは

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

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

準備

npm init

study_puphpeteerディレクトリを作成して、cdします。

$ mkdir study_puphpeteer
$ cd study_puphpeteerCode language: Bash (bash)

npmで @nesk/puphpeteerをインストールします。

$ npm init -y
$ npm install @nesk/puphpeteerCode language: Bash (bash)

composer init

composer initします。各項目に適当なものを入力して、dependenciesは "n"、dev devendenciesも"n" を入力します。

$ composer init
...
Define your dependencies.
Would you like to define your dependencies (require) interactively [yes]? n
Would you like to define your dev dependencies (require-dev) interactively [yes]? n
Code language: Bash (bash)

開発環境のPHPバージョンを調べて、composer.jsonに設定します。

puphpeteerの最新バージョンは、PHP7.3以上が必要です。開発環境のPHPバージョンが古い場合、それにあわせてpuphpeteerの古いバージョンをインストールする必要があります。

config.platform.php で7.2を指定しておくと、puphpeteerのバージョンのなかからPHP7.2に対応しているバージョンを探してインストールしてくれます。

$ php -v
PHP 7.2.34-18+ubuntu18.04.1+deb.sury.org+1 (cli) (built: Feb 23 2021 15:08:03) ( NTS )
Copyright (c) 1997-2018 The PHP GroupCode language: Bash (bash)

この場合は、7.2 なので、

$ composer config platform.php 7.2Code language: Bash (bash)

composer.jsonの config項目に、PHPバージョンが設定されました。

$ cat composer.json
{
...
    "config": {
        "platform": {
            "php": "7.2"
        }
    }
}

Code language: Bash (bash)

composer.jsonをテキストエディターで編集して、testsディレクトリにnamespaceを設定します。

    "autoload-dev": {
        "psr-4": {
            "Ninton\\Test\\": "tests/"
        }
    }
}
Code language: plaintext (plaintext)

composerパッケージをインストールします。

$ comspoer require nesk/puphpeteer
$ comspoer require phpunit/phpunit
Code language: Bash (bash)

最初のテスト

ここでは、3つのファイルを作成します。必ず合格する単純なテストを書いて、テストが動くことを確認します。

study_puphpeteer
├── composer.json
├── node_modules
├── package.json
├── phpunit.sh  ★コレ
├── phpunit.xml  ★コレ
├── tests
│   └── GoogleSearchTest.php  ★コレ
└── vendor
Code language: plaintext (plaintext)

tests/GoogleSearchTest.php

tests/GoogleSearchTest.php を新規作成します。必ず合格する単純なテストを書きます。

<?php

namespace Ninton\Test;

use PHPUnit\Framework\TestCase;

class GoogleSearchTest extends TestCase
{
    public function testSmoke(): void
    {
        $this->assertTrue(true);
    }
}Code language: PHP (php)

phpunit.xml

phpunit.xmlを新規作成します。

bootstrap属性にautoload.phpの場所を設定します。

また、テストファイルは、tests/下の xxxTest.phpのファイルであることを設定します。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="study_puphpeteer">
            <directory suffix="Test.php">./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Code language: HTML, XML (xml)

phpunit.sh

3つめのファイル、phpunit.shを新規作成します。

$ touch phpunit.sh
$ chmod +x phpunit.shCode language: Bash (bash)

次の内容に編集して、保存します。

#!/bin/bash -ux

./vendor/bin/phpunit \
  --configuration=phpunit.xml \
  $1
Code language: Bash (bash)

テスト実行

ターミナルでテスト実行します。最後にグリーンで「OK (1 test, 1 assertion)」と表示されたら、テスト合格です!

$ ./phpunit.sh tests/GoogleSearchTest.php 
+ ./vendor/bin/phpunit --configuration=phpunit.xml tests/GoogleSearchTest.php
PHPUnit 8.5.14 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 38 ms, Memory: 4.00 MB

OK (1 test, 1 assertion)Code language: Bash (bash)

やっと準備ができたんだね

Googleを表示して「PuPHPeteer」で検索する

まず、実際に、人の手で操作してみます。

  • ブラウザを起動します。
  • https://www.google.co.jp を表示します。
  • 検索ワード欄に "PuPHPeteer" と入力します。
  • 「Google 検索」ボタンをクリックします。
  • 一覧の先頭に「rialto-php/puphpeteer」が表示されています。この位置に「rialto-php/puphpeteer」が表示されていることを検証します。
  • ブラウザを閉じます。

ブラウザを起動する、ブラウザを閉じる

setUpメソッドを書いて、ブラウザを起動します。

ブラウザが起動した時点で、すでにタブが1つ表示されているので、そのタブを取得します。それが $this->page です。

<?php

namespace Ninton\Test;

use Nesk\Puphpeteer\Puppeteer;
use Nesk\Puphpeteer\Resources\Browser;
use Nesk\Puphpeteer\Resources\Page;
use PHPUnit\Framework\TestCase;

class GoogleSearchTest extends TestCase
{
    /**
     * @var Browser
     */
    private $browser;

    /**
     * @var Page
     */
    private $page;

    public function setUp(): void
    {
        parent::setUp();

        $puppeteer = new Puppeteer();
        $this->browser = $puppeteer->launch($this->launchOptions());
        $this->page = ($this->browser->pages())[0];
    }

Code language: PHP (php)

ブラウザの起動オプションです。

'headless' => false にすると、ブラウザを表示します。

    private function launchOptions(): array
    {
        return [
            'defaultViewport' => [
                'width'  => 0,
                'height' => 0,
            ],
            'headless' => false,
            'ignoreHTTPSErrors' => true,
            'slowMo' => 50,

            'args' => [
                '--no-sandbox',
                '--disable-setuid-sandbox',
                "--window-size=1280,1024",
            ],
        ];
    }
Code language: PHP (php)

tearDownメソッドを書いて、ブラウザを終了します。

    public function tearDown(): void
    {
        $this->browser->close();

        parent::tearDown();
    }
Code language: PHP (php)

testSmokeメソッドを残したまま、ここでテスト実行してみると、

    public function testSmoke(): void
    {
        $this->assertTrue(true);
    }
}Code language: PHP (php)

ブラウザが起動して、すぐに閉じるはずです。

ブラウザを操作する

testSmokeメソッドは削除してください。

その代わりに、次のメソッドを追加します。

    public function test_GoogleでPuPHPeteer検索するとrialto_php_puphpeteerがあること(): void
    {
    }Code language: PHP (php)

メソッド名に日本語を使っていいの?

JUnit実践入門」で、テスト用のメソッド名は日本語で書いたほうがいいよ、と説明していて、広く受け入れられているわ。

このメソッドにブラウザ操作を書いていきます。

まず、www.google.co.jp を表示します。

Pageクラスのgotoメソッドを使います。

$this->page->goto('https://www.google.co.jp');Code language: PHP (php)

検索ワード欄に "PuPHPeteer" と入力します。

テキスト入力は、Pageクラスのtypeメソッドを使います。DOM要素の指定には、CSSセレクターを使います。DOM idがあれば一番確実で、'#search-word' のように指定します。

フォームのinputタグは、nameやvalueを指定することができます。

$selector = 'input[name="q"]';
$this->page->type($selector, 'PuPHPeteer');Code language: PHP (php)

「Google 検索」ボタンをクリックします。

クリックは、Pageクラスのclickメソッドを使います。

$selector = 'input[name="btnK"]';
$this->page->click($selector);Code language: PHP (php)

clickメソッドを呼んだ後、検索結果が表示されるまで待たなくてはなりません。「次のページを待つ」のはさまざまな状況があり、Puppeteerを使ったE2Eテストのなかで難しい処理の一つです。いろいろ試した結果、たんに sleep(5); に落ち着くこともあります。

ここでは、onLoadイベント、通信がアイドルになってから500ミリ秒経過、を待つことにします。

$this->page->waitForNavigation("{waitUntil: ['load', 'networkidle2']}");Code language: PHP (php)

検索結果に「rialto-php/puphpeteer」があるかどうか検証します。

PageクラスのquerySelectorメソッドで、DOM要素を取得します。見つからなければ null が返ってきます。

$el->getProperty('textContent')->jsonValue()で、DOM要素のテキストが返ってきます。

$selector = 'div.tF2Cxc:nth-child(2) > div:nth-child(1) > a:nth-child(1) > h3:nth-child(2)';
$el = $this->page->querySelector($selector);
$text = $el->getProperty('textContent')->jsonValue();

$this->assertStringContainsString('rialto-php/puphpeteer', $text);
Code language: PHP (php)

DOM idがなく、INPUTタグでもない場合、HTMLソースを読んで、CSSセレクターを組み立てるのは困難です。Google Chromeのdev toolで調べます。

Google Chromeのdev toolを表示します。
dev toolのElementsタブでDOM要素を探します。
DOM要素の上で右クリック>Copy>Copy Selector
の手順で、そのDOM要素のCSSセレクターを取得できます。

DOM要素の順番や階層構造が変化したら、CSSセレクターも変わるよね?

そうね

さっきはDOM要素をとれてたのに、デバグ表示をするとDOM要素をとれなくなって、しばらく迷ってしまいことがあるわね

ここでテスト実行してみましょう。ブラウザが起動して、自動で文字をタイプして、クリックして、検索結果が表示されるはずです。

筆者の環境では、約5秒かかりました。

$ ./phpunit.sh tests/GoogleSearchTest.php 
+ ./vendor/bin/phpunit --configuration=phpunit.xml tests/GoogleSearchTest.php
PHPUnit 8.5.14 by Sebastian Bergmann and contributors.

..                                                                  1 / 1 (100%)

Time: 4.45 seconds, Memory: 6.00 MB

OK (2 tests, 2 assertions)
Code language: Bash (bash)

自動でブラウザが動いているのを見ると、おー!って感心してしまうよ

スクリーンショット

Puppeteerはスクリーンショットを保存することができます。ヘッドレスで実行したとき、スクリーンショットをとってあると、テスト失敗の原因を調べやすくなります。

study_puphpeteer
├── composer.json
├── node_modules
├── package.json
├── phpunit.sh
├── phpunit.xml
├── tests
│   └── GoogleSearchTest.php
├── tmp  ★コレ
│   ├── 2021-03-13-06-43-19.jpg
│   └── 2021-03-13-06-43-22.jpg
└── vendor
Code language: plaintext (plaintext)

まず、tmpディレクトリを作成します。

$ mkdir tmpCode language: Bash (bash)

次にスクリーンショット用のメソッドを作ります。

Pageクラスのscreenshotメソッドを使います。

キー内容
path画像ファイルの保存パス
fullPagefalse: ブラウザに表示されている範囲のスクリーンショット
true: 表示されていない範囲を含めたスクリーンショット

fullPage = trueは、注意が必要です。

内部的に、下へスクロールしながら、スクリーンショット画像を保存して、つなぎあわせているようです。

問題になりそうなのが、ページ下まで到達すると、クルクルを表示して、次のコンテンツを表示するページの場合です。ものすごく長いスクリーンショット画像になってしまうかもしれません。

別のケースとして、ページ上半分を表示しているだけでは走らないはずの処理が、スクリーンショットを保存すると、その処理が走ってしまうことがあるかもしれません。

class GoogleSearchTest extends TestCase
{

〜省略〜

    private function screenShot(): void
    {
        $path = __DIR__ . '/../tmp/' . strftime('%Y-%m-%d-%H-%M-%S.jpg');

        $this->page->screenshot([
            'path'     => $path,
            'fullPage' => true,
        ]);
    }
}Code language: PHP (php)

さきほどのテストメソッドに、screenShot()をさしこみます。

    public function test_GoogleでPuPHPeteer検索するとrialto_php_puphpeteerがあること(): void
    {
        $this->page->goto('https://www.google.co.jp');
        $this->screenShot();

        $selector = 'input[name="q"]';
        $this->page->type($selector, 'PuPHPeteer');
        
        $selector = 'input[name="btnK"]';
        $this->page->click($selector);
        $this->page->waitForNavigation("{waitUntil: ['load', 'networkidle2']}");
        $this->screenShot();

        $selector = 'div.tF2Cxc:nth-child(2) > div:nth-child(1) > a:nth-child(1) > h3:nth-child(2)';
        $el = $this->page->querySelector($selector);
        $text = $el->getProperty('textContent')->jsonValue();
        $this->assertStringContainsString('rialto-php/puphpeteer', $text);
    }

Code language: PHP (php)

テストを実行すると、2枚、スクリーンショットが保存されました。

リファクタ

ベースクラス BasePuppeteerTestCaseクラス、GoogleSearchTestクラスに分けました。

GoogleSearchTestクラス

study_puphpeteer/tests/GoogleSearchTest.php at main · ninton/study_puphpeteer
study PuPHPeteer. Contribute to ninton/study_puphpeteer development by creating an account on GitHub.

BasePuppeteerTestCaseクラス

study_puphpeteer/tests/BasePuppeteerTestCase.php at main · ninton/study_puphpeteer
study PuPHPeteer. Contribute to ninton/study_puphpeteer development by creating an account on GitHub.

つづく

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