システムの開発とか他人の作ったシステムの運用をする中で、サービスの外面だけでも動いているかどうかが常にチェックされているとすごく精神的に安定することがわかってきたので、受け入れテスト的なレベルのテストコードはとりあえずでも積極的に書いていきたいわけです。
1年くらい前に書いたWebアプリケーションを引っ張り出さなければならないことがあったわけですが、このときSeleniumでブラウザを使った自動テストを書いていたおかげで、自動テストで動くChromeの画面を眺めて「ああ、そうそう、こういう操作感のUIだったわ」みたいに思い出せたりします。何よりコードとして自動化されていれば、いつでも動かしておけて、それで安心した分睡眠の質が高まることが期待できます。多分。
そんな中で、ここ最近運用に加わったサービスのテスト、まだあんまり書けてないじゃんと気づいて、JavaかつMavenでSeleniumのテストプロジェクト作るにはどうしてたっけ?と改めてやってみたので備忘録的に。
やりたいこと
- Seleniumを使って既存のサービスをテストするようなプログラムを作りたい
- 言語は、以前Seleniumのコードを書いたという理由でJava
- 対象システムのソースコードはあるにはあるが、PythonだったりPHPだったりいろいろ
- 今回の作業自体はローカルのWindows環境だが、お仕事環境のUbuntuに置いてあるJenkinsさんにお願いできるように
- Mavenみたいな標準化されたやり方をとる
- できるだけコード内に環境依存なことは書かないように
環境
- Apache Maven 3.1.1
- Eclipse (Kepler)
手順
Mavenプロジェクトの作成
まず、Mavenの archetype:generate を使ってプロジェクトを作る。今回は適当に maven-archetype-quickstart をベースに作成。
C:\workspace> mvn archetype:generate
...
Choose a number or apply filter ...: maven-archetype-quickstart
Choose archetype:
1: remote -> org.apache.maven.archetypes:maven-archetype-quickstart (An archetype which contains a sample Maven project.)
Choose a number or apply filter ...: 1
Choose org.apache.maven.archetypes:maven-archetype-quickstart version:
...
6: 1.1
Choose a number: 6
Define value for property 'groupId': : net.yzwlab.test
Define value for property 'artifactId': : service-test
Define value for property 'version': 1.0-SNAPSHOT: : 0.1.0
Define value for property 'package': ...: (何も入力せずENTER)
これでservice-testプロジェクトが作成される。
そのあと、SeleniumHQのMaven Informationを参考に、生成されたプロジェクトの pom.xml の dependencies を以下のように修正。
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>2.41.0</version>
<scope>test</scope>
</dependency>
</dependencies>
これでpomの準備は完了。Eclipseプロジェクトを作る。
C:\workspace\service-test> mvn eclipse:eclipse
これで関連jarなどがダウンロードされ、.project, .classpath などができる。あとはEclipseでプロジェクトとして読み込み。(m2eは面倒なことになってしまった記憶しかなくて、あくまでもEclipseはエディタ的に使っているだけ・・・)
テストコードの記述
Seleniumでは、FirefoxやChromeなど自動テストに使いたいブラウザに応じて org.openqa.selenium.WebDriver の実装を生成することで、そいつに対してURLを指定してページを開かせたり、ブラウザで表示中のDOMから適当な要素を抜き出したり、その要素に対してクリックしたりなどの操作ができる。
src/test/java以下に適当に、ログイン画面を開いて、ログインしてそこから別のページにジャンプして、スクリーンショットを撮る、みたいなコードを書いてみる。
package net.yzwlab.test;
import java.io.File;
import java.util.ArrayList;
import junit.framework.TestCase;
import org.apache.commons.io.FileUtils;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class WebTest extends TestCase {
public interface WebDriverFactory {
public WebDriver create();
}
public static Iterable<WebDriverFactory> getDriverFactories() {
ArrayList<WebDriverFactory> factories = new ArrayList<WebDriverFactory>();
factories.add(new WebDriverFactory() {
@Override
public WebDriver create() {
return new FirefoxDriver();
}
});
factories.add(new WebDriverFactory() {
@Override
public WebDriver create() {
return new ChromeDriver();
}
});
return factories;
}
// おためし:コードに直接書くべきではない・・・
private static final String url = "https://...";
private static final String id = "hogehoge";
private static final String password = "fugafuga";
@Test
public void testXXX() throws Exception {
for (WebDriverFactory factory : getDriverFactories()) {
WebDriver driver = factory.create();
try {
driver.get(url);
Wait<WebDriver> wait = new WebDriverWait(driver, 30);
// Login page
WebElement button = wait.until(ExpectedConditions
.elementToBeClickable(By.className("btn-primary")));
assertTrue(driver.getTitle().contains("YYY Dashboard"));
assertTrue(driver.getTitle().contains("Login")
|| driver.getTitle().contains("ログイン"));
driver.findElement(By.id("id_username")).sendKeys(id);
driver.findElement(By.id("id_password")).sendKeys(password);
button.submit();
// Top page
WebElement linkElement = wait
.until(ExpectedConditions.elementToBeClickable(By
.xpath("//a[@href='/path/to/xxx']")));
linkElement.sendKeys(Keys.ENTER);
// XXX page
wait.until(ExpectedConditions.visibilityOfElementLocated(By
.id("xxx")));
assertTrue(driver.getTitle().contains("YYY Dashboard"));
assertTrue(driver.getTitle().contains("XXX"));
FileUtils.copyFile(((TakesScreenshot) driver)
.getScreenshotAs(OutputType.FILE), new File(driver
.getClass().getName() + "-xxx.png"));
} finally {
driver.quit();
}
}
}
}
各ページではTitleの文字列をチェックしている。あと、ひとつのテストに対して、複数のWebDriverを使いたいと思いつつ、なんか org.junit.runners.Parameterized とかでやればいいのかと思ったけれどJUnit力がいまいち足りなくてすっきりまとまらなかった。
WebDriverの準備
このテストを動かすためには、WebDriverの実装クラスが実際にブラウザを起動、制御できるように実行ファイル関係の設定をしてあげる必要がある。適宜SeleniumHQの Downloads を参照。
FirefoxDriver
WindowsならFirefoxをインストールして、firefox.exeがあるフォルダのパスを環境変数Pathに追加しておいてあげればよい。
ChromeDriver
Chrome自体とは別に、chromedriverという実行ファイルが必要。詳細は Selenium Chrome Driver に説明がある。
このバイナリは chromedriver.storage.googleapis.com/index.html からダウンロードできる。適当なバージョンのWindows向けZIPをダウンロードし、その中のchromedriver.exeを適当なフォルダに展開しておく。
なお、実行時にシステムプロパティ webdriver.chrome.driver としてこのchromedriver.exeへのパスを指定してあげる必要がある。
Mavenでテスト時にシステムプロパティを与える方法としては、pom.xmlに書く方法と、mvnコマンド実行時に引数で与える方法があるようだが、実行環境によって値を変更したいので、mvnコマンド実行時にシステムプロパティwebdriver.chrome.driverを与えることにする。
InternetE(うっ頭)Driver
さいわい自分の作っているサービスはIE対象外とさせていただけているものが多くて、保留。試したらまた書く。
実行
こんな感じ。
C:\workspace\service-test> mvn -DargLine="-Dwebdriver.chrome.driver=C:\chromedriver\chromedriver.exe" test
こんな感じで実行すると、勝手にブラウザが立ち上がってくれて、勝手にURLを参照して、勝手にボタンがクリックできるようになるまで待って、勝手にテキストフィールド入力、ボタンクリック、スクリーンショット保存などしてくれる。
それぞれの段階で期待する値や構造になっているのかをassertでチェックすれば、動きが変わってないことをかんたんに確認することができ、睡眠の質が上がる。多分。