Googleで「PuPHPeteer」を検索するテストプログラムを作ります。
PuPHPeteerとは
WebアプリケーションのE2Eテストフレームワークの一つに、Puppeteer(パペッティア)があります。Puppeteerはブラウザを起動し、ブラウザを操作してE2Eテストをします。
PuPHPeteerは、PHPからPuppeteerを使うためのライブラリです。
準備
npm init
study_puphpeteerディレクトリを作成して、cdします。
$ mkdir study_puphpeteer
$ cd study_puphpeteer
Code language: Bash (bash)
npmで @nesk/puphpeteerをインストールします。
$ npm init -y
$ npm install @nesk/puphpeteer
Code 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 Group
Code language: Bash (bash)
この場合は、7.2 なので、
$ composer config platform.php 7.2
Code 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.sh
Code 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 tmp
Code language: Bash (bash)
次にスクリーンショット用のメソッドを作ります。
Pageクラスのscreenshotメソッドを使います。
キー | 内容 |
---|---|
path | 画像ファイルの保存パス |
fullPage | false: ブラウザに表示されている範囲のスクリーンショット 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クラス
BasePuppeteerTestCaseクラス