前提として、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 の公式サイトを元に適宜修正してください)
・・・
group :development, :test do
gem 'rspec-rails', '~> 3.6'
end
・・・
ちなみに、RSpec は test フレームワークなのに、なぜインストールグループに development を追加するかというと、RSpec にはテストファイルを作成する generator があり、それを利用するために default の RAILS_ENV である development にインストールしておくと楽だからです。
$ bundler install
Gem パッケージがインストールされたら、次は Rails ソフトウェアに対して 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 ファイルを作成します。
・・・
group :development, :test do
gem 'spring-commands-rspec'
end
・・・
$ bundle exec spring binstub rspec
すると bin/rspec ファイルが作成されるので、 ./bin/rspec
を使って RSpec を実行すると Spring を使うことが出来ます。(bundle install 実行時に stub ファイルのインストール先を変更している場合はパスが異なる場合があります)
ファイルの中身を見ると spring 経由で 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
コマンドで実行できる。
$ bundle exec rspec
$ 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 を使ってファイルを作ってからファイルを編集するのがよいでしょう。
$ 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>
$ bin/rails g rspec:model test
create spec/models/test_spec.rb
: <snip>
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"を設定した状態での結果)
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を使うことが推奨されている)
- テストは RSpec.describe
- 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 される。
- テストは RSpec.feature
- View specs
- ビューに特定の文字列が含まれること等をテストします
- Helper specs
- ヘルパーメソッドを実行して、意図した結果が返ってくることをテストします
- Mailer specs
- Routing specs
- パスが意図したコントローラへルーティング出来ることをテストします
- Job specs
- System specs
その他、サービス層としてクラスを定義している場合等、spec ディレクトリ配下に spec/services のようにディレクトリを作成してテストを作成することが出来ます。
その場合、type: :service
のように開発チーム内で一定のルールを設けて指定するようにしましょう。
(特定の spec に対して設定やテスト前の初期化を行う場合に type が統一されていると都合がよいため)
テストファイルに記述する内容
テストする対象が決まったら、テストする項目を記述していく。
このときのポイントとして、テストファイル内には 「<テストする対象>は<~>であることが正しい」といった言葉に置き換えて項目を列挙する。
例えば「User は name, emailAddress を所有していることが正である」場合、
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を記録する動作をテストする場合
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
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を記録する動作をテストする場合
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 に含まれる文字列をテストすることが出来ます。
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
で読み込める。
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)
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 事始め を参照のこと。
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 不足によるエラー
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 する
- 方法
- Gemfile に capybara を記述(バージョンは適宜変更)
-
bundle install
コマンドを実行
group :test do
・・・
gem 'capybara', '~> 2.8'
end
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)>'
- 原因
- 対策
- capybara バージョンを 2.8 以上にする