テスト

WebページのビジュアルデザインをSeleniumで自動テストするには?

Seleniumの起動画面

Webアプリケーションのデザインの確認って大変面倒だよね。

ブラウザ経由での自動テストを行うSelenium周りをいろいろとちゃんとやっていこうと整備し始めたのは、先日のBlog記事通り。

それで、Seleniumの基本的な使い方はあれこれ理解したが、これを、ブラウザ上のデザインのテストにも使えないかと模索を始めた。今回はJava/ScalaのWebフレームワークであるPlayFrameworkをベースに解説するが、Selenium周りはあまり変わらないのではと思う。

Webページ(ビュー)のテストには何があるのか

基本的にビューのテストには3種類あると思っている。

  • きちんと値が出力されているか
  • 正しい遷移が行えるか
  • きちんと表示がされているか

「きちんと値が出力されているか」「正しい遷移が行えるか」については、まあ普通にSeleniumを使えば出来るので割愛。

Seleniumの基本的な使い方

ここで、Seleniumのテストコードの書き方についておさらいしてみようと思う。Seleniumにはいろいろなテストコードの書き方が出来て、すごくオーソドックスなのは、HTMLを使って書く方法。これは、よく知られているからググればわかると思う。要するにテーブルを使ってテストケースのメソッドと値とかを書いていく。仕組みさえわかってしまえば、HTMLのテーブルを書くだけなので、デザイナーにも書けるところが魅力である。

個人的な考えでは、Seleniumのことをよく理解し、テストコードを思いつくくらいのプログラミングリテラシーがあるなら、コードで書くのも簡単なのではと思うが、実際はHTMLで書いた場合、人が見たときの確認のしやすさはあるんだろうと思う。どれくらいこの書き方が使われているかは知らない。

もう1つの方法としては、各言語別のラッパー的なライブラリを使う方法がある。PHPでは、PHPUnitにSeleniumTestCaseというのがあって、これを使うとコードを使って書く事が出来る。

public function testTitle() {
    $this->open('http://localhost:8808/');
    $this->assertTitle('Hello Selenium!');
    $this->assertElementContainsText('id=content', 'Hello');
}

こんな感じだ。この手法は少し間接的で、Selenium RCを一旦立ち上げておいた上で、コマンドラインなどからPHPを実行する。成功すると、コマンドラインで結果が表示される。さすがに、この方法はデザイナー向けではないかもしれない。プログラマーが最も使うのはこういうスタイルだと思う。

さて、今回のデザインのテストつまりは「きちんと表示がされているか」については、上記の方法ではなくPlayFrameworkのSeleniumテストを使って行う。お断りを入れておくと、別にPlayFrameworkでなければ出来ないとかそういうことでは全然ないので、一般的なSeleniumのデザインテストで使用可能な方法だと思う。

PlayFrameworkでのSeleniumを使うメリットはというと、

  • フレームワークに組み込まれていて環境構築が不要
  • フレームワークのテンプレートの書式がそのまま使える
  • テスト画面がグラフィカル

というのがある。

Seleniumのビューテストでは、Seleniumに用意されている変数をいろいろと使ったほうがいいように思う。

というのもデザインはいろいろな要素がめまぐるしく変化していくことが多く、テスト用の情報は一旦別で定義したほうがいいように思うからだ。

Seleniumの変数はこんな感じで定義できる

store(値, 変数名)

その他にも、属性値を変数に入れる命令storeAttributeなどいろいろある。これはSeleniumのドキュメントに詳しく載っている

[参照]Selenium Reference

レイアウトのテストを行う

さて、まずは要素の表示位置のテストを行ってみよう。

こんなデザインがあるとして、

テストするデザイン

これが段落ちしているかどうかのテストを行ってみる。ちなみに、左のボックスは「section-title」というID、右のボックスは「section-description」というIDがついている。

<div id="hello-section">
    <h2 id="section-title">Hello Selenium!</h2>
    <p id="section-description">Hello description!!</p>
</div>

Seleniumは、storeElementPositionTopというメソッドが存在していて、要素のtop位置を取得することが出来る。つまり、

#{selenium}
    open('/sites')
    // Test for layout variable
    storeElementPositionTop('id=section-title', GGG)
    assertAttribute('section-description@data', javascript{storedVars["GGG"]})
#{/selenium}

を行うと、テストが行える。ここで行っているのは#section-titleのtop位置と#section-descriptionのtop位置を取得して同じならテストをパスするというものだ。

これらは、Seleniumの用意されているメソッドを使う事でテストが可能だが、もっと細かいデザインのテストを行うには不足だ。
ということで、僕はJavaScript(jQuery)とdata属性(べつにdata属性でなくてもいいけど)、storeAttributeメソッドを使ったテストを推奨してみる。storeAttributeというのは、要素の属性の値を取得して変数に入れる事ができるメソッドだ。

まずは、先ほどのテストを書き直してみよう。
テスト用のJavaScriptを作成する。これは、jQueryを使って、対象のタグのdata属性に調べたいプロパティの値を挿入している。

// テスト用JavaScript
$(function() {
    $('#section-title').attr('data', $('#section-title').offset().top);
    $('#section-description').attr('data', $('#section-description').offset().top);
});

そしてこのJavaScriptはファイルで保存し、テストモードで起動しているときだけ読み込むようにする。こうすることで、既存の環境やHTMLには手を加えないでテスト対応を行う検査が可能になる。PlayFrameworkでならこんな具合だ

#{if play.Play.id == 'test' }
<script src="@{'/public/javascripts/vaqum_test.js'}" type="text/javascript" charset="${_response_encoding}"></script>
#{/if}

テストモードで起動中のサイトを確認すると、目的のh2タグやpタグにdata属性にtopのプロパティ値が入っているのがわかるはずだ。
これを、Selenium上で使う。

書き方としてはこんな感じ

#{selenium}
    open('/sites')
    // Test for layout variable
    storeAttribute('section-title@data', VAQUM)
    assertAttribute('section-title@data', javascript{storedVars["VAQUM"]})
    assertAttribute('section-description@data', javascript{storedVars["VAQUM"]})
#{/selenium}

Seleniumでは、変数を${VAQUM}のように書く事もできるが、PlayFramework上ではテンプレート言語とバッティングしてしまうため、上記のように記述。
storeElementPositionTopみたいな組み込みメソッドを使わないことで、CSSのプロパティが入ったdata属性をstoreAttributeで取得することが出来、それを使ってテストを実行することが出来る。

テスト成功

試しに、失敗するようにpaddingの値とかを変えてみよう。
すると、このように段落ちしてしまう。

段落ちしたデザイン

その後、テストケースを実行すると、赤い表示が出てエラーであることが確認された。

どうもテストが失敗したっぽい

どういう感じでプロパティの値がずれているかも出るので、非常にわかりやすい。

やはり、テストが失敗した

topの位置が下になっていることがわかる

上記の方法を使う事で、例えばテーブルの行の色を1行1行入れ違いにするデザインのテストなんかも行える。
ちょっとやってみよう。

テーブルのデザインテスト

一列ごとに色が変わるテーブル

上記表示になる、こんなHTMLがあったとする

<table id="sample-table">
    <tr>
        <td>JavaScript</td>
        <td>Dart</td>
    </tr>
    <tr>
        <td>Java</td>
        <td>Scala</td>
    </tr>
    <tr>
        <td>XHTML</td>
        <td>HTML5</td>
    </tr>
</table>

これは、

$(function() {
    $('#sample-table').each(function(){
        $(this).find('tr:odd').addClass('odd');
        $(this).find('tr:even').addClass('even');
    });
});

こんなJavaScript(jQuery)によって交互に色分けされる。ここで、class属性を見てきちんとclassが出力されているかのテストを行ってもいいけど、CSSの指定がおかしいせいで、クラスは正しいのに表示がされないという可能性が考えられる。

そこで、

// テスト用JavaScript
$(function() {
    $('#sample-table').each(function(){
        $(this).find('tr:odd>td').attr('data', $.fmtColor($(this).find('tr:odd>td').css('background-color')));
        $(this).find('tr:even>td').attr('data', $.fmtColor($(this).find('tr:even>td').css('background-color')));
    });
});

こんなテスト用のJavaScriptを用意して、data要素にbackground-colorを書き込んでみる。
色の値をテストするので、jquery.color.utilsのような色変換のjQueryもテストモードのみで読み込んでおこう。
Seleniumのテストコードは、下記の通り。

#{selenium}
    open('/sites')
    // Test for table sell color
    store('#333333', TABLE_COLOR_ODD)
    store('#000000', TABLE_COLOR_EVEN)
    assertAttribute('xpath=//table[@id='sample-table']//tr[1]/td[1]/@data', javascript{storedVars["TABLE_COLOR_EVEN"]})
    assertAttribute('xpath=//table[@id='sample-table']//tr[2]/td[1]/@data', javascript{storedVars["TABLE_COLOR_ODD"]})
#{/selenium}

これがパスすると、まあ色はちゃんと交互に出ているよねみたいな確認が行える。

これらビジュアルに関わるテストというのは、大半が目視でやっているのが現状だ。今後ページ数がどんどん増えるサイトや、いろいろな画面遷移のパターンがある複雑なWebアプリケーションは、目視テストだけでも相当大変だと思う。それに、1日に1回テストを行う事で、デザインの崩れを早期に発見できるだろう。目視テストは完全になくすことは出来ないが、かなりの部分のテストをこういった方法のデザインテストでまかなえるのではないだろうか。

今後の課題

最後に今後の課題を挙げておく。

  • テスト用の一時保持属性はdata属性でいいのかどうか? 他の要素に別でマークアップして書き出すという手法も考えられなくないが、状態の変化を確実につかむには、対象の要素それ自体に記述するのがいいだろうと思う。style属性でもいいと思う。
  • Selenium側からjQueryを直接使って対象のスタイルにアクセス可能かどうか? これもまだ未調査。できるなら、それのほうがスマートかも。
  • 多分テストしやすいHTMLコードとしにくいHTMLコードがあるはず。どういう部分がポイントだろう?
Read More

[Play!][CakePHP]Selenium周りを整備する

Seleniumに関しては、前々からちょこちょこと意識していたものの、本格的に使っていこうと思い出したのはPlayFrameworkを使い始めてから。

Seleniumが完全に統合されており、かっこよすぎる

というのも、PlayFrameworkではデフォルトでSeleniumによるテスト環境が整備されており、ちょちょいのちょいで始める事が出来る。この偉大な環境に僕は心酔し、Seleniumの世界へのめりこむこととなった。僕は元々フロント周りに強い人であったので、フロント周りのテストにも強い人として更なるパワーアップに期待したいところである。

ということで、PlayFrameworkに関して言えば、インストール時に既にSelenium周りが整備されているので言う事はない。

問題はもう1つの溺愛フレームワークであるCakePHPのほうだ。
CakePHPは、現実案件が1.3系で進んでいることもあり、1.3ベースでの整備を考えていきたい。

ググった限りではSeleniumを使ったSimpleTestの話題はあまり存在せず、PHPUnitベースのものが普及しているようだった。
CakePHPは2.0からPHPUnitベースのテストとなるので、このあたりでPlayFrameworkのようなエレガントなユニットからWebテストまでをオートテスティングできるような統合がされて欲しいと思っている(っていうかもうされている?よく追えてない)

さて、CakePHPでSeleniumという場合、

http://cakebaker.42dh.com/downloads/

にて、Selenium Helperとかそういうものが配布されており、これに関する言及もいろいろなところでされているが、いかんせんネタ自体が2006年や2007年のものが多くて不安になる。

CakePHPに限らずSeleniumをPHP環境で使う場合は、Selenium IDEを使ったHTMLベースのテストをすると、何の障害もなく進む。

そうではないPHPUnitを使った場合でも、まずSelenium RCサーバーをセットアップして起動を行い(Selenium RCのセットアップはググるといろいろ出てくるので割愛)、

$ java -jar ~/selenium/selenium-server-standalone-2.11.0.jar

コマンド上からPHPUnitを起動する(PHPUnitのセットアップはググるといろいろ出てくるので割愛)ことで、実現ができる。

$ phpunit --verbose /var/www/htdocs/hoge/app/tests/selenium/myapp.php

上記のPHPコードは、こんな感じだ。

<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase {
    protected function setUp() {
        $this->setBrowser('*firefox');
        $this->setBrowserUrl('http://localhost:8808/');
    }

    public function testText() {
        $this->open('http://localhost:8808/users/');
        $this->assertElementContainsText('id=content', 'Hello');
    }
}
?>

このコードで行っていることは、通常のSeleniumのテストのコマンドライン版である下記

$ java -jar ~/selenium/selenium-server-standalone-2.11.0.jar -htmlSuite "*firefox" "http://localhost:8808/" "/var/www/htdocs/selenium_sample/app/tests/selenium/myapp.html" "/Applications/MAMP/htdocs/selenium_sample/app/tests/selenium/myapp_result.html"

の引数などをPHPから扱えるインターフェースにして、テストスイートを書く必要がなく、簡潔なコードでアサーションを書けるということである。

これをCakePHPに組み込む場合、まずテストケースをCakePHPのテスト関係が入っているフォルダに入れるというのが考えられる。続けて行う事としては、他のテストが実行される段階で同時に上記コマンドを実行するようにしてテストを走らせるか、もしくはクラスを使ってテストケース内で実行することにするかということになる。

個人的には、コマンドラインから走らせて結果がわかるというだけでも十分いいんだけどね。
PlayFrameworkのエレガントさを見てしまうと、CakePHPでもああいう感じで一気貫通で行いたいとか思ってしまったりなのだ。

※OSXでSelenium RCを動かしていると、Seleniumを終了させた後でサーバーであるJettyが終わらずに残ってしまうことがあり、ここはちょっと困ったなあという感じ。

Read More