WebDriverを統合したSelenium 2を使ってみる

Seleniumとは

Seleniumとは、Webアプリケーションのテストを自動化するためのフレームワークです。Seleniumが提供するコマンドやAPIを用いることで、実際にWebブラウザを動かしながらWebアプリケーションの動作を検証することができます。これにより、従来手動で行っていたWebアプリケーションにおける回帰テストの多くを自動化することが可能になります。

Selenium 2(Selenium WebDriver)とは

Selenium 2は、先月の8日に正式版がリリースされた、Seleniumプロジェクトの新しいプロダクトです。Selenium 2の最大の特徴は、やはりWebDriverとの統合でしょう。これについては、WebDriverの開発者であるSimon Stewart氏の話も含めた詳しい内容が以下の記事に載っているので、そちらを参照してください。
Selenium 2 (別名 Selenium WebDriver) がリリース
この統合により、WebDriver APIを用いたテストが作成できるようになりました。今回は、このWebDriver APIを使ってテストを作成してみます。WebDriver APIは、JavaPythonRubyC#による実装がありますが、今回はJavaからWebDriver APIを使ってみます。

準備

Selenium 2を利用するためには、Selenium 2のライブラリを参照できるようにする必要があります。Mavenを利用している場合は、pom.xmldependenciesに以下を追加するだけです(x には最新バージョンの番号を入れてください)。

<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>2.x.0</version>
</dependency>

Mavenを利用しない場合は、以下のURLにアクセスして、selenium-java-2.x.0.zipをダウンロードしてきます。
Downloads - selenium - Browser automation framework - Google Project Hosting
ダウンロードしたものを解凍し、その中にあるselenium-java-2.x.0.jarと、libsフォルダ以下のjarファイルを全てクラスパスに追加します。

動かしてみる

まずは簡単なプログラムを動かしてみます。以下のようなクラスを作成します。

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class GoogleTest {

    public static void main(String[] args) {
        WebDriver driver = new FirefoxDriver();
        driver.get("http://www.google.co.jp/");
        driver.quit();
    }

}

プログラムを実行すると、Firefoxが起動してGoogleのトップページが表示されます。
FirefoxDriverというのが、Firefoxを動かすためのWebDriverの実装クラスです。FirefoxDriver以外にも、以下のようなDriverが提供されています。

ほとんどが直接ブラウザを動かすものですが、実際のWebブラウザを起動せずにWebブラウザの動きをシミュレートするHtmlUnitDriverというものもあります。Webブラウザを起動する必要がないため、他のDriverよりも実行速度が早いという特徴があります。内部ではRhinoを使っているようです。
ここでは、FirefoxDriverインスタンスを生成することでWebブラウザを起動し、そのインスタンスgetメソッドを呼び出すことで、Googleのトップページを開いています。getメソッドは、引数で指定したURLに対してGETリクエストを送ります。トップページを開いた後は、quitメソッドでWebブラウザを閉じています。このように、ごく簡単な記述で、ブラウザの起動から、操作、終了までを制御することができます。
次は、Googleのトップページを開いた後に、「selenium」というキーワードでの検索を行ってみます。

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;

public class GoogleTest {

    private static WebDriver driver;

    public static void main(String[] args) {
        driver = new FirefoxDriver();
        driver.get("http://www.google.co.jp/");
        searchFor("selenium");
        driver.quit();
    }

    private static void searchFor(String keyword) {
        WebElement searchBox = driver.findElement(By.name("q"));
        searchBox.sendKeys(keyword);
        searchBox.sendKeys("\n");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) { /* ignore */ }
    }

}

新たにsearchForというメソッドを作成しました。このメソッドは、引数に指定されたキーワードを用いてGoogle検索を行うためのメソッドです。プログラムを実行すると、「selenium」というキーワードが検索ボックスに入力され、インスタント検索による検索結果が表示されます。
searchForメソッドの中では、検索ボックスの要素を探し、その要素に対して入力キー情報を送るといった処理をしています。Thread.sleep()を行っているのは、インスタント検索によるAjax通信および検索結果のレンダリングを待機するためです。もしも待機せずに、検索結果からさらに別の要素を探したりしようとすると、要素が見つからないといった例外が発生してしまう場合があります。

JUnitと組み合わせる

実際に自動テストを実現するためには、Webブラウザを動かすだけでなく、動作結果を検証する必要があります。検証には、JUnitTestNGなどのテスティングフレームワークを用いることができます。今回は、JUnitと組み合わせてテストを作成してみます。
先程のクラスを以下のように書き換えます。

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;

public class GoogleTest {

    private static WebDriver driver;

    @Test
    public void searchForSeleniumWebsite() throws Exception {
        driver = new FirefoxDriver();
        driver.get("http://www.google.co.jp/");
        searchFor("selenium");
        assertThat(driver.getTitle(), is("selenium - Google 検索"));
        driver.quit();
    }

    private static void searchFor(String keyword) throws InterruptedException {
        WebElement searchBox = driver.findElement(By.name("q"));
        searchBox.sendKeys(keyword);
        searchBox.sendKeys("\n");
        Thread.sleep(3000);
    }

}

基本的な構成はそのままに、JUnit 4形式のテストクラスへと書き換えました。
@Testアノテーションが付いたsearchForSeleniumWebsiteメソッドが、検索後の画面遷移を検証するためのテストメソッドです。ここでは、検索後に正しく画面が遷移していることを、ページのタイトルを見て確認しています。assertThatメソッドを呼び出しているのがその部分です。
このテストの問題点として、テストメソッドが増えた際に、メソッド毎に毎回ブラウザを起動してしまうという点があります。動作としては問題ないのですが、テストメソッドが増えるごとに時間が余計にかかってしまいます。また、テストが途中で失敗した場合に、ブラウザが起動しっぱなしになってしまうという問題もあります。これらの問題を解決するためには、以下のような書き換えを行います。

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;

public class GoogleTest {

    private static WebDriver driver;

    @BeforeClass
    public static void setUpBeforeClass() {
        driver = new FirefoxDriver();
    }

    @AfterClass
    public static void tearDownAfterClass() {
        driver.quit();
    }

    @Test
    public void searchForSeleniumWebsite() throws Exception {
        driver.get("http://www.google.co.jp/");
        searchFor("selenium");
        assertThat(driver.getTitle(), is("selenium - Google 検索"));
    }

    private static void searchFor(String keyword) throws InterruptedException {
        WebElement searchBox = driver.findElement(By.name("q"));
        searchBox.sendKeys(keyword);
        searchBox.sendKeys("\n");
        Thread.sleep(3000);
    }

}

JUnit 4の@BeforeClass@AfterClassアノテーションを使って、テストの実行前と実行後にWebブラウザの起動と終了の処理を入れています。実行前にFirefoxDriverインスタンスを生成し、それを全てのテストメソッドで使い回せる形にしています。テスト終了後は、テストの成功・失敗に関わらず、Webブラウザは正しく終了されます。

おわりに

今回はSelenium 2の基本的な部分を使ってみましたが、他にも色々な機能があります。詳しくは参考資料を参照してください。
洗練されたWebDriver APIを用いることで、可読性の高いWebブラウザベースでの自動テストを簡単に作成できるようになりました。これからプロジェクトで自動テストを導入しようと考えてる方は、是非一度試してみてください。

Selenium - Web Browser Automation

余談

headlessなWebブラウザを用いたテストとの比較ですが、Seleniumを用いたテストでは実際のWebブラウザを動かすため、headlessブラウザより正確な動作結果を示すでしょう(仕様に忠実という意味ではなく)。また、Seleniumは数多くのプラットフォームをサポートしています。そのため、OSやWebブラウザごとの挙動の差異や、動作速度の検証にも用いることができます。
ただ、実際のWebブラウザを起動する分、テスト内容に直接関係が無い部分で時間がかかってしまうというデメリットもあります。このあたりは一長一短というところでしょうか。うまく使い分けていけたらいいですね。