このサンプル一式を収めたGitHubプロジェクトが下記の場所にあります。
解決したい問題
わたしは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 というJavaライブラリを自作しMaven Central で公開した。subprocessjはjava.lang.ProcessBuilder
をくるんで簡素な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をインストールした。
- Widnows10のPCにWindows版のDocker Desktopをインストールして、動作することを確認済み。
- Java8+ と Gradle v6+ が必要。
- わたしはMacマシンで、bashシェルの環境でテストした。
シーケンス図
サンプルのJUnitテストがどういう動作するのか、処理シーケンスを図にした。
サンプルとしての JUnit5テスト
サンプルコードの全体を読むには下記のリンクを参照のこと。
コードの一部を引用しながら説明しよう。
@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
クラスが "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/ をブラウザで開いたらこんな画面が見える。
@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
クラスが "docker ps" コマンドを包み込んでいる。これによってDockerコンテナの状態を調べることができる。 -
com.kazurayam.subprocessj.docker.ContainerStopper
クラスが "docker stop" コマンドを包み込んでいる。これによってDockerコンテナを停止することができる。
docker stop
でコンテナを停止するにはわりと時間がかかる。わたしの手元で10秒ぐらい。
サンプルを再利用するには
build.gradle を見てください。Gradleに関するスキルを持っている人を前提し説明を省略します。
結語
この記事で紹介したクラス群は Subprocessj 0.3.1 以降に含まれています。これによってわたしはPythonで書いたWebサーバアプリを対象にWebユーザインタフェースの自動化テストをJavaで書くことができるようになった。Dockerコンテナ化したWebサーバの起動と停止をJUnitテストの中で自動実行できるので、とても楽だ。この方法はDockerコンテナの中身がなんであれ応用できる。ほかにどんな使い道があるか、いろいろ試してみたい。
Dockerに関する参考情報
その他関連情報
Subprocessj
プロジェクトの成果物はMaven Centralリポジトリにあります。