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

Ruby on Rails のテストフレームワーク RSpec 事始め

前提として、Ruby on Rails で Hello World が表示できていること。
Ruby on Rails を始める方法は Ruby on Rails 事始め に記載。

RSpec とは

RSpec とは Ruby プログラマー向けの BDD(Behaviour-Driven Development) ツールです。
ここでの BDD はテスト駆動開発(Test-Driven Development), ドメイン駆動型設計(Domain Driven Design), 受け入れテスト型設計へのアプローチのことです。

RSpec は Gem パッケージとして提供されています。

RSpec の公式サイトはこちら

RSpec の使い方

RSpec の導入

まずは RSpec の Gem パッケージをインストールします。
(インストールするバージョンは RSpec の公式サイトを元に適宜修正してください)

Gemfile
・・・
group :development, :test do
  gem 'rspec-rails', '~> 3.6'
end
・・・

ちなみに、RSpec は test フレームワークなのに、なぜインストールグループに development を追加するかというと、RSpec にはテストファイルを作成する generator があり、それを利用するために default の RAILS_ENV である development にインストールしておくと楽だからです。

RSpecのGemパッケージインストール
$ bundler install

Gem パッケージがインストールされたら、次は Rails ソフトウェアに対して RSpec 用の初期ファイルをインストールする。

RSpecのインストール
$ rails generate rspec:install
Running via Spring preloader in process 9045
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

Spring を使った RSpec の導入(任意)

Spring を使って RSpec を実行することで 1 回の実行時間を短縮させることが出来ます。

そのためには spring-commands-rspec Gem をインストールし、RSpec の stub ファイルを作成します。

Gemfile
・・・
group :development, :test do
  gem 'spring-commands-rspec'
end
・・・
RSpecのstubファイル作成
$ bundle exec spring binstub rspec

すると bin/rspec ファイルが作成されるので、 ./bin/rspec を使って RSpec を実行すると Spring を使うことが出来ます。(bundle install 実行時に stub ファイルのインストール先を変更している場合はパスが異なる場合があります)

ファイルの中身を見ると spring 経由で rspec を使うためのラッパであることが分かります。

bin/rspec
#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
require 'bundler/setup'
load Gem.bin_path('rspec-core', 'rspec')

RSpec の実行

インストールが完了した段階でテストは空だが RSpec が実行できるようになる。
bundler インストールしている場合は、bundle exec コマンドで実行できる。

RSpecの実行方法(全テストの実行:オプション無のデフォルト動作)
$ bundle exec rspec
RSpecのmaintain_test_schemaのマイグレーション
$ rails db:migrate RAILS_ENV=test

上記のようにオプションを指定せずに RSpec を実行すると全テストを実行する。
テストの数が少ない場合はテストを実行するためにかかる時間の問題はないが、
特定のテストのみ実行すればよい場合には -e STRING オプションを指定すれば任意のテストを実行できる。

-e オプションの STRING にはテスト中の describe で記述した名前を指定する。
describe の書き方は後述する。

テストの作り方

テストファイルの命名規則

テストファイルは Ruby で記載する。
RSpec は default で spec ディレクトリ配下のファイルを自動でテストファイルとして認識する。(ファイル名規則: spec/**/*_spec.rb
尚、テストファイルは必ず require 'rails_helper' を含める必要がある。(RSpec3以降)

RSpec の generator を使ってファイルを作ってからファイルを編集するのがよいでしょう。

RSpecのgenerator
$ bin/rails g
  : <snip>

Rspec:
  rspec:controller
  rspec:feature
  rspec:helper
  rspec:install
  rspec:integration
  rspec:job
  rspec:mailer
  rspec:model
  rspec:observer
  rspec:request
  rspec:scaffold
  rspec:view

  : <snip>
例modelテスト用のファイルspec/models/test_spec.rbを作成する場合
$ bin/rails g rspec:model test
      create  spec/models/test_spec.rb
  : <snip>
spec/models/test_spec.rb
require 'rails_helper'

RSpec.describe Test, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end

テストファイルの記述ルール

テスト記述ルールとしては、テストする内容を説明する文章を引数として、describe, context, it を使って記述する。
1 つのテスト内容は it で記述し、期待する動作(expect)を 1 つ含める。

require 'rails_helper'

RSpec.describe Diff, type: :model do
  it "is valid with diff string" do
    diff = Diff.new(diff: "some diff")
    expect(diff).to be_valid
  end
end

it に続く文字列は動詞から始め、should は使わない方がよいようです。(RSpec v2.11からshouldの代わりにexpectを使うこととなりshouldという表現が使わなくなったようです。参考情報)

ここまでで 1 つのテスト項目の記述ですが、実行するとなぜ it に続いて動詞から始まる説明文を記述していたのか理由が分かります。(spec/spec_helper.rbでconfig.default_formatter = "doc"を設定した状態での結果)

rspec実行結果
Diff
  is valid with diff string

Finished in 0.02296 seconds (files took 0.35495 seconds to load)
1 example, 0 failures

このように、describe, it で記述した内容がテスト結果に表示されるため、実行結果を見れば何をテストしているのかが分かるようになっています。

また、テストが特定の条件を想定する場合は context を使ってその条件を記述します。
ここで describe, context は入れ替えても動作しますし、以下の例では describe, context は記述せず it だけ記述しても動作しますが、テストが想定する内容が分かりやすくなり、将来的に after/before を使ってテスト用データを用意する場合等に、コードのスコープが明確になるメリットもあるため積極的に使うとよいでしょう。

require 'rails_helper'

RSpec.describe Micropost, type: :model do
  describe "search posts by term" do
    context "when no post is found" do
      it "returns an empty collection" do
        expect(Micropost.search("John Doe")).to be_empty
      end
    end
  end
end

Spec ファイルについて詳細な情報はこちらです。

テストする対象

テストする対象は、モデル、コントローラ、ビュー、ルーティングがあげられる。
RSpec ドキュメントには次のテスト対象が記述されている。

  • Model specs
    • モデルの validation 等をテストします
  • Controller specs
    • テストは RSpec.describe ${ControllerClass} do ~ end を使って記述する
      • オプションとして type: :feature を指定する
    • Rails5 からはRequest specsを使うことが推奨されている)
  • Request specs
    • 統合テストの細かいラッパーを記述します
      • 基本的にスタブは使わない(作り方次第)
      • ルーティングとコントローラの双方の動作を記述する
    • テスト内容の例
      • 1回の request を指定する
      • 複数の requests を複数の controllers にまたがって指定する
      • 複数の requests を複数の sessions にまたがって指定する
  • Feature specs
    • テストは RSpec.feature ${Some feature test name} do ~ end を使って記述する
      • オプションとして type: :feature を指定する
    • Feature テストとは高レベルのアプリケーションの挙動について行うテストのこと。
    • Feature テストはブラウザ操作(ボタンクリックや input ボックスへの入力)を行う。
      • ブラウザ操作を行うために capybara (Gem パッケージ)をインストールする必要がある。
      • capybara がインストールされていないと rspec 実行時にテストが pending される。
  • View specs
    • ビューに特定の文字列が含まれること等をテストします
  • Helper specs
    • ヘルパーメソッドを実行して、意図した結果が返ってくることをテストします
  • Mailer specs
  • Routing specs
    • パスが意図したコントローラへルーティング出来ることをテストします
  • Job specs
  • System specs

その他、サービス層としてクラスを定義している場合等、spec ディレクトリ配下に spec/services のようにディレクトリを作成してテストを作成することが出来ます。

その場合、type: :service のように開発チーム内で一定のルールを設けて指定するようにしましょう。
(特定の spec に対して設定やテスト前の初期化を行う場合に type が統一されていると都合がよいため)

テストファイルに記述する内容

テストする対象が決まったら、テストする項目を記述していく。
このときのポイントとして、テストファイル内には 「<テストする対象>は<~>であることが正しい」といった言葉に置き換えて項目を列挙する。

例えば「User は name, emailAddress を所有していることが正である」場合、

spec/models/user_spec.rb
describe "User" do                    # User モデルについて記述(describe)する
  it "is valid with a name and email" # name と email を保持していることが正である
  it "is invalid without a name"      # name が無いと無効である
end

などと、テスト項目(it 以降に記述した内容)を記述していく。
(なお、未だテスト項目の名前のみでテスト自体は記述していない)

テスト項目には確認したいただ1つの内容を記述する。
また、describe と it の内容を繋げて読むと文章になるよう、it 文では動詞から始める。
(上記例では "User is valid with a name and email." が文章になる)

参考情報:https://leanpub.com/everydayrailsrspec-jp/read

controller テストについて

get, post, patch, delete メソッドを実行するとテスト対象となるコントローラに対してオプションで指定したアクションが実行される。
これに応じて response.status が 200 であることなどをテスト出来る。

尚、テスト対象と異なるコントローラのアクションを呼び出したい場合は redirect_to を使えばよい。

例: セッションを保存するSessionsControllerがあり、ログイン前にアクセスしたURLを記録する動作をテストする場合

spec/controllers/sessions_controller_spec.rb
require 'rails_helper'

RSpec.describe SessionsController, type: :controller do
  include LoginHelper

  let(:user) { FactoryBot.create(:user, name: 'michael') }

  it "get new" do
    get :new
    expect(response.status).to eq(200)
  end

  it "store forwarding_url only at first" do
    redirect_to edit_user_path(user)
    expect(session[:forwarding_url]).not_to eq(edit_user_url(user))
    log_in_as(user)
    expect(session[:forwarding_url]).to be_nil
  end
end

また、デフォルトでは View をレンダリングしない。
そのため、レンダリングされた結果のタイトルが意図したものかテストしたい等の場合は render_views を使う。参考URL

spec/controllers/static_pages_controller_spec.rb
require 'rails_helper'

RSpec.describe StaticPagesController, type: :controller do
  render_views

  let(:base_title) { 'Ruby on Rails Tutorial Sample App' }

  it "get root" do
    get :home
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>#{base_title}<\/title>/i)
  end

  it "get home" do
    get :home
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>#{base_title}<\/title>/i)
  end

  it "get help" do
    get :help
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>Help | #{base_title}<\/title>/i)
  end

  it "get about" do
    get :about
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>About | #{base_title}<\/title>/i)
  end

  it "get contact" do
    get :contact
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>Contact | #{base_title}<\/title>/i)
  end
end

request テストについて

Rails5 からはRequest specsを使うことが推奨されています。

基本的なテスト内容は controller テストと同様ですが、controller テストでは controller の action を呼び出すのに対して、request テストではパスを指定する点が異なります。

get, post, patch, delete メソッドを実行するとテスト対象となるルーティングが行われ、対応するコントローラのアクションが実行されます。
これに応じて response.status が 200 であること、response.body に特定の文字列が含まれることなどがテスト出来ます。

尚、テスト対象と異なるコントローラのアクションを呼び出したい場合は redirect_to を使えばよい。

例: セッションを保存するSessionsControllerがあり、ログイン前にアクセスしたURLを記録する動作をテストする場合

spec/requests/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :request do
  include RequestLoginHelper

  describe "GET /login" do
    it "render new" do
      get '/login'
      expect(response).to have_http_status(200)
    end
  end

  describe 'forwarding url' do
    let(:user) { FactoryBot.create(:user, name: 'michael') }

    it "should store forwarding_url only at first" do
      get '/login'
      redirect_to edit_user_path(user)
      expect(session[:forwarding_url]).not_to eq(edit_user_url(user))

      log_in_as(user)
      expect(session[:forwarding_url]).to be_nil
    end
  end
end

controller テストと違って render_views を書かなくても response.body により view に含まれる文字列をテストすることが出来ます。

spec/requests/static_pages_spec.rb
require 'rails_helper'

RSpec.describe "StaticPages", type: :request do
  let(:base_title) { 'Ruby on Rails Tutorial Sample App' }

  describe "GET /" do
    it "should get root" do
      get '/'
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>#{base_title}<\/title>/i)
    end
  end

  describe "GET /home" do
    it "should get home" do
      get '/home'
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>#{base_title}<\/title>/i)
    end
  end

  describe "GET /help" do
    it "should get help" do
      get '/help'
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>Help | #{base_title}<\/title>/i)
    end
  end

  describe "GET /about" do
    it "should get about" do
      get "/about"
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>About | #{base_title}<\/title>/i)
    end
  end

  describe "GET /contact" do
    it "should get contact" do
      get "/contact"
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>Contact | #{base_title}<\/title>/i)
    end
  end
end

feature テストについて

capybara を使ったテストを記述する場合の、ブラウザ操作項目を記述する。

尚、capybara の公式ドキュメントは Module: Capybara を参照。

  • visit
    • 指定した URL パスへ HTTP GET によるアクセスを行う。
    • HTTP 応答された HTML 内容は次に記述する page に保持される。
  • page
    • ブラウザが保持する DOM ツリーを保持するオブジェクト
    • ページ内に特定のメッセージが出力されることを確認するためには expect と have_text を組み合わせる
      • 例:expect(page).to have_text("User was successfully created.")

フォーム操作

  • fill_in
    • input ボックスにテキストを入力する
  • click_button
    • button をクリックする(submmitボタン等)
  • attach_file
    • ファイルをアップロードする

ファイルをアップロードする

ファイルをアップロードする時には attach_file が使える。(参考情報: https://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions)

尚、テストでアップロードするためにファイルを用意する場合は fixture ファイルとして用意する。
デフォルトで fixture ファイルの保存先は spec/fixtures/files/ 配下である。
※ 保存先を変更したい場合は RSpec.config.file_fixture_path の値を変更する。(参考URL: https://relishapp.com/rspec/rspec-rails/v/3-8/docs/file-fixture)

spec/fixtures/files/rails.png を保存した場合、 file_fixture("rails.png") でアクセスできる。
テキストファイルは file_fixture("some.txt").read で読み込める。

spec/features/microposts_interface.rb
  it "micropost interface" do
    act_as(user) do
      visit root_path
      expect(page).to have_xpath("//div[@class='pagination']")

      # 有効な送信
      content = 'This micropost really ties the room toghether'
      picture = file_fixture("rails.png")
      expect(-> {
        within '#micropost' do
          fill_in 'micropost_content', with: content
          attach_file 'micropost_picture', picture
        end
        click_button 'Post'
      }).to change(Micropost, :count).by(1)
    end
  end

マッチャ

  • have_content
    • page に含まれるコンテンツを比較する
  • have_xpath
    • page に含まれるコンテンツを xpath で選択する
    • デフォルトでは非表示 DOM は検索されない
      • 非表示 DOM を検索する場合は visible: false をつける

リダイレクトをテストする

visit を使って遷移した場合、リダイレクト先へ自動で遷移します。
そのため、リダイレクトをテストする場合は change マッチャを使って current_path の変化を調べるとよいでしょう。(参考情報: https://ja.stackoverflow.com/questions/21673/capybara%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6-redirect%E5%85%88%E3%81%AE%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E6%A4%9C%E8%A8%BC%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)

spec/features/microposts_interface.rb
RSpec.feature "MicropostsInterface", type: :feature do

  it "micropost interface" do
      # 有効な送信
      content = 'This micropost really ties the room toghether'
      picture = file_fixture("rails.png")
      expect(-> {
        within '#micropost' do
          fill_in 'micropost_content', with: content
          attach_file 'micropost_picture', picture
        end
        click_button 'Post'
      }).to change(Micropost, :count).by(1) && change { current_path }.to(root_path)
      expect(user.microposts.first.picture?).to be_truthy
      expect(page).to have_content(content)
  end
end

ヘルパーを読み込む

特定のユーザによる操作をテストする場合、ログイン・ログアウトを毎回記述するのは DRY ではありません。

ヘルパーにログイン・ログアウト・特定のユーザによるログイン操作を行うためのメソッドを用意して読み込ませるのがよいでしょう。(参考情報: https://qiita.com/jnchito/items/a8360e5e7a829d1e19b2)

サンプルテスト

前提として、scaffold で User モデル(name, email属性を持つ)を作成しています。
scaffold を使ったモデルの作り方は別記事 Ruby on Rails での best_in_place 事始め を参照のこと。

spec/features/user_spec.rb
require "rails_helper"

RSpec.feature "user management", :type => :feature do
  scenario "End User creates a new user" do
    visit "/users/new"

    fill_in "user_name", :with => "test_name"
    fill_in "user_email", :with => "test@example.com"
    click_button "Create User"

    expect(page).to have_text("User was successfully created.")
  end
end

トラブルシュート

RSpec を使う上でエラーが発生した場合等のトラブルシュートを見つけ次第記述していく。(見つけ次第、適宜追記予定)

rspecコマンド実行時のfeatureテストにおけるエラー

capybara 不足によるエラー

capybara不足によるエラー
Pending: (Failures listed here are expected and do not affect your suite's status)

  1) user management End User creates a new user
     # Feature specs require the Capybara (http://github.com/jnicklas/capybara) gem, version 2.2.0 or later. We recommend version 2.4.0 or later to avoid some deprecation warnings and have support for `config.expose_dsl_globally = false`.
     # ./spec/features/user_spec.rb:4


Finished in 0.01628 seconds (files took 4.12 seconds to load)
1 example, 0 failures, 1 pending
  • 原因
    • capybaraがインストールされていない
  • 対策
    • Gemfile の test グループに capybara を記述して bundle install する
  • 方法
    1. Gemfile に capybara を記述(バージョンは適宜変更)
    2. bundle install コマンドを実行
Gemfile
group :test do
    ・・・
  gem 'capybara', '~> 2.8'
end

click_buttonのメソッド不足によるエラー

click_buttonのメソッド不足によるエラー
  1) user management End User creates a new user
     Failure/Error: click_button "Create User"

     NoMethodError:
       undefined method `normalize_params' for Rack::Utils:Module
     # ./spec/features/user_spec.rb:9:in `block (2 levels) in <top (required)>'
Why do not you register as a user and use Qiita more conveniently?
  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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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