この記事なんやねん
みなさん、Rspecで統合テスト(System Spec)書いてますか?
すっかりSeleniumを使ってヘッドレスブラウザを使った統合テストが主流になりましたが、Docker
とHeadless Chrome
を使ったやり方が意外とヒットしなかったので投稿します。
手順
ここから早速始めていきましょう。因みに、以下の環境は揃っているという前提で進めます。
- Dockerとdocker-composeをインストール済み
- docker-composeを使ってRails環境を構築済み
- Rspec実行環境を構築済み
もし、 環境構築まだやねん って状態だったら、こちらの記事を参考に開発環境を作ってみてください。
イメージの準備
Dockerfileにheadless chromeドライバをインストールしてもいいのですが、正直めんどくさいです。
brew
で簡単にインストールできるのにDocker使った方が環境構築めんどくさいのも本末転倒な感じするので、Dockerhubにイメージ上がってないか検索したら案の定ありました。
seleniumがイメージを提供してくれてるようですね。因みに、googleはイメージ提供していないようなので、seleniumさんのイメージをありがたく使いましょう。
サービス追加
docker-composeに以下のように既存サービスの一番下にサービスを追加してください。
version: '3'
services:
### 省略 ###
chrome:
image: selenium/standalone-chrome
ports:
- "4444:4444"
これだけです。やっぱりイメージ使えば楽ですなあ。
Rspecの設定
次に、Rspecの設定をしていきます。Capybaraとrails_helperを設定するだけで済むので簡単です。
Gemfileの修正
もしまだライブラリをインストールしていなければ以下を追記してから、bundle install
してください。
group :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem "database_cleaner"
gem "factory_bot_rails"
gem "rspec-rails"
# 下記を追加
gem 'capybara'
gem 'selenium-webdriver'
gem 'rspec-retry'
end
Capibaraの設定
下記のファイルを作成してください。ポート番号は他でもいいですよ。
Capybara.default_driver = :selenium_chrome
Capybara.javascript_driver = :selenium_chrome
Capybara.server_host = Socket.ip_address_list.detect(&:ipv4_private?).ip_address
Capybara.server_port = 3001
Capybara.default_max_wait_time = 5
Capybara.ignore_hidden_elements = true
Capybara.register_driver :selenium_chrome do |app|
opts = {
desired_capabilities: :chrome,
browser: :remote,
url: "http://chrome:4444/wd/hub",
}
Capybara::Selenium::Driver.new(app, opts)
end
rails_helperの修正
下記のようにrails_helperを修正してください。
なお、spec/support/system_support.rb
というSystemスペック用のヘルパーを
作成しなければ任意ってコメントしてる設定は不要です。
### 省略 ###
# コメントアウトされてるので、コメントアウトを外してください
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
RSpec.configure do |config|
### 省略 ###
# Systemスペック用のヘルパー。任意です。
config.include SystemSupport, type: :system
# 名称を一意にするために設定。任意かな。
config.before(:all, type: :system) do
timestamp!
end
# Systemスペックは不安定なのでリトライ用の設定。
config.verbose_retry = true
config.display_try_failure_messages = true
config.default_retry_count = 3
# 下記を追加
config.before(:each, type: :system) do
driven_by Capybara.default_driver
end
config.before(:each, type: :system, js: true) do
driven_by Capybara.javascript_driver
host! "http://#{Capybara.server_host}:#{Capybara.server_port}"
end
end
ヘルパーの作成(任意)
Systemスペックは共通の処理(ログイン処理とか)が多いので、ヘルパーを用意しておくと便利です。
任意と書いていますが、作成しておいて損はないと思います。
module SystemSupport
# 一意の名称を作成するために実行時のtimestampを、
# 数字でインスタンス変数に格納するsetterです。地味に便利。
def timestamp!(timestamp = Time.now.to_i)
@timestamp = timestamp
end
# getterです
def timestamp
@timestamp
end
# ブロックの結果がtrueになるまでループするメソッド。すげえ使う。
def wait_until(wait_time = Capybara.default_max_wait_time)
Timeout.timeout(wait_time) do
loop until yield
end
end
# 特定のcssが登場する、もしくは、なくなるまでループするメソッド
def wait_for_css(selector, wait_time = Capybara.default_max_wait_time, non_display: false)
Timeout.timeout(wait_time) do
loop until send((non_display ? :has_no_css? : :has_css?), selector)
end
yield if block_given?
end
# 非同期通信が終わるまでループするメソッド
def wait_for_ajax(wait_time = Capybara.default_max_wait_time)
Timeout.timeout(wait_time) do
loop until page.evaluate_script("jQuery.active").zero?
end
yield if block_given?
end
end
System Specの作成
ここまでで設定は完了してるので実際にSystem Specを書いていきましょう。
私のサンプルソースではこんな感じで書いてます。自分のソースコードに合わせて適宜書き直してください。
require "rails_helper"
# typeはsystemを設定、Javascriptも使うのでjsもtrueにしておく。
RSpec.describe "HelloWorlds", type: :system, js: true do
# 最初にテストデータ作成
before(:all) {
create(:hello_world, country: "JP", hello: "こんにちわ世界", priority: 1, file_name: "jp.jpeg")
create(:hello_world, country: "US", hello: "Hello World", priority: 2)
create(:hello_world, country: "CN", hello: "你好 世界", priority: 3)
}
before(:each) {
visit root_path
# コンテンツが全て表示されるまで待つ
wait_until { (page.all("div.portfolio-item").count == 3) }
}
context "when go to index page" do
it "show contents" do
expect(page).to have_css("h1", text: "Hello World")
content = page.first("div.portfolio-item")
expect(content.first("p.card-text").text).to eq("こんにちわ世界")
expect(content.first("h4.card-title")).to have_link("日本")
expect(content.first("img.card-img-top")[:src]).to match(/jp\.jpeg/)
end
end
context "when go to Create page" do
it "create content" do
page.first("#new_hello_world").click
# タイトル出るまで待つ
wait_until { page.has_css?("h3", text: "New Helloworld") }
select "ドイツ", from: "hello_world_country"
# 一意の名称で検索してテストデータを作成
fill_in "hello_world_hello", with: "Hallo Welt #{timestamp}"
fill_in "hello_world_priority", with: 4
click_on "Submit"
# メッセージ出るまで待つ
wait_until { page.has_content?("Hello world was successfully created.") }
content = page.first("div.form").all("label.form-control")
expect(content[0].text).to eq("ドイツ")
expect(content[1].text).to eq("Hallo Welt #{timestamp}")
expect(content[2].text).to eq("4")
end
end
end
テスト実行
早速テストを実行してみましょう。
下記のweb
はサービス名なので適宜自分の設定してるサービス名に変更して実行してください。
% docker-compose run --rm web rspec spec/system/hello_worlds_spec.rb
Starting hr_chrome_1 ... done
Starting hr_db_1 ... done
Capybara starting Puma...
* Version 4.1.0 , codename: Fourth and One
* Min threads: 0, max threads: 4
* Listening on tcp://192.168.176.4:3001
..
Finished in 1 minute 1.58 seconds (files took 3.71 seconds to load)
2 examples, 0 failures
通りましたね、やったぜ
まとめ
Dockerでも簡単にHeadless Chromeを使った統合テスト環境を実現できました。
テストだけじゃなくてスプレイピングにも利用できるので試してみてください。
因みに、今回使ったサンプルソースはこちらのリポジトリになるので参考までにどうぞ。