解決すべき問題
Webブラウザに表示されるHTMLベースのUIを自動化テストするコードをわたしはSeleniumをベースにたくさん作ってきました。ずっとSelenium3を使ってきたが、2021年10月にSelenium4が正式リリースされた。WebUI自動化テストを作る仕事を今後も続けるつもりだが、Selenium3と4に共通する基本的な使い方をすることが大半だろう。だから新しく作る自動化テストがSelenium3と4どちらでも動くようにしたいと考えた。いざ着手したらけっこう面倒だった。技術的問題と解決方法をここにメモします。
公式ドキュメントにSelenium3とSelenium4の違いについて説明がある。
APIが非互換的に変更された一つの例としてWebDriverWait
クラスのコンストラクタのシグナチャが変更されたことがあげられる。
// Selenium3 signature
new WebDriverWait(driver, 3)
// ^
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#id")));
Selenium3では引数timeout
をlong
型とし、指定された値3
を秒単位の値と解釈すると決めうちしていました。いっぽうSelenium4ではtimeoutパラメータがjava.time.Duration
型に変更されました。
// Selenium4 signature
new WebDriverWait(driver, Duration.ofSeconds(3))
// ^^^^^^^^^^^^^^^^^^^^^
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#id")));
Selenium4は引数timeout
をlong
型とするシグナチャを定義していません。だからSelenium3を前提に書いた自動化テストのコードはSelenium4ではコンパイル・エラーになります。
解決方法
Seleniumのバージョンの差異を吸収するヘルパクラスを作った
com.kazurayam.inspectus.selenium.WebDriverFormulas
クラスを自作しました。このクラスをどう使うかというと次のようなJUnitテストを書きます。
WebDriverFormulas formulas;
...
@Test
public void test_createWebDriverWait() {
WebDriverWait wait = formulas.createWebDriverWait(driver, 3);
assertNotNull(wait);
assertTrue(wait instanceof WebDriverWait);
}
WebDriverFormulas
クラスのcreateWebDriverWait(WebDriver, long)
メソッドを呼び出すことによってWebDriverWait
オブジェクトのインスタンスが生成されます。このメソッド呼び出しはSelenium3でもSelenium4でもどちらでも成功します。ここにトリックがあります。
createAWebDriverWait(WebDriver, long)
メソッドの実装を読めばわかるように、JavaリフクションAPIを使ってSelenium3とSelenium4とどちらのバージョンのクラスライブラリが実行時に与えられているかを調べ、現に使える方を選んで安全にWebDriverWait
オブジェクトを生成しています。
Selenium3でユニットテストしSelenium4でもユニットテストしたい、どうやるか
com.kazurayam.inspectus.selenium.WebDriverFormulas
クラスをJUnit5でユニットテストしようとしました。Gradleのbuild.gradleファイルをこう書いた。
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
implementation group: 'org.seleniumhq.selenium', name: 'selenium-java',
version: '3.141.59'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.9.1'
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.9.1'
}
tasks.withType(Test) {
useJUnitPlatform()
}
上記でSeleniumのバージョン 3.141.59 を指定していることに注意。build.xmlをこう書いた上でコマンドラインでGradleのtestタスクを実行します。
$ gradle test
これでSelenium3を与えた環境でカスタムクラスがちゃんと動くことをテストできました。では次にSelenium4でも動くことをテストしましょう。読者諸兄にはもうおわかりでしょうが、build.xmlを書き直せばいい。
implementation group: 'org.seleniumhq.selenium', name: 'selenium-java',
version: '4.6.0' // <== '3.141.59'
そして前と同じく
$ gradle test
とやる。これでSelenium4でカスタムクラスが動くことをテストできました。
しかしbuild.gradleファイルをいちいち書き直すこのやり方はダサい。我慢ならない。
わたしはこうしたいと考えた。
$ gradle testSelenium
testSelenium
というカスタムなGradleタスクを作って実行すると、dependenciesの中にSelenium3を指定したJUnitテストが実行される。さらに続いてSelenium4を指定したJUnitタスクも実行される、というふうにしたいと考えた。Gradleをどうひねれば実現できるでしょうか?
あちこち探り試行錯誤した挙句、build.gradleファイルを次のように書いたら私の望み通りにGradleが動いてくれました。
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.9.1'
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.9.1'
}
tasks.withType(Test) {
useJUnitPlatform()
}
task testWithSelenium3(type: Test, dependsOn: compileTestJava) {
dependencies {
implementation implementation group: 'org.seleniumhq.selenium', name: 'selenium-java',
version: '3.141.59'
}
include '**/WebDriverFormulas*'
}
task testWithSelenium4(type: Test, dependsOn: compileTestJava) {
dependencies {
implementation implementation group: 'org.seleniumhq.selenium', name: 'selenium-java',
version: '4.6.0'
}
include '**/WebDriverFormulas*'
}
task testSelenium(dependsOn: [testWithSelenium3, testWithSelenium4])
testWithSelenium3
タスクとtestWithSelenium4
をこのように書けばいいとわかるまでけっこう苦労しました。めでたしめでたし。