こんにちは。ユーザベース Product Team の old_horizon です。
私達の開発チームでは、E2E テストの実行環境として Zalenium を主に利用しています。
しかし 2020 年 3 月の最終リリースをもって開発が終了しており、将来的にリプレースを検討する必要がありました。
こうした状況の中で、Selenium Grid 4 が 2021 年 10 月にリリースされました。
その新機能の一つである Dynamic Grid が代替手段として有力に思え、まずは概要の理解から始めました。
Dynamic Grid とは
GitHub の docker-selenium に解説があります。
Grid 4 has the ability to start Docker containers on demand, this means that it starts a Docker container in the background for each new session request, the test gets executed there, and when the test completes, the container gets thrown away.
つまり、新規セッションごとに使い捨ての Docker コンテナを用意してテストを実行する機能です。
Zalenium や Selenoid の同機能を参考にしたものと思われます。
技術的な課題
この Dynamic Grid は、現在のところ Docker への構築しかサポートされていません。
すべての Docker コンテナが一つの環境で動作するため、性能向上には垂直スケールが必要になります。
しかし垂直スケールには再起動が必要になるうえ、ハードウェアの物理的限界もあります。
そのため Kubernetes を利用した水平スケール可能な構成を実現したいと考えました。
使い捨ての Docker コンテナを Pod として用意できれば、複数台で構成された各ノードのリソースを有効に利用できます。
公式の Grid では機能が不足しているため、今回は Grid 4 の拡張として独自に実装しました。
Grid 4 の拡張方法
Grid 3 では 独自 Servlet や Proxy を実装して拡張することができましたが、Grid 4 に同等の機構はありません。
そのため実装を読みながら、拡張できそうな箇所を探しました。
これより Selenium Server 4.1.2 のコードを参照しながら解説していきます。
--ext オプション
Selenium Server は、起動時に --ext オプションで指定された jar ファイル内のクラスをロードします。
実装はエントリポイントである Bootstrap クラスに存在します。
これで拡張を含んだ jar ファイルを読み込ませる方法がわかりました。
CliCommand インターフェース
Bootstrap クラスでクラスローダーの準備が整うと、リフレクションで Main クラスが呼び出されます。
Main クラスは、ServiceLoader という Java 標準のプラグイン機構で CliCommand インターフェースの実装を取得します。
CliCommand インターフェースの実装は Selenium Server で利用可能な各コマンドに対応します。
この仕組みを活用すると、以下の手順で任意のコマンドを拡張できます。
- 任意のコマンドを継承したクラスを作って拡張する
- CliCommand#getName をオーバーライドし、起動時に指定するコマンド名を返す
- 元となったコマンドとの競合を避けるため、別名称にする必要があります
com.google.auto.service.AutoService
アノテーションをクラスに付与する- ServiceLoader が認識するために必要な
META-INF/services
配下のファイルを自動生成してくれます
- ServiceLoader が認識するために必要な
- 拡張をビルドする
- --ext オプションで生成された jar を指定しつつ、拡張したコマンドを実行する
TOML 設定ファイルによる実装の切り替え
Grid 4 では TOML 形式の設定ファイル が採用されています。
この設定ファイルで、Selenium Grid を構成する一部コンポーネントの実装を任意のものに差し替えることができます。
4.1.2 で対応しているコンポーネントは以下のとおりです。
各コンポーネントのセクションにおいて implementation
キーでクラスの FQCN 文字列を指定します。
拡張を実装する
上記で見つけた機構を使って、Kubernetes に Dynamic Grid を構築する拡張を実装しました。
リポジトリはこちらです。
Grid 3 と同様に Hub と Node の構成かつ、既存の Zalenium と同様に一つの Pod に必要なアプリケーションを同居させています。
また以前 Zalenium の拡張として実装した、ファイルダウンロードを行う e2e テスト対応と同等の機能も備えました。
Node
- KubernetesNode
TOML 設定ファイルで、Node の独自実装であるこのクラスを指定します。 - KubernetesSessionFactory
- apply
新規セッション要求時に、WebDriver を実行する使い捨ての Pod を作成します
- apply
Hub
- DynamicGridHub
CliCommand インターフェースを実装した Hub クラスの拡張です。
起動時のコマンドに、getName
メソッドが返すdynamic-grid-hub
を指定します。- createHandlers
ファイルダウンロード対応のためにエンドポイントを追加
- createHandlers
Proxy
Hub と Node を一つの Pod で動作させる都合で、OpenResty によるリバースプロキシを間に挟みました。
ルーティングと一部レスポンスの書き換えを行っています。
- 設定ファイル
- /wd/hub/session
レスポンスボディに含まれる Hub の IP が、Kubernetes クラスタ内の IP になってしまう問題への対処
リクエストの Host ヘッダーの値でホストを置換して返します
- /wd/hub/session
動作確認
筆者が動作確認した環境は以下のとおりです。
- Ubuntu 18.04
- Docker 20.10.13
- kind 0.11.1
- Gauge 1.4.3
事前に kind でローカル環境に Kubernetes クラスタを構築しておきます。
まずは Skaffold を利用して、ビルドから Kubernetes クラスタへのデプロイまでを行います。
リポジトリの kubernetes/skaffold
に移動して、以下のコマンドを実行します。
$ kubectl create ns selenium $ skaffold run -n selenium
デプロイが完了したら、Grid 4 の Hub にアクセスしてみます。
Internal IP と、NodePort として公開されている Service のポート番号の組み合わせを使います。
$ kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}' 172.18.0.2 $ kubectl -n selenium get svc selenium -o jsonpath='{.spec.ports[0].nodePort}' 31968
したがって、この場合は http://172.18.0.2:31968/
が URL になります。
ブラウザで開くと、このように GUI が表示されます。
リポジトリ内の e2e に移動して mvn test
を実行して e2e テストを流します。
e2e ではこれらの検証をしています。
- 要素の操作やアサーションができること
- Selenium 4 でサポートされた BiDirectional functionality が利用できること
- ファイルダウンロードを行う e2e テスト向けの機能が利用できること
- 詳細な仕様は README を参照してください
全件成功することが確認できたら、録画されたビデオを見てみましょう。
(Selenium Grid の URL)/videos/
にアクセスすると、録画されたビデオファイルの一覧が表示されます。
ファイル名には Selenium セッションの ID が使われます。
ビデオファイルをクリックすると、ブラウザ上でそのまま閲覧できます。
おわりに
公式ドキュメントが充実していなくても、しっかりコードを読めばアーキテクチャへの理解が深まることを実感できました。
運用していく中で問題も出てくると思いますが、まずは Selenium 4 への移行の足がかりができたことを嬉しく思います。
ユーザベース Product Team ではエンジニアを積極採用中です。
e2e から始まるテスト駆動開発に興味をお持ちいただけたら、ぜひこちらからご応募ください。