PHP + phpunit + php-webdriver + docker-selenium でブラウザテスト　②テスト実行、エラー対策

## 目次
[PHP + phpunit + php-webdriver + docker-selenium でブラウザテスト　①環境構築](https://qiita.com/haruna-nagayoshi/items/3ab58005a6ab74309a8f)
PHP + phpunit + php-webdriver + docker-selenium でブラウザテスト　②テスト実行、エラー対策

①の続きです。

## サンプルテストを書く
本格的に書く前に、テスト実行環境が完成したかどうか確認します。

testsディレクトリ配下に `SampleTest.php` というファイルを作成します。(ファイル名はなんでもOK)

```php
<?php

use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use PHPUnit\Framework\TestCase;

class SampleTest extends TestCase
{
    /**
     * @group google
     */
    public function testGoogle()
    {
    	// Seleniumサーバを指定
    	// Dockerコンテナ内からlocalhostにアクセスするためhost.docker.internalを使う
        $seleniumServerHost = 'http://host.docker.internal:4444/wd/hub';

		// Chromeドライバの設定項目を指定
        $options = new ChromeOptions();
        $options->addArguments([
            '--headless',　    // ← ヘッドレスモードでChromeを起動
            '--disable-gpu',　
            '--no-sandbox',　 // ← Windows環境でのみ必要らしい。消しても動いたのでDocker環境なら不要かも。
            ]);

		// Chromeドライバに先ほどの設定を反映する
        $desiredCapabilities = DesiredCapabilities::chrome()
            ->setCapability(ChromeOptions::CAPABILITY, $options);
        // Chromeドライバを生成
        $driver = RemoteWebDriver::create($seleniumServerHost, $desiredCapabilities);

		// 指定したURLにアクセス
        $driver->get('https://www.google.com');

		//getTitle()メソッドでそのサイトのタイトルを取得、getCurrentURL()メソッドでURLを取得
        echo PHP_EOL, $driver->getTitle(), PHP_EOL;
        echo $driver->getCurrentURL();
        $this->assertSame('Google', $driver->getTitle());

        // ブラウザのウィンドウを閉じ、セッションも終了する。
        // close()メソッドはウィンドウを閉じるだけでセッションが終了されないので注意。
        $driver->quit();
    }
}

```

**Chromeドライバのオプション**
ヘッドレスモードだと画面表示せずテストが実行されるためテストにかかる時間が減るらしい。が、このオプションをつけても後述するVNCサーバで動作を目視できるので効いていないかもしれない・・・。
古い情報しか見つけられなかったが、[ヘッドレス Chrome ことはじめ](https://developers.google.com/web/updates/2017/04/headless-chrome)によると、ヘッドレスモードで起動する場合は `--disable-gpu` が必須。


**ほか**
そのほかテストの書き方については、
ドライバの公式 [facebook/php-webdriver](https://github.com/facebook/php-webdriver) のWikiや
Qiitaの記事 [PHPUnit + php-webdriver でWebUIのテストを書く](https://qiita.com/zaburo/items/f11357170953a3c34b8f) などを参考にしてください。

#### 参考
[facebook/php-webdriver](https://github.com/facebook/php-webdriver)
[seleniumを使ってPHPでChromeの自動操作をする](https://qiita.com/Rasukarusan/items/0ca204d5b0f0fb876252)
[PHPUnit + php-webdriver でWebUIのテストを書く](https://qiita.com/zaburo/items/f11357170953a3c34b8f)
[規模別PHPUnitでのテストの書き方いろいろ](http://blog.tojiru.net/article/253157654.html)

## サンプルテストを実行する
テストを実行するには、コマンドラインで実行する方法と、PhpStormで実行する方法があります。

#### コマンドラインで

- すべてのテストを実行する場合

```
$ docker-compose run --rm php vendor/bin/phpunit
```

  
- ファイル名指定して実行する場合

```
$ docker-compose run --rm php vendor/bin/phpunit tests/SampleTest.php
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/2c2f5075-2e0d-c7e7-6363-2e5a54144d5f.png)

- groupを指定して実行する場合  
テストの数が多くて時間がかかるなど、特定のテストだけ実行したい場合などに使われるようです。  
テストメソッドに、@groupアサーションと、group名を追加し、

```   
    /**
     * @group google
     */
    public function testGoogle()
    {
    	// 省略
    }
```



次のように--groupフラグに実行したいグループ名を付けて実行

```
$ docker-compose run --rm php vendor/bin/phpunit --group="google"
```



#### PhpStormで

①`Edit Configurations...` を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/bdab2f82-e4e5-b26b-3914-e9675def9834.png)


②+マークからPHPUnitを選択し、新しいテンプレートを追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/726fb7b3-93bc-5dae-0725-1710a85d5a20.png)


③ディレクトリ/クラス/メソッド単位で実行したいテストの範囲を定め、OKをクリック
また、コマンドでオプション指定するのと同様にオプションを指定したいときは、`Test Runner options:` に入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/00a7caae-fed3-9da0-afff-19172b3fe553.png)


このテスト実行時の設定をGitなどでバージョン管理したい場合、右上のチェックボックス`Share through VSC`にチェックを入れます。
もし「このファイルまたはディレクトリは無視されているからVCSに追加できない」というエラーが出たら、
画面の指示にしたがって`Remove from ignored files`をクリックすればバージョン管理できるように自動で.gitignoreを書き換えてくれます。

手動で書き換える場合、次のように記述すると.ideaディレクトリの他のファイルは無視しつつ、テスト実行設定ファイルだけを管理できます。

```
/.idea/*
!/.idea/runConfigurations/*
```

④作成したテスト実行テンプレートを選択した状態で再生ボタンクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/cfe84dbc-6daa-9af9-b0b0-d1ca4daeb98d.png)

これでテストが実行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/ade663cd-77fa-2999-b731-41687d1a45ef.png)



#### 参考
[.gitignore の書き方。ファイル/ディレクトリの除外と反映方法](http://www-creators.com/archives/1662)
[[Git] .gitignoreの仕様詳解](https://qiita.com/anqooqie/items/110957797b3d5280c44f)




## おまけ　VNCで爆速で入力している様子が見れる

- [UltraVNC](https://forest.watch.impress.co.jp/library/software/ultravnc/)をDL
- インストールとか諸々をこなす
- localhost:5900にConnect　※ポート番号はdocker-compose.ymlでnodeコンテナに割り当てた番号
- パスワード入力(初期値はsecret)
- この状態になったら待機
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/c9902ac6-95f1-9598-9414-6b9e1f1ba8e9.png)

- テスト実行すると、Seleniumが働いている様子が見れる。デザインなど目視したい場合などに使うもの...？
  私の場合、ブラウザテストを書いていてエラーの原因がわからないときに稀に参考になった。
- この記事では特に活用しないので楽しい気分を味わいたい人向け。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/4c37d1e3-a092-4bc3-d958-897a4ad579e1.png)


## テストの書き方tips

#### HTTP通信ヘッダ情報は、Seleniumで取得できない

[Selenium実践入門 自動化による継続的なブラウザテスト](https://www.amazon.co.jp/Selenium実践入門-自動化による継続的なブラウザテスト-WEB-PRESS-plus/dp/4774178942)　p111

によると、

>HTTP通信ヘッダ情報は、そのままではWebDriverプログラムで取得できません。
>HTTPヘッダの内容を取得するには、HTTPプロキシを利用するのがお勧めです。

ということらしいので、
ステータスコードの確認などをしたい場合、curlなどを利用する必要がある。


「なぜHTTPヘッダーを返すメソッドがないのか」という問いに対する公式回答は[これ](https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/141)っぽい。
全部は読んでいないが、一番最後に

> Browsers can check that data via js code, so I do not see why the webdriver should
> be more limited / not expose it

ブラウザはjsコードを介してデータをチェックできるので、なぜWeb Driverがそうするべきかわからない

とある。他の方法でできるんだから実装しなくていいでしょ、ってことらしい。



#### 新しいタブ/ウィンドウに切り替える方法

ボタンをクリックすると別タブが開かれ、別タブのほうをSeleniumで操作したいときは、操作対象を明示する必要があります。

次のコードでは、任意の操作の前後で全てのウィンドウを取得し、その差分を新しいウィンドウとしています。

        $windowHandlesBefore = $this->driver->getWindowHandles();
    
    	// クリックなどの任意の操作
        $this->driver->findElement(WebDriverBy::cssSelector('.ok-button'))->click();
    
        $windowHandlesAfter = $this->driver->getWindowHandles();
        $newWindowHandle = array_diff($windowHandlesAfter, $windowHandlesBefore);
        $this->driver->switchTo()->window($newWindowHandle);

この方法は、[公式のwiki](https://github.com/facebook/php-webdriver/wiki/Alert,-tabs,-frames,-iframes)に倣った。(注意点などいろいろと記載されているので不明点があったらまず読むのがおすすめ)


> Note: Do not use end($handles) to find the last open window, because getWindowHandles() does not necessarily return window in the order in which they were opened.

注：最後に開いたウィンドウを見つけるためにend($handles)を使用しないでください。
なぜなら、getWindowHandles()は、ウィンドウが開かれた順に返されるとは限らないから。



#### WebDriverBy::cssSelector()メソッドを使えばid属性やclass属性がなくても要素を指定できる

class属性、id属性、name属性、が何も指定されていない。
HTMLタグで指定しようとすると重複して特定の要素を指定できない。
CSSやJSが効いているので、テストのためだけに実装側を変える時間はない。
for属性ならあるけど...

という要素に対して操作したい。
そんなときは、WebDriverBy::cssSelector()メソッドを使い、CSSやJSでDOM要素を指定するように要素を指定すればOK。

```php
$this->driver->findElement(WebDriverBy::cssSelector('.form-check-list li label[for="apple"]'))->click();
```

id()メソッド、className()メソッド、xpath()メソッドなど他にもメソッドは存在するが、できるだけ統一するようにプロジェクト内でルールを決めたほうが見やすい。


## エラー対策
#### Facebook\WebDriver\Exception\ElementNotVisibleException : element not interactable

ざっくりいうと、 `display: none;` によって要素が見えていないとclick()などの操作ができない、というエラー。
「画面外に存在していて、スクロールしないと見えない要素」のことではない。画面外にあろうがSeleniumで操作を行える。

チェックボックスやラジオボタンなど、input属性要素を隠して上に別の要素を表示していると、
モーダルなどと同様、非表示状態であるとみなされるため起きているエラーだった。

そのため、本当に表示されている要素、display: block;な要素を選択してclick()メソッドを呼ぶ必要がある。
cssSelector()メソッドを使い、細かく指定すれば可能。

[Selenium 日本語ドキュメント Seleniumのコマンド](http://oss.infoscience.co.jp/seleniumhq/docs/04_selenese_commands.html)


#### Facebook\WebDriver\Exception\WebDriverCurlException : Curl error thrown for http POST to /session with params

```
Facebook\WebDriver\Exception\WebDriverCurlException : Curl error thrown for http POST to /session with params: {"desiredCapabilities":{"browserName":"chrome","platform":"LINUX","chromeOptions":{"w3c":false,"binary":"","args":["--disable-gpu","--no-sandbox"]}}}
```

または

```
Facebook\WebDriver\Exception\WebDriverCurlException : Curl error thrown for http POST to /session/{ランダムの文字列(たぶんセッションのhash)}/url with params:(省略)
```

というエラーに何度も出くわした。
また、テストが成功したり失敗したり不安定だった。

解決するために下記を試したが、正しい解決方法はわからない。

- ドライバをヘッドレスで実行する。
- phpunitのtearDown()メソッドを用い、テスト成功/失敗に関係なく、必ずウィンドウプロセスを終了させる。

  ```php
      protected function tearDown(): void
      {
          $this->driver->quit();
      }
  ```

以下、  [Selenium実践入門 自動化による継続的なブラウザテスト](https://www.amazon.co.jp/Selenium実践入門-自動化による継続的なブラウザテスト-WEB-PRESS-plus/dp/4774178942)　p43 より

  > quitメソッドを呼び出さずにテストプロセスを終了すると、ブラウザのウィンドウは閉じられず、
  > Chrome Driverサーバ・IEDriverサーバ・PhantomJSのプロセスも終了されずに残ってしまいます。
  > 新たにWebDriverインスタンスを生成しても、これらのウィンドウプロセスは再利用されないため、
  > 残ってしまったものは手動で終了させる必要があります。テストプログラムを途中で強制終了した場合も、これらのウィンドウ・プロセスは当然残ってしまいます。
  > たとえばデバッグ中のテストをEclipseの「停止」ボタンで終了させた場合も、quitメソッドは呼び出されず、
  > ウィンドウ・プロセスが残ってしまいます。


- ドライバ生成時にタイムアウトの時間(ms)を指定できるが、0を指定しない。また、制限時間が短すぎると、ページのロードが終わる前にタイムアウトになりテストが実行されないので、余裕をもった時間を指定する。

  ```
  $this->driver = RemoteWebDriver::create($seleniumServerHost, $desiredCapabilities, 80000, 80000);
  ```


このエラーは出にくくなったが、
テストが成功することもあれば、
WebDriverCurlExceptionが再発生したり、TimeOutExceptionが発生したりしている。

~~明示的な待機wait()～until()か、~~
~~暗黙的な待機implicitlyWait()を使う必要がありそう。調査中。~~
https://github.com/facebook/php-webdriver/wiki/HowTo-Wait#implicit-waits
[Selenium で別ウィンドウが開くまで待つ](https://qiita.com/QUANON/items/285ad7157619b0da5c67)



どうやらSeleniumで別タブを開くこと自体が不安定になる原因のよう。
[全国のSeleniumer必読](https://qiita.com/oh_rusty_nail/items/b8ba525d31ea7c522856#別タブで開くことの検証)?より

>別タブで開くことの検証別タブで開くことの検証
>
>- 別タブ/ウィンドウで開くことのテストは可能
>- ボタン等をクリックすると新しいウィンドウが「ぽこっ」と出て、waitForPopUpなりselectWindowなりをかましてカレントウィンドウにするとか
>- ただ、できればそのやり方はしないほうが良い
>  - ウィンドウが立ち上がるまでに時間がかかって不安定のなテストの原因となりかねない
>  - そもそも別タブ/ウィンドウで開くことはHTMLレベルで判断でき、ブラウザの機能として提供されているため、わざわざSeleniumでその挙動まで見る必要はない

hiddenパラメータでデータを送信しなければいけない場合はどうしたらいいのか...。

【追記 2020/01/09】
テストの結果が不安定だったのは、wait()～until()を使い、
「クリックできる状態になるまで待機してからクリックする」「画面に表示されるまで待機してから操作する」という処理を入れていなかったため。
(私の場合、セレクトボックスを選択する前に「クリックできる状態になるまで待機」という処理を入れるとテストが安定した。)
↓下記記事とそのリンク先は、wait()～until()メソッドを使うとき、どれを使ったらいいのか参考になった。
[ちゃんと動いていたSeleniumがたまに失敗するケースまとめ](https://qiita.com/radiocat/items/6cbc4903887dc9439185)
>例えばvisibilityOfElementLocated()は表示されるまで待ってくれるが、presenceOfElementLocated()は表示/非表示にかかわらずDOM要素の存在だけしか待たない。

もちろん、テスト失敗時や、テスト実行を自分で止めた場合などに、確実にプロセスを停止させることも必要。
DockerでSelenumの環境を作っておくと、
(テストを実行してからテストコードの誤りに気付いたときなどに)テスト実行を止めてDockerコンテナ終了→起動→すぐテスト再開　ができるので便利だなと思った。
むしろ、パソコンのホスト上でやったりした場合、どうやってプロセス終了させるんだろう・・・。いちいち`$driver->quit()`を呼び出すんだろうか。
【追記ここまで】

#### 参考
[Selenium実践入門 自動化による継続的なブラウザテスト](https://www.amazon.co.jp/Selenium実践入門-自動化による継続的なブラウザテスト-WEB-PRESS-plus/dp/4774178942)
[全国のSeleniumer必読](https://qiita.com/oh_rusty_nail/items/b8ba525d31ea7c522856#別タブで開くことの検証) 
[なぜSeleniumテストは不安定なのかとその対策](https://www.kaizenprogrammer.com/entry/2016/12/18/105735)
[Selenium で別ウィンドウが開くまで待つ](https://qiita.com/QUANON/items/285ad7157619b0da5c67)


## メモ

[Laravel+TDDの基礎](https://laravel-tdd.doc.tacck.net/)
Laravel dusk使ってて面白そう