以前からSelenium WebDriverを使ってWebアプリケーションのテストを行なっているんですが、ずっとつきまとっていたのがスローテスト問題(テストに時間がかかる!)。
スローテスト問題は、テストの並列化やテスト数の削減などの解決策とともに随所で議論されてきたテーマですが、Selenium WebDriverを使っていて特に私が気になっていたのは、ブラウザ(Firefox)の起動時間でした。ローカルで一度テストが通るのを確認するのに、毎回数秒(7秒くらい)待たされるのは、精神衛生上よくないですよね。これをなんとかするべく、色々悩んできましたが、ようやくいい方法が見つかったので、紹介したいと思います。
なお、ブラウザの起動も含めSeleniumテストの効率化の取り組みについては、サイボウズLiveのSeleniumテスト並列化(Slideshare)がすばらしいまとめになっています。参考にさせていただきました。
TestSuiteとClassRuleを使う
使ったのは、TestSuiteとClassRuleを使う方法です。
今回対象にしているアプリケーションのテストでは、Seleniumを使うテストクラスが20くらいあり、それぞれのテストクラスに@BeforeClassをつけた初期化メソッドが定義されていて、その中でWebDriver(Firefox)を起動するような作りになっています。
この場合、全てのテストを流すと、合計で20回ブラウザの起動・停止が行われることになります(起動・停止に合計8秒かかるとすると、合計で160秒)。本来であれば、ブラウザの起動・停止は1回で十分なはずなので、起動・停止を1回にすることで、計算上152秒のテスト時間の短縮が見込めるわけです。
@ClassRuleを使うことで、テストメソッドではなくテストクラス単位で前後処理を定義できますが、この@ClassRuleは、テストスイートにも利用することができるのがポイント。WebDriverの起動・停止を行うためのWebDriverRule(後述)を定義し、以下のようなテストスイートを定義することで、FirstTestの前にブラウザを起動し、SecondTestが終わったらブラウザを終了する、といった動作が可能になります。
package sample; import org.junit.ClassRule; import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; import sample.FirstTest; import sample.SecondTest; @RunWith(Suite.class) @SuiteClasses({ FirstTest.class, SecondTest.class }) public class AllTests { @ClassRule public static WebDriverRule webDriverRule = new WebDriverRule(); }
ただし、FirstTest, SecondTestからもWebDriverインスタンスにアクセスしないとテストになりません。しかし、素直に(?)WebDriverRuleを実装していると、以下のように書いた場合に多重(テストスイート側とテストクラス側)にFirefoxが起動することになってしまいます。
public class FirstTest { @ClassRule public static WebDriverRule webDriverRule = new WebDriverRule(); @Test public void Google検索とか() { WebDriver driver = webDriverRule.getDriver(); // ... } }
悩んだ末、以下のようなClassRuleを定義することで、解決しました。
package sample.rule; import org.junit.rules.ExternalResource; import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; public class WebDriverRule extends ExternalResource { private static int count; private static WebDriver driver; public WebDriver getDriver() { return driver; } @Override public void before() { if (count == 0) { driver = new FirefoxDriver(); } count++; } @Override public void after() { count--; if (driver != null && count == 0) { driver.quit(); } } }
TestRuleの一種であるExternalResourceを継承してルールを作ります。ExternalResourceを継承してルールを作る場合、before()で前処理、after()で後処理を書けばよいので、static変数としてbefore()の呼び出しの回数をカウントしておくようにし、0の場合に起動・停止を行うようにすれば、テストスイート側と各テストクラス側の両方に
@ClassRule public static WebDriverRule webDriverRule = new WebDriverRule();
と書いてあっても、ブラウザの起動・停止が最初と最後の1回だけに抑制されます。
あとは、build.xmlやbuild.gradleで、テストスイートを指定してテストを実行するようにすればOK。この方法だと、EclipseからJUnitテストを実行させる場合でも、テストスイートとテストクラス、どちらを選んで実行してもテストがきちんと動作するので、便利かなと思います。
Gradle&Groovyで実践
https://github.com/tq-jappy/selenium-util
テストをGroovyで書く習慣をつけたいという理由で、上記テストスイート、テストクラスをGroovyで記述し、テストをGradleタスクで行うようにしました。
build.gradleは以下のとおりです。testタスクではテストスイートだけを指定しています。
apply plugin: "java" apply plugin: "maven" apply plugin: "eclipse" apply plugin: "groovy" version = 1.0 sourceCompatibility = 1.7 def defaultEncoding = 'UTF-8' [ compileJava, compileTestJava, javadoc ]*.options*.encoding = defaultEncoding repositories { mavenLocal() mavenCentral() } dependencies { compile 'org.seleniumhq.selenium:selenium-java:2.32.0' testCompile 'org.codehaus.groovy:groovy-all:2.1.3' testCompile 'junit:junit:4.11' } test { include '**/AllTests.*' }
テストだけGroovyで書きたいという場合、「apply plugin: "groovy"」を追加して、testCompileにGroovyの依存関係を書くだけ。テストクラスはsrc/test/groovyに書いていけばOK。さすがにGradleだと簡単ですね。
AllTests.groovyは以下のとおり。Groovyでもテストスイートが問題なくできることが分かりました。
package sample; import static org.junit.Assert.* import org.junit.ClassRule import org.junit.runner.RunWith import org.junit.runners.Suite import org.junit.runners.Suite.SuiteClasses import sample.rule.WebDriverRule @RunWith(Suite) @SuiteClasses([FirstTest.class, SecondTest.class, NormalTest.class]) class AllTests { @ClassRule public static WebDriverRule browser = new WebDriverRule() }
今度は、最近リリースされたGradle1.6で導入されたCategoryを使って、重要なテストだけを実行するテストタスクや、重たいテストを除外するテストタスクを作ってみたいと思います。