はじめに
先日、RSpec 3.7がリリースされました。
参考: RSpec 3.7 has been released!
上記ブログの中で「今回のリリースはRailsのSystem Testの統合機能をいち早く使ってもらうためのリリースだ」と書いてあります。
実際、ブログの中で触れられている新機能は「System Spec」機能の追加だけです。
というわけで、この記事はrspec-rails 3.7で導入されたSystem Specの紹介と使い方の説明をしていきます。
実行環境
この記事は以下のバージョンを対象にして書かれています。
- rspec-rails 3.7.1
- Rails 5.1.4
- Ruby 2.4.2
- selenium-webdriver 3.6.0
- Capybara 2.15.4
- Chrome 62
- ChromeDriver 2.33
サンプルコード
この記事で使用したコードはこちらに置いてあります。
System Specとは?
Rails 5.1ではSystemTestCaseという新機能が導入されました。
これはいわゆるエンドツーエンド(E2E)テストを実行するための新機能です。
このテストケースクラスを使うと、JavaScriptを利用する画面のテストが書けるようになります。
参考: Rails 5.1のSystemTestCaseを試してみた - Qiita
RailsではMinitestがデフォルトのテスティングフレームワークであるため、SystemTestCaseでもMinitestを使います。
rspec-rails 3.7で導入されたSystem Specは、このSystemTestCaseをRSpecから利用するための新機能です。
Feature Specと何が違うの?どっちを使えばいいの?
rspec-railsではすでにFeature Spec(フィーチャスペック)というE2Eテスト用の機能が用意されています。
System SpecもE2Eテスト用の機能なので、単純に考えるとE2Eテスト用の機能が複数存在することになります。
実際、この両者はよく似ているのですが、System SpecはRails標準のSystemTestCaseをラップしているため、以下のような違いがあります。
- デフォルトでselnium-webdriver + Chrome上でテストが実行される。
- DatabaseCleanerやDatabaseRewinderを使ってトランザクション管理をする必要がない。
- テストが失敗すると自動的にtmp/screenshotsディレクトリにスクリーンショットを保存してくれる。
まだ本格的には使い込んでいませんが、ざっと見た感じでは「もうFeature SpecやDatabaseCleanerはいらなくなるのでは?」と思っています。
冒頭で紹介した公式ブログでも「Rails 5.1を使っている場合はFeature SpecよりもSystem Specの使用を推奨します」と書いてあります。
System Specを使ってみる
ではここから、System Specを実際に使う方法を説明していきます。
使用するサンプルアプリケーション
今回は以下のようなサンプルアプリケーションでSystem Specを実行してみます。
このアプリケーションでは郵便番号を入力すると、JavaScriptによって自動的に住所が入力されるようになっています。
コードはGitHubに置いてあります。
ChromeとChromeDriverをインストールする
System SpecではChromeとChromeDriverをマシンにインストールする必要があります。
どちらも最新版をインストールするようにしましょう。
ChromeDriverのインストールはいくつか方法があります。
- 自分でダウンロードしてPATHの通ったディレクトリに置く。
- Homebrewでインストールする。
# 新規インストール
$ brew install chromedriver
# 最新版にアップデート
$ brew upgrade chromedriver
-
chromedriver-helper gemを使う。webdrivers gemを使う
今回は一番手軽なchromedriver-helper gemを使うことにします。Gemfileにchromedriver-helperを追加して、bundle install
してください。
chromedriver-helperはサポートが終了しました。
代わりにwebdriversの使用が推奨されています。
詳しくは以下の記事をご覧ください。
サポートが終了したchromedriver-helperからwebdrivers gemに移行する手順 - Qiita
Rails 5.1以上であることを確認する
System SpecはRailsのSystemTestCaseを利用するため、Rails 5.1以上であることが必須です。
gem 'rails', '~> 5.1.4'
テスト関連のgemをインストール/アップデートする
テストに必要なgemを追加して、bundle install
します。
group :development, :test do
# ...
gem 'rspec-rails'
end
group :test do
# ...
gem 'capybara'
gem 'selenium-webdriver'
end
これまでRSpecを使っていなかった場合は、以下のコマンドで必要なファイルも追加してください。
$ rails generate rspec:install
既存のプロジェクトですでにテスト用のgemがインストールされていた場合は、以下のコマンドで各種gemを最新版にアップデートしておきましょう。
$ bundle update rspec-rails capybara selenium-webdriver
System Specを書く
System Specを配置するspec/systemディレクトリを作ります。
$ mkdir spec/system
今回はusers_spec.rb
というファイルにSystem Specを書きます。
$ touch spec/system/users_spec.rb
users_spec.rb
に次のようなコードを書きます。
require 'rails_helper'
RSpec.describe 'Users', type: :system do
before do
@user = User.create!(name: 'いとう')
end
it 'completes yubinbango automatically with JS' do
# User編集画面を開く
visit edit_user_path(@user)
# Nameに"いとう"が入力されていることを検証する
expect(page).to have_field '名前', with: 'いとう'
# 郵便番号を入力
fill_in '郵便番号', with: '158-0083'
# 住所が自動入力されたことを検証する
expect(page).to have_field '住所', with: '東京都世田谷区奥沢'
# 更新実行
click_button 'Update User'
# 正しく更新されていること(=画面の表示が正しいこと)を検証する
expect(page).to have_content 'User was successfully updated.'
expect(page).to have_content 'いとう'
expect(page).to have_content '158-0083'
expect(page).to have_content '東京都世田谷区奥沢'
end
end
3行目でtype: :system
と書いてあるのがポイントです。
これでこのテストがSystem Specであることを宣言しています。
それ以外は基本的にFeature Specと書き方は変わりません。
System Specを実行する
テストが書き終わったら、テストを実行してみましょう。
実行方法は通常のRSpecの場合と変わりません。
$ rails rspec
セットアップがうまくいっていれば、自動的にChromeが起動してSystem Testで書いた内容を実行してくれるはずです。
Feature Specでは必要だったDatabaseCleaner/DatabaseRewinder用の設定や、rails_helper.rb
でのCapybara.javascript_driver
の指定は不要になります。
応用編
describe/itではなく、feature/scenarioを使う
サンプルコードではdescribe/itを使っていますが、Feature Specと同様にfeature/scenarioを使って書くこともできます。
require 'rails_helper'
RSpec.feature 'Users', type: :system do
background do
@user = User.create!(name: 'いとう')
end
scenario 'completes yubinbango automatically with JS' do
# User編集画面を開く
visit edit_user_path(@user)
# Nameに"いとう"が入力されていることを検証する
expect(page).to have_field '名前', with: 'いとう'
# 郵便番号を入力
fill_in '郵便番号', with: '158-0083'
# 住所が自動入力されたことを検証する
expect(page).to have_field '住所', with: '東京都世田谷区奥沢'
# 更新実行
click_button 'Update User'
# 正しく更新されていること(=画面の表示が正しいこと)を検証する
expect(page).to have_content 'User was successfully updated.'
expect(page).to have_content 'いとう'
expect(page).to have_content '158-0083'
expect(page).to have_content '東京都世田谷区奥沢'
end
end
feature/scenarioを使う際はmetadata[:type]の扱いに注意する(2019.11.27追記)
RSpecではmetadataを利用して、beforeフックに共通処理を書いたりすることができます。
RSpec.configure do |config|
config.before(:each) do |example|
if example.metadata[:type] == :system
# typeがsystem(つまりシステムスペック)のときに実行したい処理を書く
end
end
# もしくはmetadataを直接参照するのではなく、type: :system のように引数で渡してもOK
config.before(:each, type: :system) do |example|
# typeがsystem(つまりシステムスペック)のときに実行したい処理を書く
end
end
このとき、以下のようなテストを書いていると、metadata[:type]
に :system
と :feature
が混在することになります。
require 'rails_helper'
RSpec.describe 'Blogs', type: :system do
it 'shows index page (it)' do
# metadata[:type] が :system
end
scenario 'shows index page (scenario)' do
# metadata[:type] が :system
end
feature 'within feature' do
it 'shows index page (it)' do
# metadata[:type] が :feature(あれ?)
end
scenario 'shows index page (scenario)' do
# metadata[:type] が :feature(あれ?)
end
end
end
上のコードを見てもらえばわかるとおり、一番外側のdescribeでtype: :system
を指定していても、feature
のブロックで囲まれるとその中のexampleはmetadata[:type]
が:feature
になってしまいます。
つまり、「一番外側のdescribeでtype: :system
と書いたので、中のテストも全部 metadata[:type] == :system
になってるはず」と思い込むと痛い目を見ます。
というわけで、rails_helper.rb
などでmetadata[:type]
を利用している場合は、次のようにテスト全体をdescribe/it
に統一した方が、不要なバグを回避しやすいはずです。
require 'rails_helper'
RSpec.describe 'Blogs', type: :system do
it 'shows index page (it)' do
# metadata[:type] が :system
end
describe 'within describe' do
it 'shows index page (it)' do
# metadata[:type] が :system
end
end
end
もし、どうしてもfeature/scenario
を残しておきたい場合は、metadata[:type]
を参照する側で対処するようにしてください。
RSpec.configure do |config|
config.before(:each) do |example|
if example.metadata[:type].in?(%i[system feature])
# typeがsystemまたはfeatureのときに実行したい処理を書く
end
end
end
なお、次のように引数で複数のtypeを指定したりすることはできないようです。
# NG!!(エラーは起きないが、こちらの期待した動きにはならない)
config.before(:each, type: %i[system feature]) do |example|
ヘッドレスモードのChromeで実行する
ヘッドレスモード(画面を起動しないモード)でChromeを実行する場合は、rails_helper.rb
に次の設定を追加します。
RSpec.configure do |config|
# ...
config.before(:each) do |example|
if example.metadata[:type] == :system
caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {"args" => %w(--headless --disable-gpu)})
driven_by :selenium, using: :chrome, screen_size: [1400, 1400], options: { desired_capabilities: caps }
end
end
end
2017.10.29 追記: 上の設定をもっとシンプルに書く
こちらのブログを参考にしたら、次のように簡単に書くことができました。
RSpec.configure do |config|
# ...
config.before(:each) do |example|
if example.metadata[:type] == :system
driven_by :selenium_chrome_headless, screen_size: [1400, 1400]
end
end
end
2018.7.1 追記: Rails 5.2 + rspec-rails 3.7の場合
Rails 5.2.0 + rspec-rails 3.7.2の環境だと、オプションの渡し方が次のように変更されていました(参考)。
RSpec.configure do |config|
# ...
config.before(:each) do |example|
if example.metadata[:type] == :system
driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
end
end
end
js: trueのときだけ、Chromeを起動する
System Specは毎回Chromeが起動しますが、JavaScriptがなくても実行可能なテストであれば、Chromeなしで検証した方が速度面で有利です。
rails_helper.rb
に次のような設定を書けば、Feature Specのときと同じように、js: true
のタグが付いているときだけChromeが起動するようになります。
RSpec.configure do |config|
# ...
config.before(:each) do |example|
if example.metadata[:type] == :system
if example.metadata[:js]
driven_by :selenium_chrome_headless, screen_size: [1400, 1400]
else
driven_by :rack_test
end
end
end
end
System SpecにもJSを使う必要があるテストにだけ、js: true
のタグを付けます。
require 'rails_helper'
RSpec.describe 'Users', type: :system do
# ...
it 'completes yubinbango automatically with JS', js: true do
# JSを使う必要があるテストを書く
end
it 'does not complete yubinbango automatically without JS' do
# JSを使わずに済むテストを書く
end
end
テスト失敗時のスクリーンショットを確認する
テストが途中で失敗した場合は、tmp/screenshotsディレクトリにスクリーンショットが保存されます。
CIでSystem Specを実行する
参考までにTravis CIとCircleCIでSystem Specを実行する場合の設定例を載せておきます。
いずれも「Chromeはヘッドレスモードで起動」「ChromeDriverはchromedriver-helperでインストール」する場合の設定です。
必要なのは最新版のChromeをインストールすることだけですね。
sudo: required
dist: trusty
language: ruby
rvm:
- 2.4.2
cache: bundler
bundler_args: --without production --deployment
before_install:
- gem install bundler --pre
# Install latest Chrome
- sudo apt-get update
- sudo apt-get install -y libappindicator1 fonts-liberation
- export CHROME_BIN=/usr/bin/google-chrome
- wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- sudo dpkg -i google-chrome*.deb
script:
- bundle exec rspec spec
general:
artifacts:
- "tmp/capybara"
machine:
timezone:
Asia/Tokyo
ruby:
version:
2.4.2
test:
override:
- bundle exec rspec spec
dependencies:
pre:
# Install latest Chrome
- sudo apt-get update
- sudo apt-get install -y libappindicator1 fonts-liberation
- export CHROME_BIN=/usr/bin/google-chrome
- wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- sudo dpkg -i google-chrome*.deb
こちらはCircleCI 2.0用の設定例です。
version: 2
jobs:
build:
docker:
- image: circleci/ruby:2.4.2-node-browsers
working_directory: ~/rails-5-1-sandbox
steps:
- checkout
- run:
name: Install System Dependencies
command: |
sudo apt-get update
sudo apt-get install -y libappindicator1 fonts-liberation
export CHROME_BIN=/usr/bin/google-chrome
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome*.deb
- restore_cache:
keys:
- v1-dependencies-{{ checksum "Gemfile.lock" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run:
name: Bundle Install
command: |
bundle install --jobs=4 --retry=3 --path vendor/bundle
- save_cache:
paths:
- vendor/bundle
key: v1-dependencies-{{ checksum "Gemfile.lock" }}
- run:
name: Create DB
command: bundle exec rake db:create db:schema:load --trace
- run:
name: Run Tests
command: RAILS_ENV=test bundle exec rspec
- store_test_results:
path: /tmp/test-results
CircleCI 2.0の設定例についてはこちらのブログも参考になると思います。
まとめ
というわけで、この記事ではrspec-rails 3.7で導入されたSystem Specの使い方を説明しました。
従来のFeature Specと一緒と言えば一緒なんですが、System SpecはRailsが標準で用意してくれているSystemTestCaseをラップしてくれているので、そのぶん手軽に導入でき、なおかつスクリーンショットの自動撮影のような便利な機能も使えるようになっています。
僕自身はまだ本格的には使い込んでいないものの、これなら今後は全部System Specに置き換えていっても大丈夫そうだなと感じています。
みなさんもぜひrspec-rails 3.7のSystem Specを試してみてください!
あわせて読みたい
System Specでもブラウザの実行や画面要素の検証ではCapybaraを使います。
Capybaraの詳しい使い方についてはこちらの記事が参考になると思います。
「そもそもRSpecがよくわかってないんだけど・・・」という方は、こちらの入門記事をご覧ください。