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

## はじめに

先日、RSpec 3.7がリリースされました。

参考: [RSpec 3\.7 has been released\!](http://rspec.info/blog/2017/10/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](https://qiita.com/jnchito/items/4d01f2faa1deee36bd27)

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はいらなくなるのでは？」と思っています。

冒頭で紹介した[公式ブログ](http://rspec.info/blog/2017/10/rspec-3-7-has-been-released/)でも「Rails 5.1を使っている場合はFeature SpecよりもSystem Specの使用を推奨します」と書いてあります。

## System Specを使ってみる

ではここから、System Specを実際に使う方法を説明していきます。

### 使用するサンプルアプリケーション

今回は以下のようなサンプルアプリケーションでSystem Specを実行してみます。
このアプリケーションでは郵便番号を入力すると、JavaScriptによって自動的に住所が入力されるようになっています。

![y9W2aFx3zv.gif](https://qiita-image-store.s3.amazonaws.com/0/7465/41624f11-1b4d-7f62-9d3f-47bbe63ecab4.gif "y9W2aFx3zv.gif")

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

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


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

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

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

- [自分でダウンロード](https://sites.google.com/a/chromium.org/chromedriver/downloads)して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](https://qiita.com/jnchito/items/f9c3be449fd164176efa)

### Rails 5.1以上であることを確認する

System SpecはRailsのSystemTestCaseを利用するため、Rails 5.1以上であることが必須です。

```rb:Gemfile
gem 'rails', '~> 5.1.4'
```

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

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

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

```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を使って書くこともできます。

```ruby
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フックに共通処理を書いたりすることができます。

```ruby
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` が混在することになります。

```ruby
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`に統一した方が、不要なバグを回避しやすいはずです。

```ruby
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]`を参照する側で対処するようにしてください。

```ruby
RSpec.configure do |config|
  config.before(:each) do |example|
    if example.metadata[:type].in?(%i[system feature])
      # typeがsystemまたはfeatureのときに実行したい処理を書く
    end
  end
end
```

なお、次のように引数で複数のtypeを指定したりすることはできないようです。

```ruby
# NG!!（エラーは起きないが、こちらの期待した動きにはならない）
config.before(:each, type: %i[system feature]) do |example|
```


### ヘッドレスモードのChromeで実行する

ヘッドレスモード（画面を起動しないモード）でChromeを実行する場合は、`rails_helper.rb`に次の設定を追加します。

```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 追記: 上の設定をもっとシンプルに書く**
[こちらのブログ](https://medium.com/table-xi/a-quick-guide-to-rails-system-tests-in-rspec-b6e9e8a8b5f6)を参考にしたら、次のように簡単に書くことができました。

```rb: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の環境だと、オプションの渡し方が次のように変更されていました（[参考](https://github.com/rails/rails/commit/ada05850f84ee0eef5413950333e5b5332a64b48)）。

```rb: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が起動するようになります。

```rb: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`のタグを付けます。

```ruby
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](https://qiita-image-store.s3.amazonaws.com/0/7465/0803abab-59e1-e522-6332-48d825e87253.png "Screen Shot 2017-10-24 at 7.02.15.png")

### CIでSystem Specを実行する

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

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


```yml:.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
```

```yml: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用の設定例です。

```yaml:.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の設定例についてはこちらのブログも参考になると思います。

- [A Quick Guide to Rails System Tests in RSpec – Table XI – Medium](https://medium.com/table-xi/a-quick-guide-to-rails-system-tests-in-rspec-b6e9e8a8b5f6)

## まとめ

というわけで、この記事では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入門・その4「どんなブラウザ操作も自由自在！逆引きCapybara大辞典」 \- Qiita](https://qiita.com/jnchito/items/607f956263c38a5fec24)


「そもそもRSpecがよくわかってないんだけど・・・」という方は、こちらの入門記事をご覧ください。

- [使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 \- Qiita](https://qiita.com/jnchito/items/42193d066bd61c740612)