docker-seleniumを使ってDocker環境でPhoenix + Headless ChromeでE2Eテストをする
Installation
公式のドキュメントに従いdocker-seleniumのイメージをdocker-compose.ymlに追加する
app:
image: yourimage
# 〜中略〜
# 以下を追加
selenium-hub:
image: selenium/hub:3.14.0-europium
container_name: selenium-hub
ports:
- "4444:4444"
chrome:
image: selenium/node-chrome:3.14.0-europium
depends_on:
- selenium-hub
environment:
- HUB_HOST=selenium-hub
- HUB_PORT=4444
Elixir側はhoundを使う
{:hound, "~> 1.0"}
$ mix deps.get
houndのドキュメントに従いセットアップ
config :hound,
driver: "chrome_driver",
host: "http://selenium-hub",
port: 4444,
path_prefix: "wd/hub/"
オプションの:browser
をchrome_headless
に指定すると逆にエラーになったのでこのままで
test_helper.exsでApplication.ensure_all_started(:hound)
でhoundを起動させておく
Application.ensure_all_started(:hound)
ExUnit.start()
試しに以下のテストを追加してみる
websocket.orgのブラウザ対応チェックページにアクセスする
defmodule MyAppWeb.PageControllerTest do
use MyAppWeb.ConnCase
use Hound.Helpers
hound_session()
@tag :chrome
test "websocket", meta do
navigate_to("https://websocket.org/echo.html")
# スクショ撮影
take_screenshot()
# connectボタンを押下
find_element(:id, "connect")
|> click
# 待ち
:timer.sleep(2000)
# id=consoleLogの要素から表示されているテキストを取得
log =
find_element(:id, "consoleLog")
|> visible_text
# CONNECTEDという文字列が含まれるかassert
# 出力されていればWebsocketが有効
assert log =~ "CONNECTED"
# ページに以下の文字列が含まれるかassert
assert page_source =~ "This browser supports WebSocket"
# ページタイトルにwebsocketという文字列が含まれるかassert
assert page_title() =~ "websocket"
end
end
テスト実行
$ mix test --only chrome
Including tags: [:chrome]
Excluding tags: [:test]
.
Finished in 6.7 seconds
39 tests, 0 failures, 38 skipped
Randomized with seed 987879
# スクショも取れてる
$ ls -al screenshot-2018-9-24-18-44-46.png
-rw-r--r-- 1 root root 104170 Sep 24 18:44 screenshot-2018-9-24-18-44-46.png
Headless ChromeでE2Eテストが出来た。
補足
サブドメイン有りのホスト名をテストしたい場合
ローカル環境でテストする場合はサブドメインが無ければそのままnginx
やapp
といったservices
で指定されているコンテナ名で名前解決出来るので以下のようにテスト出来るが
test "websocket", meta do
navigate_to("http://nginx/")
assert page_source =~ "Hello"
end
admin.example.com
のようにサブドメインによってPhoenixのRouteを振り分けるようなホスト名をテストしたい場合そのホスト名でアプリコンテナへ名前解決出来る必要がある
以下でその方法を検討する
方法1: dockerのネットワークモードをhostネットワークにし、admin.localhost
にアクセスさせる
services:
app:
# ~中略~
network_mode: "host"
chromeは *.localhost
というホスト名に対してはサブドメインに関わらず全てlocalhostへリクエストするのでhostネットワークを使うことでサブドメインを使用してアプリにアクセス出来る
ただホストネットワークモードはホストのネットワークリソースを共有するのでホストネットワークモードで動作に影響が出るようなアプリやコンテナが存在する場合テスト出来ず、また競合するポートがある場合コンテナを起動出来ない
経験的にホストネットワークは本番環境では大抵使わないし想定していないバグが出やすいのであまりおすすめは出来ない
方法2: chrome用のコンテナでアプリコンテナの名前解決をする
実際にheadless chromeが走るのはchromeコンテナなのでchromeコンテナからアプリへのネットワーク到達性を確保する必要がある (もっと言えば前段のnginx。Cowboyに直接chromeからアクセスするとなぜかBad Requestになった)
例えば以下のようなdocker-compose.ymlが存在する場合nginxコンテナにadmin.example.com
のようなホスト名でchromeコンテナからアクセス出来る必要がある
services:
nginx:
image: nginx:latest
ports:
- 80:80
app:
image: your/image
# ~中略~
chrome:
image: selenium/node-chrome:3.14.0-europium
depends_on:
- selenium-hub
environment:
- HUB_HOST=selenium-hub
- HUB_PORT=4444
名前解決する方法はhostsファイルだけに限らずサービスディスカバリ用のコンテナを追加するなど色々あるが、個人的には余計なイメージを導入したり依存を追加せず、出来る限りデフォルト状態で対処したいので今回はhostsファイルに追加する方法を取る
なぜhostsファイルなのかというとnginx-proxyのようなイメージやサービスディスカバリはCI環境などでも同じように動作出来る保証がないからである。
CircleCIなどはCI時に依存するコンテナを立ち上げることが出来るがhostネットワークとして動作するのでhostネットワークモードを想定していないとまぁかなりめんどう
既存のイメージにipコマンド等ネットワーク系のコマンドが入っていればnginxコンテナのIPアドレスを名前解決し、それを/etc/hosts
に書き込むという方法もあるがdocker-seleniumには残念ながら入っていなかったので今回はdocker networksを使う
最終的にはdocker-compose.ymlは以下のような形になる
services:
nginx:
image: nginx:latest
ports: "80:80"
networks:
- app
app:
image: your/image
networks:
- app
chrome:
image: selenium/node-chrome:3.14.0-europium
depends_on:
- selenium-hub
environment:
- HUB_HOST=selenium-hub
- HUB_PORT=4444
networks:
- app
extra_hosts:
- "admin.example.com:172.30.0.1"
- "example.com:172.30.0.1"
networks:
app:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.30.0.0/16
- gateway: 172.30.0.1
解説
-
- gateway: 172.30.0.1
でこのサブネットのゲートウェイを設定している。このゲートウェイというのは実際はdockerホストとのインターフェイスなので172.30.0.1にアクセスする=ホストにアクセスするということになる - 各コンテナに所属するサブネットを
networks
で指定。全コンテナをappネットワークに所属させる
networks:
- app
- chromeコンテナの
extra_hosts
で追加のホストをhostsファイルに書き込む
extra_hosts:
- "admin.example.com:172.30.0.1"
- "example.com:172.30.0.1"
これによりchromeコンテナはhttp://admin.example.com/
をリクエストするとnginxコンテナにリクエストが飛ぶようになりE2Eが可能になる
この方法の利点は
- CIサービスなどでdocker-composeの構成をそのまま使えない場合でもchromeが名前解決さえ出来ればいいので大抵はhostsを書き換えるだけで済む
- テストコードを変更しないでも環境側だけで対応出来る
- 任意のホスト名を指定出来る
といったあたり。テストコードでホスト名を切り替えるのはテストコードが複雑になるので環境で吸収する方がいい。
ちなみにchromeは.dev
TLDに対しては最近強制的にHTTPSへリダイレクトするようになってしまったので.example
, .test
といった予約済みのテスト用TLDを使うことをおすすめする
まとめ
Docker環境上でSelenium (ChromeDriver) + Headless ChromeでPhoenixでE2Eテストをした。
自前でChromeDriverとChromeのバイナリを用意してメンテするのは結構面倒なので公式のイメージでサクっと出来るのはありがたかった。
またPhantomJSなどは対応していないWebsocketなどもChromeだと確認出来るのでPhoenix Channels周りのE2Eテストなどでも重宝しそう。
PhantomJSが正式に開発中止したこともあるのでHeadless Chromeに乗り換えを検討している人は使ってみてはどうか。