patorashのブログ

方向性はまだない

docker-composeでseleniumを使っている際のCapybaraの設定について

以前にこんな記事を書きました。

patorash.hatenablog.com

今回はこれの続きみたいなものです。 まだCircleCIでのテスト実行まで至らず、ローカルでテストが全部通ることを目標に調整中です。 featurespec以外のテストは完走したので、問題はfeaturespecのみ。 ということで、Capybaraの設定の見直しです。

エラーの傾向を見る

落ちたエラーを確認したところ、

  • 画像をダウンロードしようとしてURLが間違っていてエラー
  • ファイルのダウンロードが完了したことを検出できずにエラー
  • ChromeのSession IDが不正

みたいなのが大半でした。

特に最後のが結構困ったもので、これが起きるとrspecの後処理が完走せずに次のテストに行ってしまって巻き込みエラーになっているみたいなので、どうしたものか、という状況です。

Capybaraの設定を見直す

Before

とりあえずBeforeのやつを貼ります。spec/support/capybara.rbです。 WaitForDownloadモジュールは私が作っているファイルのダウンロードを待つためのモジュールです。

詳しくは私が書いたQiitaの記事を…。

qiita.com

Capybara.register_driver :selenium do |app|
  options = Selenium::WebDriver::Chrome::Options.new
  options.headless!
  options.add_argument '--disable-gpu'
  options.add_argument '--window-size=1680,1050'
  options.add_argument '--blink-settings=imagesEnabled=false'
  options.add_argument '--lang=ja'
  # options.add_argument '--no-sandbox'
  # options.add_argument '--no-zygote'
  driver = Capybara::Selenium::Driver.new(app,
                                          url: ENV.fetch('SELENIUM_DRIVER_URL'),
                                          browser: :remote,
                                          options: options,
                                          desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
                                            login_prefs: { browser: 'ALL' },
                                            loggingPrefs: { browser: 'ALL' },
                                          ),
  )
  bridge = driver.browser.send(:bridge)

  path = "session/#{bridge.session_id}/chromium/send_command"

  bridge.http.call(:post, path, cmd: 'Page.setDownloadBehavior',
                                params: {
                                  behavior: 'allow',
                                  downloadPath: WaitForDownload::PATH,
                                })
  driver
end

Capybara.configure do |config|
  config.server_host = "test" # <= chromeから見た、テストを実行するイメージ名を指定
  config.server_port = 9887 + ENV['TEST_ENV_NUMBER'].to_i
  config.app_host = "http://#{config.server_host}:#{config.server_port}" # <= Seleniumが接続するテストを実行するAppHostのURL

  config.javascript_driver = :selenium
  config.default_max_wait_time = ENV['CI'].present? ? 15 : 5
  config.ignore_hidden_elements = true
  config.server = :puma, { Silent: true }
end

画像のダウンロードをしない設定を調査

Capybaraの設定は特に変えていないにも関わらず、画像をダウンロードしようとするということは、リモートのChromeに設定が渡ってないってことか?と思い、調査開始。 すると、desired_capabilitiesオプションで設定するっぽい感じの情報を見つけました。

qiita.com

これを参考にして、desired_capabilitiesに対してcrhomeOptionsを定義してそちらにChromeに関するオプションの配列を渡したところ、動き始めました🎉

よりよい書き方を模索

しかし、Selenium::WebDriver::Chrome::Optionsクラスがあるにも関わらず、オプションを配列にして渡すかね?と思って綺麗な書き方を模索するためにコードを読みました。

selenium/options.rb at master · SeleniumHQ/selenium · GitHub

すると、as_jsonメソッドを発見。これを実行すると、goog:chromeOptionsというKeyを持つHashを作ってくれます(as_jsonとは?🤔)これを、desired_capabilitiesに設定するようにしたところ、リモートのChromeにもいい感じに反映されました。👍👍👍

この時点でのregister_driverだけを書き出します。

Capybara.register_driver :selenium do |app|
  chrome_options = Selenium::WebDriver::Chrome::Options.new
  chrome_options.headless!
  %w(
    no-sandbox
    disable-gpu
    window-size=1440,900
    disable-desktop-notifications
    disable-extensions
    blink-settings=imagesEnabled=false
    lang=ja
  ).each { |option| chrome_options.add_argument(option) }
  capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(chrome_options.as_json)
  driver = Capybara::Selenium::Driver.new(
      app,
      url: ENV.fetch('SELENIUM_DRIVER_URL'),
      options: chrome_options,
      browser: :remote,
      desired_capabilities: capabilities,
  )

  bridge = driver.browser.send(:bridge)
  path = "session/#{bridge.session_id}/chromium/send_command"
  bridge.http.call(:post, path, cmd: 'Page.setDownloadBehavior',
                                params: {
                                  behavior: 'allow',
                                  downloadPath: WaitForDownload::PATH,
                                })
  driver
end

ダウンロードしたファイルを検出したい

ファイルをダウンロードするまで待つ、という処理をしているのですが、rspecを実行しているコンテナとchromeが動いているコンテナは異なるため、ダウンロードしたファイルはchromeのコンテナ内に保存され、rspecのコンテナでは見つかりません。そりゃそうですね。そこで、docker-compose.ymlを修正して、ホストOSとファイル共有するようにします。chromeコンテナでダウンロードしたファイルをrspecのコンテナからも参照できるようにするわけです。

version: '3.3'

services:
  # 略
  chrome:
    image: selenium/standalone-chrome:latest
    ports:
      - '4444:4444'
    volumes:
      - ./tmp/download:/tmp/download
  # 略

これで、ダウンロードしたファイルを検出できるようになりました!

ダウンロードファイルのパス指定方法が変わったらしい

これを調べているときに、メドピアさんのブログを見かけて、ダウンロードのファイルパス指定方法が変わったことを知りました。

tech.medpeer.co.jp

こっちのほうがわかりやすいので、変更しました。テストを実行しても問題なくファイルがダウンロードされていたのでOK。コードが簡潔になりました👍

この時点でのregister_driverを書き出します。

Capybara.register_driver :selenium do |app|
  chrome_options = Selenium::WebDriver::Chrome::Options.new
  chrome_options.headless!
  %w(
    no-sandbox
    disable-gpu
    window-size=1440,900
    disable-desktop-notifications
    disable-extensions
    blink-settings=imagesEnabled=false
    lang=ja
  ).each { |option| chrome_options.add_argument(option) }
  # ダウンロードディレクトリを設定
  chrome_options.add_preference(:download, default_directory: "/tmp/download")
  capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(chrome_options.as_json)
 Capybara::Selenium::Driver.new(
      app,
      url: ENV.fetch('SELENIUM_DRIVER_URL'),
      options: chrome_options,
      browser: :remote,
      desired_capabilities: capabilities,
  )
end

bridge云々のコードがなくなってわかりやすくなりました。

並列化に対応したい

弊社ではparallel_testsを使っているので、ファイルのダウンロードが並列に行われても影響がないようにしたいので、ダウンロードパスを修正します。

# ダウンロードディレクトリを設定
chrome_options.add_preference(:download, default_directory: "/tmp/download/#{ENV.fetch('TEST_ENV_NUMBER', '1')}")

こうすることで、並列実行したら、

  1. /tmp/download/1
  2. /tmp/download/2
  3. /tmp/download/3

のようにディレクトリができるようになります。

併せて、wait_for_download.rbも修正しました。

module WaitForDownload
  PATH = Rails.root.join("tmp/download/#{ENV.fetch('TEST_ENV_NUMBER', '1')}")
  # 他は略
end

After

ということで、これがとりあえず今のところのspec/support/capybara.rbの全コードになります。

Capybara.register_driver :selenium do |app|
  chrome_options = Selenium::WebDriver::Chrome::Options.new
  chrome_options.headless!
  %w(
    no-sandbox
    disable-gpu
    window-size=1440,900
    disable-desktop-notifications
    disable-extensions
    blink-settings=imagesEnabled=false
    lang=ja
  ).each { |option| chrome_options.add_argument(option) }
  chrome_options.add_preference(:download, default_directory: "/tmp/download/#{ENV.fetch('TEST_ENV_NUMBER', '1')}")
  capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(chrome_options.as_json)
  Capybara::Selenium::Driver.new(
      app,
      url: ENV.fetch('SELENIUM_DRIVER_URL'),
      options: chrome_options,
      browser: :remote,
      desired_capabilities: capabilities,
  )
end

Capybara.configure do |config|
  config.server_host = "test"
  config.server_port = 9887 + ENV['TEST_ENV_NUMBER'].to_i
  config.app_host = "http://#{config.server_host}:#{config.server_port}"

  config.javascript_driver = :selenium
  config.default_max_wait_time = ENV['CI'].present? ? 15 : 5
  config.ignore_hidden_elements = true
  config.server = :puma, { Silent: true }
end

SessionIDが不正のやつは?🤔

まだ調査中です🥺 Chromeがメモリを使いすぎてクラッシュするみたいな話を見かけたので、window-sizeを調整したりもしているのですが、直りません。

qiita.com

window-size云々よりも、並列化で動かすことでメモリがなくなってクラッシュしている可能性が高いかなと推測しております。 まぁとりあえずシングルプロセスでの完走を目指します🏃‍♂️

追記:調査完了

Chromeへのメモリ割当方法が分かったので記事にしました。

patorash.hatenablog.com