DockerのChromeでCupriteの動作を検証してみる
はじめに
Seleniumの調査だったり開発環境がApple Siliconだったりで考慮しないといけない事が多いのでSystemTestの設定を後回しにしていたRailsアプリがあったのだが、CupriteというSeleniumではなくCDP(Chrome DevTools Protocol)を使用したGemの存在を知ったのがきっかけでやっぱりきちんとやろうという事になり、まずは勉強がてら一旦Railsから離れてCupriteとDockerのChormeで環境を作って試してみた。
詳細
Gemなど
Ferrum
RubyでCDPを使うためのGem。Ferrumとはラテン語で鉄の意味らしい。
名前がラテン語である理由やGItHubのトップにあるイラストがMUCのエンドゲームでサノスが使っていたインフェニティティガントレットっぽく見えるのが気になる。
Cuprite
CapybaraでFerrumを使うためのGem。
CDP(Chrome DevTools Protocol)
Cupriteを知るまでその存在を知らなかったのだがプログラムから呼び出せるChromeに実装されたAPI。E2ETestなどではSeleniumを代替出来るのでCuprite & Ferrumはそのアプローチを取っている。
やったこと
疎通のテストなので単純なリクエストを返してくれるRackアプリをテスト用に作成。 最終的にCapybaraを通してJavaScriptの実行を試したいので検証用のボタンも追加しておく。
config.ru
class App def call(env) html = "<html><head><script>function changeText() { document.querySelector('#text').textContent = 'Change Text'; }</script></head><body><h1 id='text'>Hello World!</h1><button onclick=changeText()>Click</button></body></html>" [200,{ "Content-Type" => "text/html" },[html]] end end run App.new
DockerはWebアプリを起動するためのRubyとChromeを別コンテナで立ち上げている。
version: "3.9" services: web: build: context: . command: ["bundle", "exec", "rackup", "-p", "80", "-o", "0.0.0.0"] ports: - "80:80" volumes: - ./app:/app depends_on: - chrome chrome: image: browserless/chrome ports: - "3333:3333" volumes: - .:/app:cached environment: PORT: 3333 CONNECTION_TIMEOUT: 600000
Rubyのコンテナでは必要なGemをbuild時にインストールしておりRuby3からWEBRickが標準ライブラリでなくなったのでPumaを追加している。
# frozen_string_literal: true source "https://rubygems.org" gem "rack" gem "puma" gem "cuprite" gem "test-unit" gem "debug"
FROM ruby:3.1.1 RUN mkdir /app WORKDIR /app ADD ./app /app RUN gem install bundler RUN bundle install
$ docker compose build $ docker compose up -d
Dockerのビルドとコンテナの起動が済んだら動作を一つずつ確認していく。
Webアプリコンテナ <-> Chromeコンテナ
Rackアプリの起動の確認とWebコンテナからChormeのコンテナにアクセス出来るかを確認しておく
$ docker compose exec web curl 'http://localhost:80' <html><head><script>function changeText() { document.querySelector('#text').textContent = 'Change Text'; }</script></head><body><h1 id='text'>Hello World!</h1><button onclick=changeText()>Click</button></body></html>% $ docker compose exec web curl 'http://chrome:3333' <!doctype html><html><head><meta charset="utf-8"/><title>browserless debugger</title>...
Chrome経由でWebアプリに接続するのでChromeのコンテナからホストネームでアクセス出来るかも確認しておく
$ docker compose exec chrome curl 'http://web:80' <html><head><script>function changeText() { document.querySelector('#text').textContent = 'Change Text'; }</script></head><body><h1 id='text'>Hello World!</h1><button onclick=changeText()>Click</button></body></html>%
Ferrum -> Chromeコンテナ
WebコンテナからFerrum経由でChromeにアクセス出来るか確認
options = { url: 'http://chrome:3333', browser_options: { 'no-sandbox': nil } } browser = Ferrum::Browser.new(options) browser.go_to("https://google.com") puts browser.current_title browser.quit
$ docker compose exec web ruby ferrum.rb Google
Capybra -> Chromeコンテナ -> Webアプリ
Capybara経由でChromeを通してRackアプリに接続出来るか確認
require "capybara/cuprite" require 'capybara/dsl' options = { url: 'http://chrome:3333', browser_options: { 'no-sandbox': nil }, base_url: "http://web" } Capybara.register_driver(:cuprite) do |app| Capybara::Cuprite::Driver.new(app,options) end Capybara.javascript_driver = :cuprite Capybara.server_host = "0.0.0.0" Capybara.app_host = "http://web:80" session = Capybara::Session.new(:cuprite) driver = session.driver browser = driver.browser page = browser.page browser.visit "/" puts page.body
今回ChromeもDockerにした上に似た名前の項目が多いので設定が紛らわしいが Capybara::Cuprite::Driver.new
のurlにはChromeの宛先をCapybara.app_host
にはWebアプリケーションの宛先を設定する。
その他はCapybaraからvisit "/"
のようにホスト名を省略してページを選択したければ
Capybara::Cuprite::Driver.new
にbasu_url
を渡す。
browser_options: { 'no-sandbox': nil }
はDocker経由の場合必要とのこと。
Capybara.server_host
もDocker経由の場合 "0.0.0.0" を指定して外部からのアクセスを許可してやる必要がある。
この内容を保存して実行してやるとCapybara経由でRackアプリに接続出来る事が確認できる。
$ docker compose exec web bundle exec ruby capybara.rb <html><head><script>function changeText() { document.querySelector('#text').textContent = 'Change Text'; }</script></head><body><h1 id="text">Hello World!</h1><button onclick="changeText()">Click</button></body></html>
TestCase
最後にテストケースを作成してSystemTestでの実行を試す。
# 必要な設定は上のCapybaraのテストとほぼ同じだが、そのままだとRackサーバーを立ち上げようとするので以下の設定を追記 Capybara.default_driver = :cuprite require 'test/unit' require 'capybara/dsl' class SystemTestCase < Test::Unit::TestCase include Capybara::DSL def test_foo visit '/' assert_equal(page.find('h1').text, 'Hello World!') page.click_on 'Click' assert_equal(page.find('h1').text, 'Change Text') end end
$ docker compose exec web ruby test_case.rb Loaded suite test_case Started . Finished in 0.319190042 seconds. -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 3.13 tests/s, 6.27 assertions/s
JavaScriptの実行も確認出来た
つまずいたところ
途中上記の設定で実行しても以下のような空のHTMLで返ってきて困った。
<html><head></head><body></body></html>
原因は意外なところでDockerComposeで指定していたサービス名が理由だった。 こちらを参考にしてapp -> webに変えたらうまく動いた。エラー内容も違うし詳細な理由はわからないのが、とりあえずはDockerComposeのService名はappにしないほうが無難なのだろうか
まとめ
SystemTestはブラウザが必要だったりUnitTestよりも複雑でそこにDockerや新しく使うGemが絡んでくると確信のないまま設定をあれこれいじくる事になりがちなので一旦小さな構成で試してみた。 とりあえず迷った時に立ちかえられる構成が出来たので次はRailsアプリに組み込んでいきたい。