Testing Ansible Web App Roles with Selenium

Michael Lynch

SeleniumでAnsibleのWebアプリロールをテストする

概要

AnsibleはWebアプリのデプロイに非常に優れたツールです。Ansibleでは、Webアプリを構成するさまざまな「ロール」(例:Webサーバー、データベースサーバー、アプリケーションサーバー)という単位でWebアプリを定義できます。ロールやロール間の連携が複雑になるにつれて、Webアプリが正しくデプロイされていることを検証するために、それに見合ったより強力なテスト手法が必要になります。

前回の投稿では、Ansibleを使ってClipBucketという動画共有用のWebアプリをデプロイしました。その投稿では、Webアプリが正しくインストールされたことを確認する自動テストを含めましたが、これらのテストではアプリケーションの機能を十分に厳密には検証できていませんでした。

今回は、アプリの機能をより深く検証する、より強力な自動テストの方法を紹介します。そのために、SeleniumというWebブラウザ自動化ツールを使います。ここでもClipBucketのロールを例にしますが、ここで紹介する考え方はAnsibleでデプロイするあらゆるWebアプリに一般的に応用できます。

curlによる基本的なテスト

元のビルドスクリプトでは、最後のテストは次のようになっていました。

curl -s "${container_ip}"  \
  | grep "Login" && \
  (echo 'Landing page test: pass' && exit 0) || \
  (echo 'Landing page test: fail' && exit 1)

このテストは、Webアプリのいくつかの重要な特性を検証できるため有用です。具体的には次の点を確認できます。

  • Webサーバーがポート80でリッスンしていること
  • Webサーバーがユーザーのリクエストに応答していること
  • WebサーバーがClipBucketのランディングページを配信していること

なぜより優れたテストが必要なのか

元のテストでもいくつかの重要な確認はできましたが、Webアプリ本来の機能はほとんど検証できていません。そのため、Ansibleロールにバグを混入させてWebアプリが壊れてしまっても、基本的なテストでは検出できない可能性があります。

例えば、ClipBucketのPlaybookで重要なタスクを誤って削除してしまった場合を考えてみます。

diff --git a/tasks/main.yml b/tasks/main.yml
index 8542ffc..e9d42c0 100644
--- a/tasks/main.yml
+++ b/tasks/main.yml
@@ -29,15 +29,6 @@
     name: flvtool2
     state: present

-- name: create a symlink for ClipBucket to find ffmpeg and MP4Box
-  file:
-    path: "/usr/local/bin/{{ item }}"
-    src: "/usr/bin/{{ item }}"
-    state: link
-  with_items:
-    - ffmpeg
-    - MP4Box
-
 - name: install the Python MySQLB module
   pip: name=MySQL-python

この変更後のPlaybookでデプロイし、対象のサーバーにブラウザでアクセスしても、一見するとすべて正常に見えます。

ClipBucketでエラーが発生していない状態

すべてのインストールタスクは成功し、ClipBucketのランディングページも表示されます。では、何が問題なのでしょうか。

動画をアップロードしてみます。視聴しようとするまでは、すべて問題なく動作します。

動画エラーのスクリーンショット

Playbook内でffmpegへのシンボリックリンクを作成するタスクを削除してしまったため、ClipBucketは動画をストリーミング可能な形式にトランスコードできなくなっているのです。

この問題を自動テストで検出したいところですが、単純なコマンドラインツールで動画アップロードをスクリプト化するのは困難です。まずユーザーがログインする必要があり(つまりスクリプトがリクエスト間でCookieを管理しなければなりません)、さらにWeb UIを操作して動画をアップロードしなければなりません。これを一連のcurlコマンドで実現するのは非常に難しいでしょう。

幸い、Seleniumを使うことができます。Seleniumは、Webブラウザの操作をプログラムで実行できるWebテストツールです。

Seleniumのセットアップ

Seleniumを使い始めるには、Ansibleのコントロールマシンにいくつかのコンポーネントをインストールする必要があります。

  • Selenium Pythonパッケージ - Python APIを使用するため、このパッケージでSeleniumフレームワークとPythonバインディングを導入します。
  • Firefox - Seleniumが操作するためのブラウザが必要です。Seleniumは主要なブラウザのほとんどに対応していますが、Firefoxはネイティブにサポートされています。
  • xvfb - このテストは実際のディスプレイがないVM上で実行するため、仮想ディスプレイとしてxvfbを使用します。これにより、Firefoxはモニター上で動作していると認識できます。

このセットアップ用に、比較的シンプルなPlaybookを作成できます。少し手間取ったのは、xvfbが分かりにくいinitスクリプトを必要とする点だけでした。幸い、ブロガーのCory Klein氏が昨年、xvfbのinitスクリプトの例を紹介する投稿を書いており、その例を参考にして私たちの用途に合ったスクリプトを作成できました。

テストするWebフローの選定

Seleniumのインストールが完了したので、Webアプリを検証するSeleniumスクリプトを作成します。検証したいWebフローには、例えば次のようなものが考えられます。

  • ログイン
  • 動画をアップロードして再生する
  • 動画にコメントを投稿し、正しく表示されることを確認する
  • 新しいユーザーアカウントを作成する

ClipBucketでは特に動画が正しくアップロードされることを確認したいため、ログインと動画アップロードをテストする必要があります。しかし、ClipBucketはアップロードの管理に重量級のJavaScriptパッケージを使用しているため、通常のSeleniumのファイルアップロード用APIではうまく動作しません。

代替策として、ClipBucketのモジュール診断ページを利用します。このページにはインストールされているモジュールが一覧表示され、いずれかのモジュールが正しくインストールされていない場合はエラーメッセージが表示されます。

ClipBucketのモジュール一覧表示

このページを動画アップロードフローの代わりに使うことで、すべてのモジュールが正しくインストールされていることを確認できます。

検証したい機能が決まったので、自動化するWebフローを概略してみます。次のような流れになります。

  1. ClipBucketのURLを読み込む
  2. 管理者としてログインする
  3. モジュール診断ページに移動する
  4. すべてのモジュールがインストールされていることを確認する

これにより、基本的なテストでは検証できていなかった、以下のような追加機能を自動的に検証できるようになります。

  • ユーザーログインが機能していること(つまりClipBucketがデータベースに正常にアクセスできること)
  • ClipBucketが必要なツール群にアクセスできること

Webフローの自動化

Seleniumでブラウザ操作を自動化するには、まずどのURLをブラウザで読み込むかをSeleniumに指示し、次にページ上の要素をどのように見つけて操作するかを教える必要があります。ここでは、目的のWebフローに必要な操作をどのように自動化するかを見ていきます。

ログインの自動化

ログインするには、ログインページで認証情報の入力欄を見つけ、ユーザー名とパスワードを入力してから「Login」ボタンを押す必要があります。幸い、ユーザー名とパスワードの欄にはid属性があるため、ページ上で簡単に特定できます。「Login」ボタンにはid属性がありませんが、ページ内で一意なname属性loginがあるため、これを一意の識別子として利用できます。

ClipBucketのログインフィールド

次のコードスニペットでは、これらのフィールドを特定してログイン情報を入力しています。

self.get('/admin_area/login.php')

username_element = self._driver.find_element_by_id('username')
ui.WebDriverWait(
    self._driver, TIMEOUT).until(
    expected_conditions.visibility_of(username_element))
password_element = self._driver.find_element_by_id('password')

username_element.send_keys(username)
password_element.send_keys(password)
self._driver.find_element_by_name('login').click()

モジュール一覧のスクレイピング

インストール済みモジュールのページの確認方法は少し異なります。ページを操作する必要はなく、ページ上の要素を見つけて解釈するだけで済みます。すべてのモジュールが正しくインストールされていることを確認するには、各モジュールを示すボックスを特定し、そこにエラーメッセージが表示されているかどうかをプログラムで判定する必要があります。

対象の要素(およびDOM上の親要素)にid属性が一切ないため、少し工夫が必要です。これらはすべてclass="well"を持つ<div>なので、これが各モジュール情報ボックスを見つけるための最善の手がかりになります。

ボックスを見つけた後は、そのボックスがモジュールの正常なインストールを示しているのか、問題を示しているのかを判定する必要があります。成功を示す指標を探す方法と、失敗を示す指標がないことを確認する方法があります。前者の方がより厳密ですが、後者の方がコーディングはシンプルです。エラーメッセージを含むボックスには必ずclass="alert"属性を持つ要素が含まれているため、このクラスの子要素を持たないボックスを正常と判断できます。

ClipBucketのモジュールエラー表示

これは次のコードスニペットで実現できます。

self.get('/admin_area/cb_mod_check.php')

for module_element in self._driver.find_elements_by_class_name('well'):
    try:
        ui.WebDriverWait(
                self._driver, TIMEOUT).until(
                expected_conditions.visibility_of(module_element))
        alert_element = module_element.find_element_by_class_name(
            'alert')
        if alert_element:
            raise ClipBucketModuleError(alert_element.text)
    except exceptions.NoSuchElementException:
        # Lack of alert is good: the module is installed correctly.
        continue

TravisでのWebフローテストの統合

以上をまとめ、Seleniumのインストール用PlaybookとSeleniumスクリプトをビルドファイルに追加します。

次に、先ほどの不具合を含むコミットでGitHubのプルリクエストを作成します。Travisのビルドが実行されると、次のように失敗することが確認できます。

2016-09-21 00:17:06,229 clipbucket_driver INFO     Logging in with username=testadmin
2016-09-21 00:17:06,229 clipbucket_driver INFO     Loading url: http://172.17.0.2/admin_area/login.php
2016-09-21 00:17:07,525 clipbucket_driver INFO     Login complete
2016-09-21 00:17:07,526 clipbucket_driver INFO     Checking ClipBucket modules
2016-09-21 00:17:07,526 clipbucket_driver INFO     Loading url: http://172.17.0.2/admin_area/cb_mod_check.php
Traceback (most recent call last):
  File "tests/clipbucket_driver/main.py", line 36, in <module>
    main(parser.parse_args())
  File "tests/clipbucket_driver/main.py", line 24, in main
    driver.do_check_modules()
  File "/home/travis/build/mtlynch/ansible-role-clipbucket/tests/clipbucket_driver/clipbucket_driver.py", line 64, in do_check_modules
    raise ClipBucketModuleError(alert_element.text)
clipbucket_driver.ClipBucketModuleError: ffmpeg is not found

素晴らしい!新しいテストがAnsibleロールの不具合を正しく検出しています。

次に、通常のPlaybookを実行した場合にすべてが正常に動作することも確認してみます。

2016-09-21 01:58:21,992 clipbucket_driver INFO     Logging in with username=testadmin
2016-09-21 01:58:21,992 clipbucket_driver INFO     Loading url: http://172.17.0.2/admin_area/login.php
2016-09-21 01:58:23,036 clipbucket_driver INFO     Login complete
2016-09-21 01:58:23,039 clipbucket_driver INFO     Checking ClipBucket modules
2016-09-21 01:58:23,040 clipbucket_driver INFO     Loading url: http://172.17.0.2/admin_area/cb_mod_check.php
2016-09-21 01:58:23,724 clipbucket_driver INFO     Module check complete
2016-09-21 01:58:23,724 clipbucket_driver INFO     Exiting ClipBucket driver

Seleniumスクリプトは、必要なすべてのモジュールが正しくインストールされていることを正しく判定しています。今後、ログイン機能の失敗やモジュールのインストール不良を引き起こすような変更を加えた場合でも、この自動テストがそれを検出してくれます。

まとめ

今回は、AnsibleとSeleniumを組み合わせて、AnsibleロールがWebアプリを正しくデプロイし、デプロイされた環境が期待通りのユーザー操作をサポートしていることを検証しました。Seleniumは非常に柔軟性が高いため、さまざまなWebアプリケーションで多様なWeb UIフローをテストすることが可能です。

原文は Michael Lynch により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。