RspecとはRubyプログラマー向けのBDDツールです。
ここでのBDDはテスト駆動開発、ドメイン駆動型設計、受入テスト型設計へのアプローチのこと。
#Rspec導入
group :development, :test do
gem 'rspec-rails', '~> 3.6'
end
RspecのGemパッケージをインストールします。
developmentを追加している理由としてはRspecにはテストファイルを作成するgenaratorがあり、それ利用する時に便利だからです。
そしたら$ bundle install
します。
##Springを使用したRSpecの導入
Springを使用してRSpecを実行することで一回の実行時間を短縮することができる。
:
:
group :development, :test do
gem 'spring-commands-rspec'
end
:
:
$ bundle exec spring binstub rspec
を実行することでbin/rspec
ファイルが作成されるのでbin/rspec
を使用してRSpecを実行するとSpringを使用することができます。
#Rspecの実行
インストールが完了した段階で$ bundle exec rspec
でRSpecが実行できます。
#テスト作成
テストファイルはRubyで記載します。
RSpecはデフォルトでspec
ディレクトリ配下のファイルをテストファイルと認識します。
RSpecのgenerator
を使用してファイルを作成して編集します。
$ 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を使用して記述します。
テスト内容は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
に続いて動詞から始まる説明文で記述していた理由としては、記載した内容が表示されるため何のためにテストを実施したのかが実行結果を見ればわかるためです。
Diff
is valid with diff string
Finished in 0.02296 seconds (files took 0.35495 seconds to load)
1 example, 0 failures
またテストが特定の条件を想定する場合はcontext
を使用してその条件を記載します。
ここでdescribe
,context
は入れ替えても実行できます。
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
##テストする対象
対象はモデル、ビュー、コントローラがあげられる。
・Model specs
→モデルのvalidation等をテストする。
・controller specs
→テストは RSpec.describe ${ControllerClass} do ~ end を使って記述する
※オプションとして type: :feature を指定する
Rails5 からはRequest specsを使うことが推奨されている)
・Request specs
→・統合テストの細かいラッパーを記述します。
→・基本的にスタブは使用しない。
・ルーティングとコントローラの双方の動作を記述する。
・テスト内容の例
→・1回のrequestを指定する。
・複数のrequestを複数のcontrollersにまたがって指定する。
・複数のrequestを複数の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 が統一されていると都合がよいため)
#テストファイルに記述する内容
テストする対象が決まったらテストの内容を記述していきます。
テストの内容は確認したいことを1つ記載する。
また、describeとitの内容を繋げて読むと文章になるようにit文は動詞から始める。
describe "User" do # User モデルについて記述(describe)する
it "is valid with a name and email" # name と email を保持していることが正である
it "is invalid without a name" # name が無いと無効である
end
##controllerテストについて
get
, post
, patch
, delete
メソッドを実行するとテスト対象となるコントローラに対してオプションで指定したアクションが実行される。
これに応じてresponse,statusが200であることをテストできる。
テスト対象と異なるコントローラのアクションを呼び出したいときはredirect_to
を使用すれば良い。
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
を使用する。
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テストについて
基本的にはcontrollerと同様ですが、requestテストではcontrollerのactionを呼び出すのに対してrequestテストではパスを指定する点が異なります。
get
,post
,patch
,delete
メソッドを実行することでテスト対象となるルーティングが行われ、対応するコードのアクションが実行されます。
これに応じてresponse.statusが200であること、response.bodyに特定の文字列が含まれること等がテストされます。
尚、テスト対象と異なるコントローラのアクションを呼び出したいときはredirect_to
を使用します。
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テストについて
capycaraを使用したテストを記述する場合の、ブラウザ操作項目を記述する。
・visit
・指定したURLパスへHTTP GETによるアクセスを行う
・HTTP応答されたHTML内容は次に記述するpageに保持される。
・page
・ブラウザが保持するDOMツリーを保持するオブジェクト
・ページ内に特定のメッセージが出力されることを確認するためにはexpect
とhave_text
を組み合わせる。
##フォーム操作
・fill_in
→・inputボックスにテキストを入力する
・click_button
→・buttonをクリックする(submmitボタン等)
・attach_file
→・ファイルをアップロードする
###ファイルをアップロードする
attach_file
を使用するとファイルアップロードが行える。
尚、テストでアップーロードするためにファイルを用意する場合はfixture
ファイルとして用意する。
デフォルトの保存先はspec/fixtures/files/
配下になる。
※保存先を変更したい場合はRSpec.config.file_fixture_path
の値を変更する。
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
の変化を調べると良いでしょう。
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ではありません。
ヘルパーにログイン・ログアウト・特定のユーザーによるログイン操作を行うためのメソッドを用意して読み込ませるのが良いでしょう。