JavaのSeleniumテストがDockerコンテナを起動・停止できるようにした

このサンプル一式を収めたGitHubプロジェクトが下記の場所にあります。

-   [SeleniumTestInJavaBackedByDockerContainer](https://github.com/kazurayam/SeleniumTestInJavaBackedByDockerContainer)



# 解決したい問題

わたしはWebのユーザインターフェースつまりブラウザに表示されたHTML画面を対象として自動化テストを実行するコードをJavaまたはGroovy言語で書いています。いっぽう勉強のため、Python言語で書いたWebサーバアプリケーションを包んだDockerイメージを作った。サーバアプリをDockerコンテナとして立ち上げて、Javaで書いたSeleniumテストで動作確認したくなりました。

それはもちろんできます。次のような手順で操作すればいい。

(1) わたしのMacBook AirでTerminalのウインドウを開き、Dockerコンテナをデーモンプロセスとして起動する。するとWebサーバアプリが立ち上がって `http://127.0.0.1/` というURLが使えるようになる。

```
$ cd ~/tmp
$ docker run -d -p 80:8080 kazurayam/flaskr-kazurayam:1.1.0
```

(2) JUnitテストを含むプロジェクトにcdして、Gradleのtestコマンドを起動してSeleniumテストを実行する。

```
$ cd ~/SeleniumTestInJavaBackedByDockerContainer
$ gradle test
```

するとSeleniumによってWebブラウザが立ち上がり、その中で http://127.0.0.1/ のサイトが開いて、閉じる。これでテストは終了。

(3) テストが終了したら後始末をしよう。下記のコマンドを実行する。IPポート80番をLISTENしているDockerコンテナのidがわかる。

```
$ docker ps --filter publish=80 --filter status=running -q
fd5ad3b76b13
```

(4) 見つけたDockerコンテナのidを指定して停止する。

```
$ docker stop fd5ad3b76b13
```

この手順はむずかしくない。だがSeleniumテストを実行するたびに繰り返しDockerコンテナを起動して停止するのが面倒だ。操作を間違えるし。たとえばDockerコンテナを停止せずにもう一度起動しようとすると "IP port is already in use" といわれてしまう。だからわたしは次のことを実現したい。

**Seleniumテストのためにlocalhost上でWebサーバを動かしたいが、そのためにDockerコンテナを起動・停止するのをJUnitテストの一部としてJavaコードから実行したい。**

## 解決方法

わたしは [subprocessj](https://github.com/kazurayam/subprocess) というJavaライブラリを自作し[Maven Central](https://mvnrepository.com/artifact/com.kazurayam/subprocessj) で公開した。subprocessjは[`java.lang.ProcessBuilder`](https://www.baeldung.com/java-lang-processbuilder-api)をくるんで簡素なAPIで呼び出せるようにしたもの。subprocessjのクラス群を使えばJavaコードから`docker run` コマンドを実行することができる。

次のようなことをするJUnitテストをサンプルとして書いた。

* このテストはSelenium WebDriverを使って "http://127.0.0.1:3080/" というURLをテストする

* このURLを提供するWebサーバはローカルホスト上のプロセスで動く。そのプロセスを起動するのにDockerコンテナを使う。わたしが事前に準備したDockerイメージを利用する。

* このURLを提供するWebサーバはPython言語で書かれており、Palletsプロジェクトが開発して https://flask.palletsprojects.com/en/2.0.x/tutorial/ で公開しているもの。それをわたしがDockerイメージに直した。Seleniumテストを開発するための練習台として繰り返し利用できるようにしたかったので。

* このテストはDockerコンテナを起動・停止するのだが、そのためには `docker run`, `docker ps` , `docker stop`などのコマンドライン用のコマンドをJUnitテストの内部でJavaコードから実行する。


## 説明

### 実行環境

サンプルを実行するために必要な環境は次のとおり

- Dockerをインストールする必要がある。わたしは主にMacで作業する。[Docker Desktop](https://www.docker.com/products/docker-desktop)をインストールした。
- Widnows10のPCにWindows版のDocker Desktopをインストールして、動作することを確認済み。
- Java8+ と Gradle v6+ が必要。
- わたしはMacマシンで、bashシェルの環境でテストした。

### シーケンス図

サンプルのJUnitテストがどういう動作するのか、処理シーケンスを図にした。


![sequence](https://kazurayam.github.io/SeleniumTestInJavaBackedByDockerContainer/diagrams/out/sequence.png)


### サンプルとしての JUnit5テスト

サンプルコードの全体を読むには下記のリンクを参照のこと。

- [example.DockerBackedSeleniumTest](https://github.com/kazurayam/SeleniumTestInJavaBackedByDockerContainer/blob/master/src/test/java/example/DockerBackedSeleniumTest.java)

コードの一部を引用しながら説明しよう。

#### @BeforeAll

Dockerコンテナを起動するところ

```
        @BeforeAll
        public static void beforeAll() throws IOException, InterruptedException {
            File directory = Files.createTempDirectory("DockerBackedWebDriverTest").toFile();
            ContainerRunningResult crr =
                    ContainerRunner.runContainerAtHostPort(directory, publishedPort, image);
            if (crr.returncode() != 0) {
                throw new IllegalStateException(crr.toString());
            }
            // setup ChromeDriver
            WebDriverManager.chromedriver().setup();
        }

```

[`com.kazurayam.subprocessj.docker.ContainerRunner`](https://github.com/kazurayam/subprocessj/blob/master/src/main/java/com/kazurayam/subprocessj/docker/ContainerRunner.java) クラスが "docker run" コマンドを包み込んでいる。これによってDockerコンテナを起動する。

#### @BeforeEach

Webブラウザを起動するところ

```
        @BeforeEach
        public void beforeEach() {
            driver = new ChromeDriver();
        }
```

#### @Test

ブラウザの中でURLを開き、応答されたHTMLの中身を検査するところ

```
        @Test
        public void test_page_header() {
            driver.navigate().to(String.format("http://127.0.0.1:%d/", HOST_PORT));
            WebElement siteName = driver.findElement(By.xpath("/html/body/nav/h1"));
            assertNotNull(siteName);
            assertEquals("Flaskr", siteName.getText());
            delay(2000);
        }
```

ちなみに http://127.0.0.1/ をブラウザで開いたらこんな画面が見える。

![Flaskr](https://github.com/kazurayam/SeleniumTestInJavaBackedByDockerContainer/raw/master/docs/images/Flaskr.png)

#### @AfterEach

ブラウザを閉じるところ

```
        @AfterEach
        public void afterEach() {
            if (driver != null) {
                driver.quit();
                driver = null;
            }
        }
```

#### @AfterAll 

Dockerコンテナを停止するところ。所定のIPポート番号をLISTENしているDockerコンテナを探し、そのidを把握する。そしてコンテナidを指定して`docker stop`する。

```
        @AfterAll
        public static void afterAll() throws IOException, InterruptedException {
            ContainerFindingResult cfr = ContainerFinder.findContainerByHostPort(HOST_PORT);
            if (cfr.returncode() == 0) {
                ContainerId containerId = cfr.containerId();
                ContainerStoppingResult csr = ContainerStopper.stopContainer(containerId);
                if (csr.returncode() != 0) {
                    throw new IllegalStateException(csr.toString());
                }
            } else {
                throw new IllegalStateException(cfr.toString());
            }
        }

```

* [`com.kazurayam.subprocessj.docker.ContainerFinder`](https://github.com/kazurayam/subprocessj/blob/master/src/main/java/com/kazurayam/subprocessj/docker/ContainerFinder.java) クラスが "docker ps" コマンドを包み込んでいる。これによってDockerコンテナの状態を調べることができる。

* [`com.kazurayam.subprocessj.docker.ContainerStopper`](https://github.com/kazurayam/subprocessj/blob/master/src/main/java/com/kazurayam/subprocessj/docker/ContainerStopper.java) クラスが "docker stop" コマンドを包み込んでいる。これによってDockerコンテナを停止することができる。

`docker stop`でコンテナを停止するにはわりと時間がかかる。わたしの手元で10秒ぐらい。

### サンプルを再利用するには

[build.gradle](https://github.com/kazurayam/SeleniumTestInJavaBackedByDockerContainer/blob/master/build.gradle) を見てください。Gradleに関するスキルを持っている人を前提し説明を省略します。

## 結語

この記事で紹介したクラス群は [Subprocessj 0.3.1](https://mvnrepository.com/artifact/com.kazurayam/subprocessj) 以降に含まれています。これによってわたしはPythonで書いたWebサーバアプリを対象にWebユーザインタフェースの自動化テストをJavaで書くことができるようになった。Dockerコンテナ化したWebサーバの起動と停止をJUnitテストの中で自動実行できるので、とても楽だ。この方法はDockerコンテナの中身がなんであれ応用できる。ほかにどんな使い道があるか、いろいろ試してみたい。

## Dockerに関する参考情報

-   [forum.docker.com, “docker run” cannot be killed with ctrl+c](https://forums.docke.com/t/docker-run-cannot-be-killed-with-ctrl-c/13108/)

-   [docker ps command](https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/commandline/ps/)

-   [docker run command](https://docs.docker.com/engine/reference/commandline/run/)

-   [docker stop command](https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/commandline/stop/)

-   [subprocessj project top](https://github.com/kazurayam/subprocessj)

## その他関連情報

`Subprocessj`プロジェクトの成果物はMaven Centralリポジトリにあります。

-   <https://mvnrepository.com/artifact/com.kazurayam/subprocessj>