🍚

Testcontainers:Seleniumテスト環境を素早くDockerに構築

2023/10/27に公開

はじめまして。Accenture Japan SDETの中西です。

私の所属するSDETでは、APIやE2Eテストの自動化を支援する活動を行っています。

テストの自動化というと、Seleniumを用いたブラウザテストの自動化を思い浮かべる方も多いのではないでしょうか。
しかし、SeleniumによるE2Eテストは、実行するたびに結果が変わるフレーキーテストが発生するという問題がついてまわります。可能な限り結果を安定させるためには、実行環境を一定にすることが求められます。
この要求に対する一つの答えが、以前紹介したBrowserStackでした。しかしこちらはクラウドサービスかつ有償ツールであるため、プロジェクトの要件によっては、利用できない場合もあります。
そこでこの記事では、Dockerコンテナ化されたブラウザでE2Eテストを実行できるライブラリ、Testcontainersを紹介します。

想定する読者

  • SeleniumやDockerに興味のある方
  • Selenium GridやSelenoidの利用を検討したことがある方
  • 開発チーム内に複数のOSが混在している方

1.Testcontainersとは

Testcontainersは、JavaやPython等で書かれたテストコードからDockerコンテナを操作し、テスト環境を構築するためのライブラリです。
本記事では、主にChromeやFireFoxなどのブラウザコンテナを用いてアプリをテストする事例を紹介しますが、この他にもWebサーバ、アプリケーションサーバ、DB、ネットワークなど、コンテナを用いたテスト環境構築が可能です。
docker-composeなどの定義ファイルが不要で、テストコードの中で環境構築することができます。
実行ごとに、いわば"使い捨て"の環境を簡単に構築することができるため、単体テストや結合テスト等に広く活用されています。

SeleniumテストをDockerコンテナ内で実行するメリット

Dockerコンテナ内で実行することで、Windows、MacなどホストOSが変わっても、常に一定の環境でブラウザを実行することができます。
また、ローカル環境を汚さずにマルチブラウザテストや、複数のブラウザバージョンでのテストが可能となります。

SeleniumテストをDockerコンテナ内で実行するデメリット

Dockerコンテナ内のブラウザで実行するため、ブラウザの実行環境はLinux固定となります。
そのため、WindowsやMacでしか発生しないバグを検出することができませんし、SafariなどLinux非対応のブラウザも実行することはできません。
執筆時点では、Chrome、FireFox、Edgeのみの対応となっている点は留意する必要があります。

Selenium Grid、Selenoidとの比較

Selenium Gridや、その改良版であるSelenoidも、ブラウザをDockerコンテナで実行することができます。
実はTestcontainers実行時に実行されるブラウザコンテナは、Selenium Gridと同じくSeleniumの公式Dockerイメージが利用されており、本質的にやっていることは同じです。
Selenium GridやSelenoidは、テスト実行前にdocker-composeでサービスを起動しておく必要がありますが、Testcontainerの場合はテストランナーが直接コンテナを起動でき、テストコード内で全て完結する点で、よりシンプルにテストを定義できると言えます。
一方で、Selenium GridやSelenoidは、Hubと呼ばれるサーバでNodeを管理する構成で、NodeにはDockerだけでなくWidowsやMac、モバイル端末などの実機を接続することができます。そのため、実機でのテストを行う場合や大規模な並列実行を行う必要がある場合などは、Selenium
Gridを利用するメリットがあります。このあたりは、プロジェクトの特性を見極め、適切なツールを導入する必要があるでしょう。

2.実際にTestcontainersを使ってみる

前置きが長くなりましたが、実際にTestcontainersを使って、SeleniumのテストをDockerコンテナ内で実行してみましょう。

本記事で前提とする技術スタック

  • Java 18
  • Maven
  • JUnit4
  • Selenide
  • Testcontainers

サンプルテスト

Testcontainersの導入前に、今回使用するサンプルテストを紹介します。
Selenideを利用して、Googleのトップページを開き"Selenide"について検索し、検索結果のトップにSelenideの公式ページがあることを確認するシナリオです。

import com.codeborne.selenide.Selenide;
import com.codeborne.selenide.SelenideElement;
import org.junit.Test;

import static com.codeborne.selenide.Condition.text;
import static com.codeborne.selenide.Selenide.*;

public class SampleTest {
    @Test
    public void test() {
        open("https://google.com");
        SelenideElement searchBox = $x("//textarea[@id='APjFqb']");
        searchBox.setValue("Selenide").pressEnter();
        Selenide.$("#rso .g").shouldHave(text("selenide.org"));
    }
}

このテストを実行すると、ローカル環境にブラウザが起動してテストが実行されます。

このテストをDockerコンテナで実行していきます。

依存関係の追加

まずはMavenの依存関係を追加します。

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>selenium</artifactId>
    <version>1.19.1</version>
    <scope>test</scope>
</dependency>

テストコードの修正

Testcontainersを利用するために、テストコードを修正します。
@Ruleアノテーションで、ブラウザコンテナを起動し、インスタンスから取得したドライバーをSelenideで利用するドライバとして指定しています。

import com.codeborne.selenide.Selenide;
import com.codeborne.selenide.SelenideElement;
import com.codeborne.selenide.WebDriverRunner;
import org.junit.Rule;
import org.junit.Test;
import org.openqa.selenium.chrome.ChromeOptions;
import org.testcontainers.containers.BrowserWebDriverContainer;

import static com.codeborne.selenide.Condition.text;
import static com.codeborne.selenide.Selenide.*;

public class SampleTest {

    // ブラウザコンテナの起動   
    @Rule
    public BrowserWebDriverContainer<?> chrome = new BrowserWebDriverContainer<>()
            .withCapabilities(new ChromeOptions());

    @Test
    public void test() {
        // WebDriverの指定
        WebDriverRunner.setWebDriver(chrome.getWebDriver());
        
        open("https://google.com");
        SelenideElement searchBox = $x("//textarea[@id='APjFqb']");
        searchBox.setValue("Selenide").pressEnter();
        Selenide.$("#rso .g").shouldHave(text("selenide.org"));
    }

}

このコードを実行すると、先程とは異なり、ローカルのブラウザが起動しないままテストが実行されます。
テスト中にdocker psを実行すると、ブラウザコンテナが起動していることが確認できます。
selenium/standalone-chromeの他に、testcontainers/ryukというコンテナが起動していますが、こちらはTestcontainersで利用するコンテナのライフサイクルを管理するためのコンテナで、適切なタイミングでコンテナを終了してくれます。

$ docker ps
CONTAINER ID   IMAGE                               COMMAND                   CREATED         STATUS         PORTS                                                                                      NAMES
76299df4a2a7   selenium/standalone-chrome:4.13.0   "/opt/bin/entry_poin…"   2 seconds ago   Up 1 second    0.0.0.0:32797->4444/tcp, :::32797->4444/tcp, 0.0.0.0:32796->5900/tcp, :::32796->5900/tcp   inspiring_shtern
1273e9969244   testcontainers/ryuk:0.5.1           "/bin/ryuk"               3 seconds ago   Up 2 seconds   0.0.0.0:32795->8080/tcp, :::32795->8080/tcp                                                testcontainers-ryuk-6e973c94-b1a8-40c9-b2ef-d67eba8d8d03

Videoの保存

Testcontainersは、VNC経由でコンテナに接続し、動作を録画する機能があります。
コンテナのインスタンス作成時、下記のように設定することで、videosディレクトリに動画が保存されます。
事前にvideosディレクトリを作成しておく必要があることに注意してください。

@Rule
public BrowserWebDriverContainer<?> browser = new BrowserWebDriverContainer<>()
        .withCapabilities(new ChromeOptions()
        .withRecordingMode(
                BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL,
                new File("videos"),
                VncRecordingContainer.VncRecordingFormat.MP4
        );

出力された動画を見ると、Selenium Gridのコンテナ内でテストが実行されていることがわかります。

ブラウザの指定

ブラウザの指定は、withCapabilitiesの引数を変更することで可能です。
今回は、selenideでブラウザを指定するプロパティであるselenide.browserを読み込んで、ブラウザを変更する実装としてみます。


private Capabilities getOptions (){
    String browserName = System.getProperty("selenide.browser", "chrome");
    Capabilities options;
    switch (browserName.toLowerCase()) {
        case "firefox":
            options = new FirefoxOptions();
            break;
        case "edge":
            options = new EdgeOptions();
            break;
        default:
            options = new ChromeOptions();
            break;
    }
    return options;
}

@Rule
public BrowserWebDriverContainer<?> browser = new BrowserWebDriverContainer<>()
        .withCapabilities(getOptions())
        .withRecordingMode(
                BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL,
                new File("videos"),
                VncRecordingContainer.VncRecordingFormat.MP4
        );

ARM64環境での注意点

M1 Mac等のARM環境を利用される方は注意が必要です。
現時点では、seleniumのdockerイメージはARM64に対応していません。MacですとVirtualization
Frameworkなどを通して、x86のイメージを実行することは可能ですが、完全な動作は保証されておりません。
実際に私の環境の手元のMacBookPro@M1 Proでもエラーが発生することがあります。

この場合の回避策として、有志が作成したseleniarmというARM64対応のイメージを利用することができます。seleni'arm'という名前が洒落ていますよね。

これらのイメージを利用するには、使用するDockerイメージを置換する処理を追加してください。

@Rule
public BrowserWebDriverContainer<?> chrome = new BrowserWebDriverContainer(
                DockerImageName.parse("seleniarm/standalone-chromium:latest")
                    .asCompatibleSubstituteFor("selenium/standalone-chrome")
                ).withCapabilities(new ChromeOptions());

我々のチームで実際にTestcontainersを利用する際には、実行環境を判別して使用するイメージを切り替えるようにしています。

3.まとめ

Testcontainerにより、シンプルな処理でSeleniumのテストをDockerコンテナ内のブラウザ実行することができました。
今回のサンプルがテストコードの中で完結しており、dockerファイルやdocker-composeファイルが登場しなかったことからも、そのシンプルさが伝われば幸いです。
我々SDETチームでも、以前はSelenoidを採用していましたが、単純な実行環境ではTestcontainersを利用することが多くなってきました。

Testcontainersを、テストコードの中からDockerを直接操作するツールだと考えれば、さらに色々な活用方法が見つかりそうです。
私は最近、pythonのFAST APIを使ったサービスの構築に携わっているのですが、テスト実行ごとにDB内にテストデータが増えていくのが気になっており、Testcontainerを使ってテスト実行ごとにDBを使い捨てる仕組みにできないかと思案しております。

それでは皆様、Happy Testing!

参考リンク

https://testcontainers.com/
https://java.testcontainers.org/modules/webdriver_containers/
https://github.com/seleniumhq-community/docker-seleniarm

Accenture Japan QE&Aでは、テスト自動化を通じてお客様の変革を支援しています。
同じビジョンを共有する仲間を募集しています!
https://www.accenture.com/jp-ja/careers/jobdetails?id=R00091947_ja

Accenture Japan SDET (Voluntary)

Discussion