Kensuke Kousaka's Blog

Notes for Developing Software, Service.

SeleniumとPhantomJS,noseを用いたWeb UIテスト

WebのUIテストツールであるSeleniumとヘッドレスな(ブラウザ画面が無い)ブラウザであるPhantomJS,これらとPythonユニットテストライブラリであるnoseを用いることで,WebアプリのUIテストを簡単に行える.

Selenium,PhantomJSのインストール

以下のコマンドで,pipからseleniumをインストールする.

$ pip install selenium

また,PhantomJSは使用するOSごとのパッケージシステムを用いてインストールする.以下はMacでHomebrewを用いる場合.

$ brew install phantomjs

nose,Selenium,PhantomJSによるFlask WebアプリのUIテスト

テスト対象となるWebアプリには,PythonベースのWebアプリケーションフレームワークであるFlaskを用いて開発しているHelloFlaskを利用する.このアプリはapp.pyから起動し,http://127.0.0.1:8080/にアクセスするとログイン画面が表示される.右上のSign inボタンからログインモーダルを表示させてユーザ名にadmin,パスワードに適当なものを入力してモーダル上のSign inボタンをクリックすることでログインできる.ログイン後,テキスト入力フォームで適当な英語大文字の文字列を入力してその下のChange textボタンを押すことで,入力された文字列を小文字に変換したものが表示される,というサンプルになっている.

さて,今回はこれらの動作が正しく行えているのかをUIの側からテストする. ディレクトリ構造は以下のようになる.

- HelloFlask/
 - FlaskApp/
   - app.py
   - static/
     - css/
       - sample.css
     - js/
       - sample.js
   - templates/
     - index.html
     - login.html
 - Tests/
   - test_access.py
   - test_ui.py

このうち,test_ui.pyを除く全てのファイルは以前までのFlaskの記事で作成している. ただ,今回のUIテストコードを書くにあたってHTMLの一部要素について要素IDの追加を行っているため,UIテストを試す場合は本記事末尾に載せるGitHubリポジトリのリンクからプログラムをダウンロードしてほしい.

これ以降で説明するUIテストコードは,test_ui.pyに書いていく.

UIテストに用いるライブラリのインポート

テストコードを書いていく前に,まずはUIテストに用いるPythonライブラリのインポートを以下のように書く.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
from nose.tools import eq_

このコードで,SeleniumのWebドライバーとPythonユニットテストライブラリであるnoseをインポートしている.

ルートURLへのアクセス

まずは,アプリケーションのルートURLにアクセスし,ログインページに正常にリダイレクトされるかの確認をする. そのためのコードは以下のようになる.

def test_access():
  driver = webdriver.PhantomJS('/PATH/TO/phantomjs')
  wait = WebDriverWait(driver, 5)

  driver.get('http://127.0.0.1:8080/')
  wait.until(ec.presence_of_all_elements_located)
  eq_('http://127.0.0.1:8080/login', driver.current_url)
  driver.close()

まず,用いるWebドライバーとしてPhantomJSを指定し,準備をする.その後driver.get('http://127.0.0.1:8080/')でルートURLにアクセスする.全ての要素が配置された後eq_('http://127.0.0.1:8080/login', driver.current_url)でWebドライバーの現在のURLがhttp://127.0.0.1:8080/login,つまりリダイレクト後のログインページになっているかを確認している.

ログイン処理(失敗)

Sign inボタンをクリックしてログインモーダルを表示し,わざとログインに失敗するIDとパスワードを入力してログインを試行することで,認証処理がきちんと行われてログインページに再びリダイレクトされるかを確認する.

def test_fail_login():
    driver = webdriver.PhantomJS('/usr/local/bin/phantomjs')
    wait = WebDriverWait(driver, 5)

    driver.get('http://127.0.0.1:8080/')
    wait.until(ec.presence_of_all_elements_located)
    eq_('http://127.0.0.1:8080/login', driver.current_url)

    show_signin = driver.find_element_by_id('showSignIn')
    show_signin.click()

    wait.until(ec.visibility_of_element_located((By.ID, 'username')))
    username = driver.find_element_by_id('username')
    username.send_keys('test')

    wait.until(ec.visibility_of_element_located((By.ID, 'password')))
    password = driver.find_element_by_id('password')
    password.send_keys('test')

    wait.until(ec.visibility_of_element_located((By.ID, 'signIn')))
    signin = driver.find_element_by_id('signIn')
    signin.click()

    wait.until(ec.presence_of_all_elements_located)
    eq_('http://127.0.0.1:8080/login', driver.current_url)
    driver.close()

show_signin = driver.find_element_by_id('showSignIn')Sign inボタンを取得し,show_signin.click()でこれをクリックしてモーダルを表示させている.ユーザ名の入力フォームが見えるようになるまで待った後,フォームにsend_keys('test)でテキストを入力している.同様にパスワードも入力し,モーダル上のSign inボタンをクリックしてログインを試行している.

認証処理が正常に機能しているなら,このログインは失敗しログインページにリダイレクトされるので,最後にこれをテストしている.

ログイン処理(成功)

今度は,ログインに成功するIDとパスワードを入力して,正常にログインしルートURLにアクセスできるかを確認する.

def test_success_login():
    driver = webdriver.PhantomJS('/usr/local/bin/phantomjs')
    wait = WebDriverWait(driver, 5)

    driver.get('http://127.0.0.1:8080/')
    wait.until(ec.presence_of_all_elements_located)
    eq_('http://127.0.0.1:8080/login', driver.current_url)

    show_signin = driver.find_element_by_id('showSignIn')
    show_signin.click()

    wait.until(ec.visibility_of_element_located((By.ID, 'username')))
    username = driver.find_element_by_id('username')
    username.send_keys('admin')

    wait.until(ec.visibility_of_element_located((By.ID, 'password')))
    password = driver.find_element_by_id('password')
    password.send_keys('test')

    wait.until(ec.visibility_of_element_located((By.ID, 'signIn')))
    signin = driver.find_element_by_id('signIn')
    signin.click()

    wait.until(ec.presence_of_all_elements_located)
    eq_('http://127.0.0.1:8080/', driver.current_url)

    logout = driver.find_element_by_id('logout')
    logout.click()

    wait.until(ec.presence_of_all_elements_located)
    eq_('http://127.0.0.1:8080/login', driver.current_url)
    driver.close()

この場合ではログインに成功するIDとパスワードを入力しているため,モーダル上のSign inボタンを押した後はログインページではなくルートURLへと移動するはずなので,これをテストしている.また,URLの確認をした後にログアウト処理が正常に機能しているかの確認もしている.

大文字から小文字への変換処理

本アプリの機能である,テキスト入力フォームで入力された英語の文字列を小文字に変換するというものが機能しているかをテストする.

def test_lower_conversion():
    driver = webdriver.PhantomJS('/usr/local/bin/phantomjs')
    wait = WebDriverWait(driver, 5)

    driver.get('http://127.0.0.1:8080/')
    wait.until(ec.presence_of_all_elements_located)
    eq_('http://127.0.0.1:8080/login', driver.current_url)

    show_signin = driver.find_element_by_id('showSignIn')
    show_signin.click()

    wait.until(ec.visibility_of_element_located((By.ID, 'username')))
    username = driver.find_element_by_id('username')
    username.send_keys('admin')

    wait.until(ec.visibility_of_element_located((By.ID, 'password')))
    password = driver.find_element_by_id('password')
    password.send_keys('test')

    wait.until(ec.visibility_of_element_located((By.ID, 'signIn')))
    signin = driver.find_element_by_id('signIn')
    signin.click()

    wait.until(ec.presence_of_all_elements_located)
    eq_('http://127.0.0.1:8080/', driver.current_url)

    input_text = driver.find_element_by_id('input-text')
    input_text.send_keys('TEST')

    change_text = driver.find_element_by_id('button')
    change_text.click()

    wait.until(ec.presence_of_all_elements_located)
    hello = driver.find_element_by_id('hello')
    eq_('test', hello.text)

    logout = driver.find_element_by_id('logout')
    logout.click()

    wait.until(ec.presence_of_all_elements_located)
    eq_('http://127.0.0.1:8080/login', driver.current_url)
    driver.close()

ログインした後,テキスト入力フォームにTESTと入力してChange textボタンをクリックし,testと変換されているかの確認をしている.

UIテスト

実装したテストコードを用いて実際にテストを行う前に,テスト対象のアプリケーションを動かさないといけない. FlaskAppディレクトリまで移動した上で以下のコマンドを実行して,Flaskアプリケーションを起動する.

$ python app.py

起動が確認できたら,プロジェクトのルートディレクトリ(HelloFlask)まで移動して,以下のコマンドでテストを行う.

$ nosetests

実行すると,自動的にテストが実行されていく.UIテストでは要素の表示を待つ処理などがあるため多少の時間がかかる.全てのテストをパスすれば,OKという出力が最後にあるはずだ.テストでコケた場合は,その内容が出力される.

テストコードについて一応の説明をしてみたが,コードを見れば説明が不要なくらい実装が簡単であることがわかると思う.

このアプリはGitHubに公開しているので,興味があれば是非手を動かして試してほしい.