どうも。エンジニアの@bumcruです。
Qiitaの「Selenium/Appium」アドベントカレンダー9日目の記事です。
去る10月に行われた開発合宿、今年のテーマは「自動化」でした。
テーマの範囲内で、個々人が自由に好きなものを作るわけですが、
僕が選んだお題は「Seleniumでのブラウザテスト自動化」です。
今回は、Selenium WebDriver
*1
の知識がほとんど無い状態から、
2泊3日で弊社の外資就活ドットコムの会員登録フローを自動化するまでの顛末と、
ハマりやすいポイント、その解決法をご紹介します。
基本的には「せれにうむ?」な人に向けた記事なので、すでにSeleniumを十分使いこなせている人は、 「あるあるw」とか「そこはこうした方がいいよ」などのコメントを頂けると幸いですm(_ _)m
Webサービスこそ E2Eテストを自動化すべき
パッケージ開発や受託開発の世界では、
単体試験、結合試験、総合試験などのテスト計画を立て、きちんとテストを行ってから納品します。
僕自身、テストのためだけに60人月以上を平気で費やす大規模なプロジェクトにテスターとして参加したこともありますが、
基本的に1度納品したら今後関わることが無くなるため、納品前にとにかく徹底的にテスト させられます させて頂きます。
要件定義→設計→開発→テスト→納品といフローが明確なウォーターフォール・モデルの開発現場では、 同じ内容のテストを継続的に何度も繰り返すということはほとんど無いため、E2Eテストを自動化するためのテストコードを書くよりも、 テストを手作業で行う方が工数が少なく済む場合が多く、テストを自動化するメリットがほとんど無いケースも間々あります。
対してWebサービスの場合、
大小様々な機能追加や仕様変更、A/Bテストなどの各種施策を随時リリースするのが日常茶飯事なので、
その度に、今回のリリースとは全く関係ない部分のテストまで全て手作業で行っていたら、リソースがいくらあっても足りませんし、
テストの質を一定に保つのも困難です。
もし「やれ!」と言われたなら、 3ヶ月もしない内に気が狂ってピクニックに出かけてしまうでしょう。
主要な導線だけでも自動テストを回す
いきなり「E2Eテストを自動化しろ」と言われても、何から手を付ければ良いか戸惑うかもしれませんが、 まずはサービスの成長やマネタイズに密接に関わっている部分からテストを作成するのが良いのではないでしょうか?
ほとんどのWebサービスにとって、最も重要な導線の1つに「会員登録」があります。 なぜなら、会員登録数は そのサービスの成長規模を示す最も分かりやすい指標となるからです。 もし、会員登録フローで何らかの異常が発生し新規登録ができなくなっていたら、サービスにとって大打撃となることは間違いありません。
ちなみに、 「外資就活ドットコム」の会員登録フローは手作業でやると7分以上もかかります。 ユーザーの皆さん、よく登録してくれているな...。本当、ありがとうございます!
きっと熟練者ともなれば4分くらいで終わるのかもしれませんが(熟練者って何だ)、 そもそもテストする人によって、かかる時間も精度もマチマチでは、せっかくテストをしても全く安心感がありませんね。
Seleniumでできること
Seleniumとは何か。簡単に言うと、
❝ブラウザで出来る大体のことをコード化するためのライブラリ❝
です。
具体的に挙げると、
- ブラウザを起動する
- URLを指定して、任意のページに遷移する
- 画面上の指定した入力欄に値を入力する
- 指定したボタンをクリックする
- 表示されている内容を評価して、テストの可否を判定する *2
といった基本的な操作を組み合わせたテストコードを、RubyやJava、PHPなどの多様な言語で書き、CLIから実行することができます。
つまり一度テストコードを書いてしまえば、 コマンド一発で同じ内容のテストを全て自動で再現でき、成功か失敗かを簡単に確認できるのです。
Seleniumには他にも、
- Firefox、Chrome、IEなどのクロスブラウザテスト
- AndroidやiOSなどのモバイルブラウザテスト
- 画面のスクリーンショットを画像として保存
- ヘッドレス化してブラウザを表示せずに実行し、CIに組み込む
などなど、豊富な機能が用意されており、開発コミュニティも活発です。
開始時点のスキルセット・使用言語など
- HTML、CSS、JavaScript、jQuery、PHPはまずまず
- Rubyは少しかじった程度
- せれにうむ(^v^)?
今回は、合宿前に慌てて買った
『Seleniumデザインパターン & ベストプラクティス』
を読み進める形で開発を行いました。備えあれば憂いなしです。
同書ではFirefoxとRubyを使ってサンプルが作成されているため、使用言語はRuby。使用ブラウザは Firefox に決定です。
ちなみに、Firefox以外のブラウザを使う場合は別途ドライバーをインストールする必要があり、少し手間がかかるようです。
テストシナリオ
今回の目的は、新規会員登録したユーザーが登録したアカウントで正常にログインできることを確認することです。
会員登録の流れ
- 登録用のメールアドレス入力画面
- 仮登録メール送信完了画面
- 届いたメールに記載されたワンタイムURLへアクセス
- ユーザー情報入力画面 ✕ 5〜7 (!?)
- 確認画面
- 完了画面
...なかなかゴツいですね。
完了画面へ遷移した後は、
- ログイン画面へリダイレクト
- 作成したばかりのアカウントでログイン
- ログイン後のページに登録したユーザ名が表示されていることを確認
で、晴れてテスト成功となります。
準備など
前提として、
- Ruby
- Firefox
のインストールは終わってるものとします。
肝心のSelenium WebDriverは
$ gem install selenium-webdriver
でインストールし、rbファイルの先頭で
require 'selenium-webdriver'
で読み込み、
@selenium = Selenium::WebDriver.for(:firefox)
で、Objectを生成するだけです。*3
立ちはだかる壁
上述の新規会員登録フローを突破するテストを書くには、いくつか超えなければならない関門があります。
ベーシック認証
テスト実行に本番環境を使うのはちょっと...、という方も多いでしょう。 弊社でも、最新の本番環境とほとんど同じ状態のステージング環境を用意しており、 テストはそこで実行することにしました。 しかし、ステージング環境にはベーシック認証をかけて一般に非公開にしているため、 普通にテストを動かすとブラウザを起動した後にベーシック認証のユーザー名とパスワードを入力するダイアログが表示されて止まってしまいます。 困った!
この場合、http://username:password@hogehoge.com
のように、
ホスト名の前にユーザ名:パスワード@
を挿入することで、
認証ダイアログを表示しないようにすることができます。あっさり解決。
*4
一意制約バリデーション
メールアドレスやユーザー名などの一意制約のある入力項目の場合、 一工夫しないとバリデーションに引っかかってしまい次の画面に進めずテストが中断してしまいます。
これは、タイムスタンプを含めたメールアドレスをテストごとに生成するようにすることで(ほぼ)解決できます。
# 年月日時分秒までのタイムスタンプを含んだメールアドレス生成 timestamp = Time.now.strftime("%y%m%d%H%M%S") email = timestamp + '@example.com' # id="emailInputField"のテキストボックスに、生成したメールアドレスを入力 @selenium.type_text(email, "emailInputField", :id)
ワンタイムURL
登録用のメールアドレスにチェックディジット付きのワンタイムURLが送られ、 そのURL以外からだと登録画面にアクセスできない、という仕様はよくあるパターンです。
サーバが生成したワンタイムURLを取得するにはどうすればよいでしょう?
方法はいくつかありますが、重要なアイデアは、
❝ ブラウザでできることなら大体できる ❝
ということです。
テスト用のメールをGmailなどのWebメーラで受信できるようにしておけば、
- Gmailへアクセス
- Googleアカウントのユーザ名とパスワードを入力してログイン
- 受信したメールを検索
- 該当メールをクリック
- メール本文のワンタイムURLを取得
ということも難しくありません。
Gamilの有名な裏ワザとして、
- 「@」以前の「.」(ドット)は認識されない
- 「+」と「@」の間の文字列は認識されない
というものがあります。つまり、
example@gmail.com
e.x.a.m.p.l.e@gmail.com
example+test@gmail.com
は、 Gmail的には全て同じアドレスとして認識されるので、 テスト対象のアプリケーション側では無限に一意のメールアドレスを登録しつつ、 その全てを同じメーラで受信することが出来るのです。
これで何回でもテストを走らせることができますね。
ちなみに弊社では、ステージング環境にMailCatcherというライブラリが導入されており、 ステージングサーバが送信したメールは本物のアドレスには届かず、代わりに全てのメールをMailCatcherのメーラで確認できるようになっています。
MailCatcherのメーラにはブラウザからアクセスできるので、仮登録メール送信完了画面へ遷移した後は、 MailCatcherのメーラの画面に移動し、先ほど生成したアドレス宛のメールを検索することで、 ワンタイムURLを取得することができます。*5
実際にSelenium WebDriverが動いている様子
まだSelenium WebDriverがどんなものなのかイメージ出来ていない方もいるかと思うので、 開発合宿での成果をお見せしてしまいます。
最初にcommandファイルをクリックしてからは、マウスにもキーボードにも一切触れていません。 恐るべきスピードで入力欄を埋め尽くしていますが、もちろん早送りなどもしていません。(全編を見たい人はこちら。100秒のGIFアニメです。)
ハマりがちなポイント
Test::Unitが使えない
自分がRuby初心者ということもあり、以下の警告で盛大に出鼻を挫かれました。
Warning: you should require 'minitest/autorun' instead. Warning: or add 'gem "minitest"' before 'require "minitest/autorun"'
調べてみると、
test-unitはRuby用のxUnit系の単体テストフレームワークです。Ruby 1.8まではRuby本体に標準添付されていましたが、Ruby 1.9.1からはminitestというフレームワークが標準添付されています。(引用元)
...、とのこと。
なので、本のサンプルソースの、
require 'test/unit' ... class CheeseFinderTest < Test::Unit::TestCase
となっている所を、
require 'minitest/autorun' ... class CheeseFinderTest < Minitest::Test
と読み替えることで解決です。
「指定した要素が見つからない」と怒られる...
Seleniumu WebDriverを始めたばかりの人が最もつまずくのは、
Selenium::WebDriver::Error::NoSuchElementError
(そんな要素ないぜ?)
というエラーではないでしょうか?え、僕だけ?
今になってみると何てことないかもしれませんが、 2泊3日の間、僕がハマりにハマったポイントとその対処法をいくつかご紹介します。
リッチなセレクトボックス
画像のような、少しリッチなセレクトボックスでは注意が必要です。 セレクトボックスをクリックするとプルダウンに検索ボックスと選択肢が表示され、 検索ワードを入力すると、選択肢が動的に絞り込まれるというものです。
「このセレクトボックスではこの選択肢をクリックしよう」と決めてテストを書いていたのですが、
IDを指定してもNoSuchElementError
というエラーが出てテストが失敗します。
「IDで指定してるのに何でやねん」と不思議に思いながら何度かトライしていたら、 ふと「あれ、さっきとID変わってるやん!」という驚愕の真実に気が付きます。
後からアプリ側のソースを確認してみると、Select2という jQuery系のライブラリを利用して実装されており、選択肢に振られているIDは便宜上振られているに過ぎないようでした。
そこで、まず選択肢リストが1件のみになるように絞り込み、 CSSパスで指定しやすくしてから選択肢の要素をクリックする、という方法で切り抜けました。
# セレクトボックスをクリック @selenium.click("comapanySelect", :id) # 表示されたプルダウン内の検索ボックスに検索ワードを入力 @selenium.type_text(companyName, "s2id_autogen1_search", :id) # 1件のみがリストに残る状態にした上で、選択肢をクリック @selenium.click("#select2-results-1 ul.select2-result-sub li")
DOM要素のIDではなく、選択肢の表示ラベル名がそのままテストデータになるため、 非エンジニアのメンバーにテストデータを作ってもらう時にも作りやすくなりますね。
「IDもアテにならないな...」という、良い勉強になりました。
「一意のセレクタ」でも指定できない?
MailCatcherのメールの本文をクリックしようと思った時のことです。
ページ右上の検索ボックスに登録時のメールアドレスを入力して受信メールリストを絞り込み、 たった今受信したばかりのメール1件のみを表示するところまでは上手くいきました。
表示されているメールの本文をクリックしようとしても、NoSuchElementError
が出てしまいます。
Firefoxのデバッグツールには、「一意のセレクタをコピー」という超便利機能があるのですが(3日目に気づいた...)、 この機能を使って取得した一意なCSSパスを使っても上手く行きません。
よくよくDOMを読み解いていくと、MailCatcherの本文を表示する領域は<iframe>
の中に入れ子になっています。
Seleniumで<iframe>
内部の要素を指定する場合、以下のように向いているフレームを切り替える必要があります。
# 1つ目のiframeに切り替える @selenium.switch_to.frame(0) # フレーム内でのセレクタで要素を指定する linkUrl = @selenium.get_inner_text("body > a:nth-child(3)")
本格的に導入するにあたって
Selenium WebDriverでの自動ブラウザテストを導入するにあたり、アプリケーション側でも対応すべきことがありそうです。
テスト対象となる要素にIDをつける
今回テストを書いてみて思ったことは、
「セレクタ(ロケータ)を書くのが大変!」
ということでした。
というのも、クリックしたいボタンや入力したいフィールドにIDがついていないことが多く、 CSSやXPathでセレクタを書くと長くなってしまい、後から見てもどの要素のセレクタなのか分からないことが多々ありました。
日本のSeleniumコミュニティでこの話題になったのですが、
テスト対象となるような要素については出来るだけIDを付けておくようにし、
何らかの事情によりIDを付けられない場合は、例えばdata-selenium
などのカスタムデータ属性を付ける、という方法がベターのようです。
jQueryを読み込む(まだ使ってない場合)
Ajaxを使って、選択肢Aの内容に応じて選択肢Bの内容をサーバから取得して表示したり、 モーダルダイアログやスライドショーなどを表示するのにアニメーションを使ったりしているUIでは、 対象の項目が画面に表示されるまで、きちんと待ってあげる必要があります。
Selenium WebDriverには、処理完了を待つためのWaitクラスも用意されていますが、ここでは自分で完了条件を記述する必要があります。 ここで、「(Ajax処理の結果、)特定の要素が表示されること」を完了条件とするのが一般的ですが、 完了条件の定義は常に明確とは限らず、テストを書く人人によって異なる定義をしてしまったり、 定義した完了条件が不十分でテストが中断してしまったりということは十分に考えられます。
そこで、同書ではjQueryを使った方法が有効だと紹介されています。
Selenium WebDriverでは、excute_script
メソッドを使うことで任意のJavaScriptコードを実行することができますが、
jQueryなら標準のメソッドやプロパティを使うことで、Ajaxやアニメーション処理の完了を簡単に確認することが可能です。
モダンなWebサービスのUIでは、こうしたエフェクトや処理が頻繁に使用されていると思うので、 jQueryを導入した方がSeleniumでのテストを書くのが楽になる場面は多いでしょう。
AngularやReactなどを使っている場合でもjQueryを共存させることは可能ですが、 どうしても難しい場合は、上述のWaitクラスを使うしかないのかもしれません。(もし良い方法をご存知の方がいたら教えて下さいm(_ _)m)
CIに組み込む
先にも述べましたが、Selenium WebDriverはヘッドレスで実行することも可能ですので、 コミットやデプロイのタイミング、あるいは何もなくても1日1回など定期的に実行することで、 サービスに異常が発生した場合、迅速に検知することができます。
テストの量が膨大になってきたら、PhantomJSなどのヘッドレスブラウザを使うことで、実行時間を短縮することも出来るようです。
まとめ
僕が実際にSelenium WebDriverを使いテストを書いてみて思ったことは、
❝ Seleniumって、思ってたより簡単じゃん! ❝
ということです。
自分が比較的フロント寄りのエンジニアだからということもあるかもしれませんが、 要素を指定して入力/クリックする、などの処理は、jQueryに近い感覚で書くことができます。
自由度がかなり高く、ブラウザで出来ることは大体できるので、 工夫次第であらゆるシステムのE2Eテストに導入できると思います。
このエントリーを読んだ皆さんならきっと、つまずくことなく、もっと速くテストを書けることでしょう。
今回はRubyでテストを書きましたが、前述の通り多様な言語で書くことができるので、 自分の得意な言語で書けば学習コストもそれほど高くありません。
そして何より、苦労して書いたテストがブラウザ上で疾走する様は、
眺めているだけで心が震えます!!
皆さんもこの爽快感を味わったら、テストを書くのが好きで好きで堪らなくなるでしょう。
おまけ:本のレビュー
今回お世話になった『Seleniumデザインパターン & ベストプラクティス』は、かなりの名著だと思います。
日本版の初版が出たのは2015年9月で、かなり最近です。
本書の構成は、
初めははわざと問題のある(でも一見良さそうな)テストを書かせてから、
その問題点と改善策を提示しつつ、徐々に高度で複雑なテストを書くようにスケールアップさせるようになっているので、
より良いテストの書き方をスムーズに理解することができました。
SpaghettiパターンからPageObjectパターンまで、テストを書くための様々なデザインパターンを紹介されているのですが、 それぞれの利点と欠点がわかりやすく解説されており、自分たちの開発規模に応じてどのパターンを採り入れるのがベストなのかを考えさせてくれます。
また、初心者にとにかく親切に書かれており、 Windows、MacそれぞれのCLIの起動方法から、Rubyの基本的な文法まで手厚くレクチャーしてくれます。
もし本格的にSelenium WebDriverを始めようと考えているなら、一度読んでおくと習得がより速くなると思うのでオススメです。
*1:Seleniumには1系と2系があります。 2系は「Selenium Webdriver」と呼ばれており、単に「Webdriver」とも呼びます。 1系と2系とではかなり仕様が異なっており、本エントリーでは常に2系を指しています。
*2:Selenium Webdriver自身に、テストツールとしての機能はありません。テスト可否の判定は、JUnitやPHPUnitなどの言語ごとに用意されたテストライブラリと組み合わせて行います。
*3:サンプルコード内で使っているメソッドは、テストコードを楽に書くためにSelenium WebDriverの標準メソッドをラップしたヘルパーメソッドです。標準メソッドとやや異なりますが、何をしてるのかは理解できるはずです。
*4:Internet Explorerでは、セキュリティ上の理由によりこの方法でBASIC認証は突破できないようです。
*5:ワンタイムURLをそのままクリックすると再度ベーシック認証に足止めされると思うので、 文字列として取得してから「ユーザ名:パスワード@」を挿入し、URLを書き換える必要があります。