rspecを用いて行う。
rspec
テストコードを書く際の原則
- 各exampleで期待する値は1つ
- 期待する結果をはっきりわかりやすく記述
- 起きて欲しいことと起きてほしくないこと両方をテストする
- 境界値をテストする
- 可読性を考えつつ、適度にDRYにする
rspecの導入
gemfileに以下を追記し、bundle install
group :development, :test do
gem 'rspec-rails'
end
group :development do
gem 'web-console'
end
rspec設定ファイルを作成
rails g rspec:install
specフォルダに以下が作成される。
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
rails_helper.rb
共通の設定を書いておくファイル。
各テスト用ファイルに読み込ませ、共通の設定やメソッドを適用する。
spec_helper.rb
rails_helper.rbと同じくRSpec用の共通の設定を書いておくファイル。
こちらはRSpecをRails無しで利用する際に利用する。
フォーマット指定
.specファイルにフォーマットを指定できる。
デフォルトではProgressになっている。
- progress (略:p)
- documentation (d)
- html (h)
- json (j)
--format documentation
progressの場合、以下のような意味。
- . 成功
- F 失敗
- * 未実装
User
#create
is invalid without a nickname
#create
is invalid without a email (FAILED - 1)
↑documentation
↓progress
.F
ファイル管理
project/spec/models
project/spec/controllers
のようにそれぞれモデル・コントローラーで対応するフォルダを分ける。
specファイルは対応するクラス名_spec.rbという名前にする。
テストコード
describe "hogehoge" do
it "1 + 2は3になること" do
expect(1 + 2).to eq 3
end
end
describeでテストとしての塊を作る。””内にはテストの説明を書く。
クラスみたいな感じ。
以下のdo~endまでがテスト内容。
it “”で実際に動作させるコードの説明を書く。
メソッドみたいな感じ。エクスペクテーションと呼ぶ。
実行内容はexpect()内に書き、equalで期待する結果を書く。
ここはメソッド内の実行式みたいな感じ。
equalのほか、incledeやvalidなどがある。マッチャと呼ぶ。
テストの実行
テストを実行するときは以下のコマンドを使う
全てのテストファイルを動かす場合
bundle exec rspec
ファイルを指定する場合
bundle exec rspec spec/controllers/tweets_controller_spec.rb
モデルに対するテスト
CarrierWaveを使っているModelをRSpecでテスト
rspecでテストする際に画像を渡す場合は単に文字列を渡してはダメ。
任意の場所にファイルを置いて、file.openで画像を渡す。
image {File.open("#{Rails.root}/public/images/test_image.jpg")}
パスを連結する方法はjoinでも良い
Rails.root.join('spec/support/**/*.rb')
コントローラーに対するテスト
1つのアクションにつき、以下の2点を確かめる。
- インスタンス変数の値が期待したものになるか
- ビューに正しく遷移するか
describe Hogehoge_Controller do
describe 'HTTPメソッド名 #アクション名' do
it "インスタンス変数は期待した値になるか?" do
"擬似的にリクエストを行ったことにするコードを書く"
"エクスペクテーションを書く"
end
it "期待するビューに遷移するか?" do
"擬似的にリクエストを行ったことにするコードを書く"
"エクスペクテーションを書く"
end
end
コントローラーテスト用のgemをインストール
group :development, :test do
gem 'rails-controller-testing'
end
フォルダとファイル管理
spec/controllersフォルダを作成し、以下にcontrollerに対応したspecファイルを作成する。
例
hogehoge_controller.rb に対して
hogehoge_controller_spec.rbを作成。
疑似的なHTTPリクエスト
テストしたいコントローラのHTTPリクエストとアクションを指定する。
httpメソッドをシンボル型にして渡す。(get, post, delete, patch)
必要に応じてparamsの情報も付与する。
describe 'GET #show' do
it "renders the :show template" do
get :show, params: { id: 1 }
end
end
response
example内でリクエストが行われた後の遷移先のビューの情報を持つインスタンス。
it "renders the :edit template" do
get :edit, params: { id: tweet }
expect(response).to render_template :edit
end
assignsメソッド
アクションで定義しているインスタンス変数をテストするためのメソッド。
引数に、直前でリクエストしたアクション内で定義されているインスタンス変数をシンボル型でとる。
例
it "assigns the requested tweet to @tweet" do
tweet = create(:tweet)
get :edit, params: { id: tweet }
expect(assigns(:tweet)).to eq tweet
end
同名のワードが何度も出てくるのに、それぞれ意味が違うので注意。
tweet = create(:tweet)
get :edit, params: { id: tweet }
expect(assigns(:tweet)).to eq tweet
青字のtweetは、FactoryBotへ渡す引数。factories/tweets.rbを示す。
赤字のtweetは、example内で生成したインスタンス。
緑字のtweetは、赤字のtweetを元にget #editを動かした結果、生成されたインスタンス。
この赤と緑のtweetをassignsで比較している。
letメソッド
複数のexampleで同一のインスタンスを使いたい場合、letメソッドを使う。
遅延評価(lazy evaluation)のため、呼び出し時に一度評価された後は値がキャッシュされ、以降の呼び出し時に同じ値を吐く。かつキャッシュされているため処理が高速化する。
以下の例の場合、groupとuserは各expectationで使い回してOKなのでletでインスタンスを生成する。
describe MessagesController do
let(:group) { create(:group) }
let(:user) { create(:user) }
describe '#index' do
context 'log in' do
before do
login user
get :index, params: { group_id: group.id }
end
it 'assigns @message' do
expect(assigns(:message)).to be_a_new(Message)
end
it 'assigns @group' do
expect(assigns(:group)).to eq group
end
it 'redners index' do
expect(response).to render_template :index
end
end
end
end
before
beforeブロックの内部に記述された処理は、各exampleが実行される直前に、毎回実行される。
before do
login user
get :index, params: { group_id: group.id }
end
it do
end...
context
context do ~ endで囲むと結果がブロックとして表示される。
context 'can save' do
it "is valid without a image" do
end
it "is valid without a body" do
end
it "is valid with body & image" do
end
end
Message
#create
is invalid without image&body
is invalid without a group_id
is invalid without a user_id
can save
is valid without a image
is valid without a body
is valid with body & image
contextで囲まれていないものが自動的に先に表示される。
contextが複数ある場合は、上から順に並ぶ。
なおformat progressの場合には表示されないので意味なし。
matchers(マッチャ)
主にModelテスト用
eqマッチャ
同じかどうか。
includeマッチャ
含まれるかどうか。
モデルのテストで使う場合にはエラーメッセージを入れて使ったりする。
be_valid
validationがtrueかどうか。
it "is valid with nickname & email & password" do
user = build(:user)
user.valid?
expect(user).to be_valid
end
be_validマッチャ
expectの引数にしたインスタンスが全てのバリデーションをクリアする場合にパスする。
expect(user).to be_valid
エラーメッセージの種類
- is too short
- is too long
- can’t be blank
- doesn’t match Password
- has already been taken
shortやlongはエラーメッセージの配列の1つ目を取ってくる必要あり。
user.errors[:name][0]とせず、user.errors[:name]とした場合、
“is too short (minimum is 6 characters)” といった文字で返ってくる。
password_confirmationのエラーを誘導する場合、Pが大文字なのに注意。
主にControllerテスト用
render_templateマッチャ
引数にシンボル型でアクション名を取る。
引数で指定したアクションがリクエストされた時に自動的に遷移するビューを返す。
matchマッチャ
引数に配列クラスのインスタンスをとり、expectの引数と比較するマッチャ。
配列の中身の順番までチェックしてくれる。
be_a_newマッチャ
対象が引数で指定したクラスのインスタンスかつ未保存のレコードであるかどうか確かめる。
it 'assigns @message' do
expect(assigns(:message)).to be_a_new(Message)
end
redirect_toマッチャ
引数にとったプレフィックスにリダイレクトした際の情報を返す。
context 'not log in' do
before do
get :index, params: { group_id: group.id }
end
it 'redirects to new_user_session_path' do
expect(response).to redirect_to(new_user_session_path)
end
end
changeマッチャ
引数が変化したかどうかを確かめる。
change(Message, :count).by(1)と記述し、Messageモデルのレコードの総数が1個増えたかどうかを確かめる
.to .not_to
以下と同じことを示す場合は.toを。
違うことを示す場合には.not_toを。
it 'does not count up' do
expect{ subject }.not_to change(Message, :count)
end
テスト例 一覧まとめ
基本フォーマット
下の例は継ぎはぎしているのでそのままでは意味が通りません。
requireが必要だったり、letやcontextなどはこの位置で使うんだなぁ程度で捉えてください。
require 'rails_helper'
describe MessagesController do
let(:group) { create(:group) }
describe '#index' do
context 'log in' do
it 'redirects to new_user_session_path' do
post :create, params: params
expect(response).to redirect_to(new_user_session_path)
end
end
end
end
派生フォーマット
HTTPリクエストをbefore doでやる場合
describe '#index' do
before do
get :index, params: { group_id: group.id }
end
it 'redirects to new_user_session_path' do
expect(response).to redirect_to(new_user_session_path)
end
end
HTTPリクエストをsubjectでまとめる場合
describe '#index' do
subject {
post :create,
params: invalid_params
}
it 'renders index' do
subject
expect(response).to render_template :index
end
it 'does not count up' do
expect{ subject }.not_to change(Message, :count)
end
end
expect例
インスタンスを評価する場合
expect(assigns(:group)).to eq group
expect(assigns(:message)).to be_a_new(Message)
viewを評価する場合
expect(response).to redirect_to(new_user_session_path)
expect(response).to render_template :index
データベースを評価する場合
expect{ subject }.to change(Message, :count).by(1)
expect{ subject }.not_to change(Message, :count)
factory_bot
インスタンスの生成を楽にしてくれる。
rspecと併用するので以下のようにgemファイルへ記入してbundle install
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
end
specディレクトリ直下にfactoriesディレクトリを作成。
テストしたいインスタンスの複数形のファイル名でRubyのファイルを作成。
例
spec/factories/user.rb
FactoryBot.define do
factory :user do
nickname {"hogehoge"}
email {"aaa@aaa.com"}
password {"12345678"}
password_confirmation {"12345678"}
end
end
こうすることで以下のように書ける。
describe User do
describe '#create' do
it "is invalid without a nickname" do
user = build(:user, nickname: nil)
user.valid?
expect(user.errors[:nickname]).to include("can't be blank")
end
end
end
buildメソッド
引数にクラス名をシンボルで指定するとfactoriesファイルを参照し、自動的にインスタンスが生成される。インスタンス以外を続けて指定すると上書きできる。
user = FactoryBot.build(:user)
user = build(:user, nickname: nil)
buildなのは昔の名残???newと同義と思って良さそう。
createメソッド
createの場合はテスト用のDBに値が保存する。
ただし、テスト実行ごとにDB内容はロールバックされるため、保存した値は消えるので確認できない。DBの中身を確認したい場合はbinding.pry等で処理を止めること。
user = FactoryBot.create(:user)
create_list
リソースを複数作成したい場合に利用する。
第1引数にリソースを、第2引数に作成したい数を指定する。
hoges = create_list(:hoge, 3)
attributes_for
FactoryBotによって定義されるメソッドで、オブジェクトを生成せずにハッシュを生成する。
paramsの中身を作る際に使う。
let(:invalid_params) { { group_id: group.id, user_id: user.id, message: attributes_for(:message, body: nil, image: nil) } }
FactoryBotの省略記法
helperのconfigureに下記を記述することで冒頭のようにbuild(:user)でインスタンスを作成できるようになる。
spec/rails_helper.rb
RSpec.configure do |config|
#下記の記述を追加
config.include FactoryBot::Syntax::Methods
end
Faker
emailや電話番号、名前などのダミーデータを作成するためのGem。
インストール後、factory_botの設定ファイルの中でFakerのメソッドを利用し、ダミーデータを生成する。
FactoryBot.define do
factory :user do
nickname {"abe"}
password {"00000000"}
password_confirmation {"00000000"}
sequence(:email) {Faker::Internet.email}
end
end
fakerのインストール
テスト環境にのみ必要なのでgemfileの以下に記述しbundle install
group :test do
gem 'faker'
end
fakerパターン例
email 重複回避
sequence(:email) {Faker::Internet.email}
email {Faker::Internet.free_email}
deviseをテストで利用する
ユーザーのログイン状態で検証項目が変わるため、準備が必要。
rspec下にsupportフォルダを作成し、module用にファイルを作成。
/spec/support/controller_macros.rb
module ControllerMacros
def login(user)
@request.env["devise.mapping"] = Devise.mappings[:user]
sign_in user
end
end
helperに、deviseのコントローラのテスト用のモジュールと先ほど定義したControllerMacrosを読み込む記述をする。
/spec/rails_helper.rb
RSpec.configure do |config|
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
config.include Devise::Test::ControllerHelpers, type: :controller
config.include ControllerMacros, type: :controller
#〜省略〜
end
テストのやり方 まとめ
4つのgemをインストール
web-consoleを入れていなければweb-consleも。
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'rails-controller-testing'
gem 'faker'
end
group :development do
gem 'web-console'
end
記入したらbundle install。
厳密にはfakerはtestのみで十分です。
設定ファイルを作成
rails g rspec:install
specフォルダに以下が作成される。
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
.rspecファイルに必要に応じて以下を追記
--format documentation
フォルダ作成
project/spec/models
project/spec/controllers
疑問
テストの疑問
前提としてfactorybotに以下の記述があるとして、
factories/message.rb
FactoryBot.define do
factory :message do
body {Faker::Lorem.sentence}
image {File.open("#{Rails.root}/public/images/cat1.png")}
user
group
end
end
it "is valid without a body" do
message = build(:message, body: nil)
binding.pry
message.valid?
expect(message).to be_valid
end
build直後で止めてmessageの中身を確認すると、すべてnil。
にも関わらずmessage.valid?はtrue。
pry> message
=> #<Message:0x00007f89391d5d48 id: nil, body: nil, image: nil, created_at: nil, updated_at: nil, user_id: nil, group_id: nil>
pry> message.valid?
=> true
bodyとimageはnull許可だけど、
validates :body, presence: true, unless: :image?
でモデルに片方は必要だとvalidationを掛けている。
どっちもnilでvalid?がtrueなのは何故だろう?
ではbuildをcreateにすると、どうなるか?
it "is valid without a body" do
message = create(:message, body: nil)
binding.pry
message.valid?
expect(message).to be_valid
end
こちらは値が入ってvalid?もOK.
pry> message
=> #<Message:0x00007feb4da4e198
id: 3,
body: nil,
image: "cat1.png",
created_at: Sat, 10 Aug 2019 10:37:08 UTC +00:00,
updated_at: Sat, 10 Aug 2019 10:37:08 UTC +00:00,
user_id: 3,
group_id: 3>
pry> message.valid?
=> true
不思議なのはbuildした段階ではimageに値が入っていないけど、saveすると値が入ってくること。しかもvalidationのチェックは値が入ったものとして行われている。何故だろう?
ちなみにbodyはbuildした段階で既に値が入っている。これが普通の挙動だと思うけどなぁ。
外部キーのエラーメッセージの格納場所はどこ?
user_idを外部キーに持つmessageテーブルを保存した時、エラーメッセージの格納場所がmessage.errors[:user_id]になるのかと思いきやmessage.errors[:user]に格納される。
has_many through で持っている影響だと思うけど、わかりにくい。
it "is invalid without a user_id" do
message = build(:message, user_id: nil)
message.valid?
expect(message.errors[:user]).to include("を入力してください")
end
コメント