Help us understand the problem. What is going on with this article?

rspec-rails 3.7の新機能!System Specを使ってみた

はじめに

先日、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

サンプルコード

この記事で使用したコードはこちらに置いてあります。

https://github.com/JunichiIto/rails-5-1-system-test-sandbox/tree/rspec-3-7

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によって自動的に住所が入力されるようになっています。

y9W2aFx3zv.gif

コードはGitHubに置いてあります。

https://github.com/JunichiIto/rails-5-1-system-test-sandbox/tree/rspec-3-7

ChromeとChromeDriverをインストールする

System SpecではChromeとChromeDriverをマシンにインストールする必要があります。
どちらも最新版をインストールするようにしましょう。

ChromeDriverのインストールはいくつか方法があります。

# 新規インストール
$ 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以上であることが必須です。

Gemfile
gem 'rails', '~> 5.1.4'

テスト関連のgemをインストール/アップデートする

テストに必要なgemを追加して、bundle installします。

Gemfile
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に次のようなコードを書きます。

spec/system/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に次の設定を追加します。

spec/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 追記: 上の設定をもっとシンプルに書く
こちらのブログを参考にしたら、次のように簡単に書くことができました。

spec/rails_helper.rb
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の環境だと、オプションの渡し方が次のように変更されていました(参考)。

spec/rails_helper.rb
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が起動するようになります。

spec/rails_helper.rb
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ディレクトリにスクリーンショットが保存されます。

Screen Shot 2017-10-24 at 7.02.15.png

CIでSystem Specを実行する

参考までにTravis CIとCircleCIでSystem Specを実行する場合の設定例を載せておきます。

いずれも「Chromeはヘッドレスモードで起動」「ChromeDriverはchromedriver-helperでインストール」する場合の設定です。
必要なのは最新版のChromeをインストールすることだけですね。

.travis.yml
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
circle.yml
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用の設定例です。

.circleci/config.yml
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がよくわかってないんだけど・・・」という方は、こちらの入門記事をご覧ください。

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away