Selenium3とSelenium4どちらでも動くように作ったJavaクラスをひとつのGradleタスクでテストする方法

## 解決すべき問題

Webブラウザに表示されるHTMLベースのUIを自動化テストするコードをわたしは[Selenium](https://www.selenium.dev/documentation/overview/)をベースにたくさん作ってきました。ずっとSelenium3を使ってきたが、2021年10月にSelenium4が正式リリースされた。WebUI自動化テストを作る仕事を今後も続けるつもりだが、Selenium3と４に共通する基本的な使い方をすることが大半だろう。だから新しく作る自動化テストがSelenium３と4どちらでも動くようにしたいと考えた。いざ着手したらけっこう面倒だった。技術的問題と解決方法をここにメモします。

公式ドキュメントにSelenium3とSelenium4の違いについて説明がある。

https://www.selenium.dev/documentation/webdriver/getting_started/upgrade_to_selenium_4/

APIが非互換的に変更された一つの例として`WebDriverWait`クラスのコンストラクタのシグナチャが変更されたことがあげられる。

```
// Selenium3 signature
new WebDriverWait(driver, 3)
                       // ^
　　　　　　　　.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#id")));
```

Selenium３では引数`timeout`を`long`型とし、指定された値`3`を秒単位の値と解釈すると決めうちしていました。いっぽうSelenium４では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`](https://github.com/kazurayam/inspectus4selenium/blob/master/src/main/java/com/kazurayam/inspectus/materialize/selenium/WebDriverFormulas.java)クラスを自作しました。このクラスをどう使うかというと次のようなJUnitテストを書きます。

[WebDriverFormulasTest](https://github.com/kazurayam/inspectus4selenium/blob/master/src/test/java/com/kazurayam/inspectus/selenium/WebDriverFormulasTest.java)

```
　　　　　　　　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)`メソッドの[実装](https://github.com/kazurayam/inspectus4selenium/blob/master/src/main/java/com/kazurayam/inspectus/selenium/WebDriverFormulas.java)を読めばわかるように、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ファイルを[次のように](https://github.com/kazurayam/inspectus4selenium/blob/0.1.0/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: '４.6.0'
    }
    include '**/WebDriverFormulas*'
}

task testSelenium(dependsOn: [testWithSelenium3, testWithSelenium4])
```

`testWithSelenium3`タスクと`testWithSelenium4`をこのように書けばいいとわかるまでけっこう苦労しました。めでたしめでたし。