SPAサイトにおいて、Selenium::Remote::DriverでGUIテストを行う

特にSPAサイト限定というわけでもないが、業務でSeleniumを使ったGUIテストを試せたので知見として共有する。なお、Perlを使ったのは現在のアプリケーションがPerlで構成されているので統一感を持たせたかった為である。

個人的にはRubyのほうがよりカジュアルに書けるかも知れないと思っているが、それはこちらの[ruby + selenium webdriverチュートリアル](http://katsulog.tech/ruby-selenium-webdriver-tutorial/)を参照されたい。

この記事ではタイトルにSelenium::Remote::Driverと入れたが、**実際にはサーバ事前立ち上げの必要がないブラウザ毎の拡張モジュールを使う**。今回はChromeで動かしたため、Test::Selenium::Chromeを使うこととする。

# テストするWebサイトの構成
**SPAであり、テーブル生成などはjQueryの非同期通信でデータ受信後にDOM生成しているタイプ**のものである。しかしSPAの構成が完全ではない。URLがトップページのまま変化しない。

# テストを行うための環境
クライアントサイドからテストする。

- Win7 pro 64bit
- ActivePerl
- [こちら](https://www.seleniumhq.org/download/)からダウンロードしたChrome用のSelenium Web Driver

# 実施するテスト
トップページ遷移⇒ログイン⇒一覧情報ページから詳細ページへ遷移⇒目的のリンクリストの数が想定と合っているか？　である。

# テストの主な流れ
前途したようにSPAが不完全であるので、**クリックなどの動作を実行させる前にかならず必要な要素の状態を確認する**というやり方を取る。ログイン後の流れは以下の通りである:

- 必要な要素の確認
- ブラウザ操作の実施
- スクリーンショットの取得
- 次操作のためのトリガーとなるアクション

# テスト実施
## ブラウザ用のドライバを立ち上げる
今回使うCPANモジュールは以下の通り。

```perl:check_download_link.t
use Test::More;
use Selenium::Waiter qw(wait_until);
use Test::Selenium::Chrome;
```

- Test::Moreはリンクリストの数が想定通りかを確認するために使う。
- Selenium::Waiterは目的要素の出現や状態を監視するために使う。
- Test::Selenium::Chromeはブラウザ用ドライバの立ち上げや各種操作のために使う。Selenium::Remote::Driverと比較してサーバ立ち上げの必要がなく、Selenium::Chromeと比較してテストに好都合なメソッドが含まれているためである。

ドライバ立ち上げの箇所:

```perl:check_download_link.t
my $driver = Test::Selenium::Chrome->new(
  # webdriverのパスを指定
  binary => "$driver_dest/chromedriver.exe",
  # ver.1.27で足りない要素を指定
  webelement_class => 'Test::Selenium::Remote::WebElement',
  # ヘッドレス(GUI非表示)で立ち上げるために設定
  extra_capabilities => {chromeOptions => {args => ['headless', 'disable-gpu', 'window-size=1920,1080', 'no-sandbox' ]}}
  );
```

**$driver_destはドライバの格納先を示している。環境によって読み替えてほしい**。
AcrivePerlでは最新のver.1.31がインストールできなかったので、webelement_class要素を追加している。これは、**Test用に特化したWeb要素操作のためのモジュールを読み込みますよ**、ということを示している。ver.1.31にはこの記述があるが1.27には存在しないのでこちらを追記する。どんなチェック操作ができるのか、というのは[公式リファレンス](https://metacpan.org/pod/Test::Selenium::Remote::Driver#$twd-%3Eclick_element_ok($search_target-%5B,$finder-,$desc%5D);)を参照すると詳細が載っている。

Selenium::Remote::Driverではページ遷移などの汎用操作を担保し、Web要素の操作にはSelenium::Remote::WebElementというモジュールを使うことで役割分担している。そしてユーザはどの操作でどのモジュールのメソッドが使われるか意識する必要がないようになっている。

## トップページ遷移⇒ログイン

```perl:check_download_link.t
my ($user_id, $password) = ('ユーザid', 'パスワード');
$driver->get('http://ログインページのURL');
$driver->title_is('トップページのタイトル', 'top page title');
## 必要な要素の確認
wait_until { $driver->find_element_ok("button.login-btn", "css", 'enable login button') };
## ブラウザ操作の実施
$driver->find_element_by_css("#login-email")->send_keys($user_id);
$driver->find_element_by_css("#login-password")->send_keys($password);
## スクリーンショットの取得
$driver->capture_screenshot("$output_dest/1_login.png");
## 次操作のためのトリガーとなるアクション
$driver->find_element_by_css("button.login-btn")->click();
```

wait_until()でブロック内の動作がTRUEになるまで待機できる。デフォルトは30秒。
find_element_ok()はチェックメソッド。第二引数には第一引数の要素指定をどう解釈させるかの文字列を書く。デフォルトは"xpath"であるがcssセレクタの方がわかりやすい。第三引数はテスト実施時にコンソールに出力されるメッセージである。

## 一覧情報ページから詳細ページへ遷移

```perl:check_download_link.t
## 必要な要素の確認
wait_until { $driver->find_element_ok(".dgc-list-body > tr:nth-child(1)", "css", 'get fleet list') };
## スクリーンショットの取得
$driver->capture_screenshot("$output_dest/2_fleetlist.png");
## 次操作のためのトリガーとなるアクション
my $link_list_of_voyage = $driver->find_element_by_css(".dgc-list-body > tr:nth-child(1) > td:nth-child(4) > a:nth-child(2)");
$link_list_of_voyage->click();
```

ここでは非同期処理を使って一覧情報が作成されることを待機している。待機できたら後はリンククリックのみなので、ブラウザ操作は特に実施しない。

## 目的のリンクリストの数が想定と合っているか？

```perl:check_download_link.t
my $dl_elem;
## 必要な要素の確認
wait_until {
  $dl_elem = $driver->find_element("#dl", "css");
  $dl_elem->is_enabled;
  $dl_elem->click;
};
$driver->click_element_ok("#dl", "css", " download link");
## ブラウザ操作の実施
$dl_elem->click();
## スクリーンショットの取得
$driver->capture_screenshot("$output_dest/3_download_list.png");
## ダウンロードリンクの数が想定と合っているか確認
my @dl_list = $driver->find_elements(".download-add-window", "css");
is(scalar @dl_list, 1, 'valid nums of download list');
is($dl_list[0]->get_text, 'List of Voyage', 'download type');
```

データが準備し終わるまでダウンロードリンクが有効化されないので、クリックできるようになるまで待機させている。メッセージを出したかったのでclick_element_ok()を使っている。ここで自分はハマったので（公式リファレンスを読み込んでおけば良かったのだが。。）以下に整理しておく：

- $driver->find_element()を使って取得したものは、Selenium::Remote::WebElementのインスタンスである。
- $driver->clear_element_ok()はTest::Selenium::Remote::WebElementのインスタンスを使って要素のチェックを実施している。

clear_element_ok()が失敗、**Selenium::Remote::WebElementにそんなメソッドはない！**とエラーになった。が、これは**Test::Selenium::Chromeをnewするときにwebelement_classを指定していなかったので、デフォルト設定のSelenium::Remote::WebElementが使われた**ということだった。

is()はTest::Moreのメソッドで、ここでは取得したリストの数が正しいか、などのWebに限定しないチェックのために使っている。

## 後始末

```perl:check_download_link.t
$driver->quit;
done_testing;
```

ドライバの終了、テストコードの完了文言を記載。これで以上である。

テスト実行時のコンソール出力は以下のとおり：

```:テスト結果
C:\gui_test>perl check_download_link.t
ok 1 - top page title
ok 2 - enable login button
ok 3 - get fleet list
ok 4 - click_ok' download link'
ok 5 - valid nums of download list
ok 6 - download type
1..6
```

# 最後に
途中何度か詰まった箇所もあったが、振り返ってみればSeleniumと使用するドライバの公式リファレンスをきちんと読むことが大事なんだと実感させられた。