Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

symfony/pantherによるJavaScriptを含めた自動テスト

サーバーサイドエンジニアの田実です!

YappliのPHPアプリケーションに symfony/panther を使ってJavaScriptの処理を含めた自動テストを導入したので、 導入の背景とインストール方法などを紹介をしたいと思います。

※この記事は PHP Advent Calendar 2022 の16日目の記事です!

背景

Yappliの機能のフロント部分はほとんどがネイティブアプリで作られているのですが、一部WebViewで作られている機能もあります。 また、社内用の管理画面もJavaScriptを利用しています。 これらの機能や画面の変更頻度は高く、JavaScriptによる不具合も発生していました。 自動テストがなかったので、ミドルウェアやライブラリのアップグレードの障壁にもなっていました。

WebViewの機能や社内用の管理画面の保守性を上げていくためには、自動テストが必要不可欠です。 これまで、APIやHTMLのテストに関しては以下の記事にあるように実サーバーをたてたE2Eテストを利用していました。

tech.yappli.io

しかしながら、レスポンスを単純比較するだけのテストではJavaScriptなどのフロント領域の動作をカバーできず、フロントを含めてテストするにはブラウザを使ったテストを行う必要があります。 ブラウザを使った自動テストでは SeleniumPuppeteer などのツールがありますが、今回はPHPアプリケーションに対してテストを行いたく、言語的なコンテキストスイッチをなるべく発生させたくありませんでした。 そこで今回、PHPでテストを書けて手軽に導入できる symfony/panther を導入しました。

インストール方法

まずはChromiumのインストールが必要です。yumだと以下のようにしてインストールできます。

$ yum install -y chromium

このままだと日本語が文字化けするので日本語フォントもインストールします。

$ yum install -y ipa-pgothic-fonts

composerを使って symfony/panther をインストールします。

$ composer require --dev symfony/panther

ChromeDriverを dbrekelmans/bdi 経由でインストールします

$ composer require --dev dbrekelmans/bdi
$ vendor/bin/bdi browser:chromium --browser-path /usr/bin/chromium-browser drivers 

READMEには vendor/bin/bdi detect drivers でインストール可能と書いてありますが、バイナリ名が chromium である必要がある ので chromium-browser のバイナリ名で入っている場合はリネームするか、上記のように直接オプションを指定する形でインストールできます。

PHPUnitの設定

phpunit.xmlに以下のようにExtensionを追加します

    <extensions>
        <extension class="Symfony\Component\Panther\ServerExtension" />
    </extensions>

環境変数の設定

chromeの引数を変更する場合は PANTHER_CHROME_ARGUMENTS をセットします。 弊社で試したときはメモリ不足で落ちてしまったので --disable-dev-shm-usage を設定しています。

例) PANTHER_CHROME_ARGUMENTS: '--disable-dev-shm-usage'

symfony/pantherはデフォルトでPHPのビルトインサーバーを使って、そのサーバーに対してリクエストします。 もともと立ち上げている別のサーバーにリクエストを向けたい場合は PANTHER_EXTERNAL_BASE_URI にベースURLを書けばOKです。

例) PANTHER_EXTERNAL_BASE_URI: 'https://example.com'

PANTHER_NO_SANDBOX に1を指定すると --no-sandbox のオプションが設定されます。 弊社のDocker環境だとこの設定無しでは動かなかったので 1 を指定して動かしています。--no-sandbox は推奨されてなさそうですが、上記の PANTHER_EXTERNAL_BASE_URI のベースURLにしかアクセスしないので一旦 💦

実装方法

以下のように PantherTestCaseTrait をuseして、 self::createPantherClient() を呼び出すとクライアントが取得できるので、これを使ってクロール・テストしていきます。

<?php

use Symfony\Component\Panther\PantherTestCaseTrait;

class E2ETest extends TestCase
{
    use PantherTestCaseTrait;

    public function test_ログイン()
    {

        // クライアントを生成
        $client = self::createPantherClient();

        // ログインページにアクセス
        $client->get('/login');
        $crawler = $client->waitFor('.login', 2);

        // フォームの送信
        $form = $crawler->filter('form')->form([
            'email' => 'hoge@yappli.co.jp',
            'password' => 'hogehoge',
        ]);
        $client->submit($form);
        $crawler = $client->waitFor('#account', 2);

        // 値の検証
        $title = $crawler->filter('#title')->getText();
        $this->assertSame('タイトル名', $title);
    }
}

get() メソッドでURLにアクセスし、 waitFor() で要素が表示されるまで待ち、 submit() でPOSTしたりcrawlerの filter() などを使って要素を取得して検証していく感じになります。

同一クラス内でClientの値がキャッシュされるようなので、CookieなどはtearDownで適宜削除していく必要があります。

<?php

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

        // Cookieを全て削除
        self::createPantherClient()
            ->getWebDriver()
            ->manage()
            ->deleteAllCookies();

        // もし失敗したらスクリーンショットを撮る
        $this->takeScreenshotIfTestFailed();
    }

また、takeScreenshotIfTestFailed() メソッドを使って、テスト失敗時にスクリーンショットを自動で撮ることで、どこでどのように失敗したかを確認することも可能です。

Cookieに値を入れたい場合は以下のコードでいけます。

<?php

$client->getCookieJar()
  ->set(new Cookie('{cookie_name}', '{value}'));

Cookieを明示的に入れるケースはそこまで多くないと思いますが、 以前の記事 で書いた auto_prepend_file によるモックを利用する場合は、HTTPヘッダーやCOOKIEにデータを渡す必要があります。 HTTPヘッダは操作できないようなので、代わりにCookieにセットすることでテスト時のみ挙動を変えるようなことも可能です。

その他TIPS

blurをしたい場合は、bodyの要素をクリックすればOKです。

<?php

$crawler->filter('body')->click();

JS/APIによる反映待ちをしたい場合、反映後に画面のフィードバックがあれば、そのフィードバックに使われる要素をwaitFor系のメソッドで監視して待ちます。

<?php

$client->waitFor('#account', 2);
$client->waitForVisibility('#dialog');
$client->waitForInvisibility('#dialog');

ただし、フィードバックがない場合は、ブラウザ側でそれを測定する手段が無いので sleep() で待つような感じになります。

<?php

sleep(2);

アラートのダイアログの文言を取得して検証したり [OK] ボタンを押下することもできます。

<?php

$alert = $client->getWebDriver()->switchTo()->alert();
$title = $alert->getText();
$this->assertSame('{アラート本文}', $title);
$alert->accept();

ファイルをアップロードしたいときは upload()メソッドを使ってローカルのファイルをアップロードすることができます。

<?php

$form = $crawler->filter('.dialog form')->form();
$form['thumbnail_image']->upload('/path/to/image.png');

画面のデバッグは takeScreenshot() で画面キャプチャを撮ることで画面の確認ができます。

<?php

$client->takeScreenshot('screen.png');

console.log() などによるログを取得したい場合は以下のようにオプションを指定することでログが取得できます。

<?php

$client = self::createPantherClient([], [], [
  'capabilities' => [
    'goog:loggingPrefs' => ['browser' => 'ALL'],
  ],
]);

$logs = $client->getWebDriver()->manage()->getLog('browser');
array(1) {
  [0]=>
  array(4) {
    ["level"]=>
    string(4) "INFO"
    ["message"]=>
    string(92) "http://example.com/test.min.js 0:21 "hogehoge""
    ["source"]=>
    string(11) "console-api"
    ["timestamp"]=>
    int(1669703203949)
  }
}

まとめ

symfony/panther を使った自動テストについて紹介しました!

ヘッドレスブラウザを使った自動テストがPHPUnitで手軽にできて開発体験が良いツールでした。 Yappliではサーバーサイドだけで完結するアプリケーションはほとんど無く、JavaScript自体のテスト設計が難しいこともあり、このように一気通貫にテストを行って品質保証を行うことが重要になっています。

YappliのPHPの自動テストや品質保証に興味を持った方がいましたら、ぜひカジュアル面談しましょう! open.talentio.com