前回(Devise Token Authの挙動を確認してみた)で実際にAPIを叩いて動作確認しつつ、設定変更と不味そうな所のカスタマイズを行いましたが、今回はRSpecを書いて、テスト駆動で挙動の確認と修正を行なって行きます。
RSpec書く事で、継続的に品質(求める挙動[仕様]も含む)を担保できるのが良い所ですね。
改修だけでなく、GemやRubyのバージョンアップも容易になります。
(本日はAWS Innovateを聞きながら記事を書いています。いくつか使えそうな話もありました)
今回は下記の順番で書いて行きます。
routing spec
先ずは通るテストを書いて、変更したい箇所を直して失敗させてから、実装を直して行きます。
コメント行が元々通ったテストで、コメントの下が新しい期待値です。
パスも変更しています。同じパスでメソッド間違えても実害がないようにする為。
spec/routing/users/auth/registrations_routing_spec.rb を作成
require 'rails_helper'
RSpec.describe Users::Auth::RegistrationsController, type: :routing do
describe 'routing' do
it 'routes to #new' do
# expect(get: '/users/auth/sign_up').to route_to('users/auth/registrations#new')
expect(get: '/users/auth/sign_up').not_to be_routable
end
it 'routes to #create' do
# expect(post: '/users/auth').to route_to('users/auth/registrations#create')
expect(post: '/users/auth').not_to be_routable
expect(post: '/users/auth/sign_up').to route_to('users/auth/registrations#create')
end
it 'routes to #edit' do
# expect(get: '/users/auth/edit').to route_to('users/auth/registrations#edit')
expect(get: '/users/auth/edit').not_to be_routable
end
it 'routes to #update' do
# expect(put: '/users/auth').to route_to('users/auth/registrations#update')
# expect(patch: '/users/auth').to route_to('users/auth/registrations#update')
expect(put: '/users/auth').not_to be_routable
expect(patch: '/users/auth').not_to be_routable
expect(put: '/users/auth/update').to route_to('users/auth/registrations#update')
expect(patch: '/users/auth/update').to route_to('users/auth/registrations#update')
end
it 'routes to #destroy' do
# expect(delete: '/users/auth').to route_to('users/auth/registrations#destroy')
expect(delete: '/users/auth').not_to be_routable
expect(delete: '/users/auth/destroy').to route_to('users/auth/registrations#destroy')
end
it 'routes to #cancel' do
# expect(get: '/users/auth/cancel').to route_to('users/auth/registrations#cancel')
expect(get: '/users/auth/cancel').not_to be_routable
end
end
end
spec/routing/users/auth/confirmations_routing_spec.rb を作成
require 'rails_helper'
RSpec.describe Users::Auth::ConfirmationsController, type: :routing do
describe 'routing' do
it 'routes to #new' do
# expect(get: '/users/auth/confirmation/new').to route_to('users/auth/confirmations#new')
expect(get: '/users/auth/confirmation/new').not_to be_routable
end
it 'routes to #create' do
expect(post: '/users/auth/confirmation').to route_to('users/auth/confirmations#create')
end
it 'routes to #show' do
expect(get: '/users/auth/confirmation').to route_to('users/auth/confirmations#show')
end
end
end
spec/routing/users/auth/sessions_routing_spec.rb を作成
require 'rails_helper'
RSpec.describe Users::Auth::SessionsController, type: :routing do
describe 'routing' do
it 'routes to #new' do
# expect(get: '/users/auth/sign_in').to route_to('users/auth/sessions#new')
expect(get: '/users/auth/sign_in').not_to be_routable
end
it 'routes to #create' do
expect(post: '/users/auth/sign_in').to route_to('users/auth/sessions#create')
end
it 'routes to #destroy' do
expect(delete: '/users/auth/sign_out').to route_to('users/auth/sessions#destroy')
expect(get: '/users/auth/sign_out').not_to be_routable
end
end
end
spec/routing/users/auth/unlocks_routing_spec.rb を作成
require 'rails_helper'
RSpec.describe Users::Auth::UnlocksController, type: :routing do
describe 'routing' do
it 'routes to #new' do
# expect(get: '/users/auth/unlock/new').to route_to('users/auth/unlocks#new')
expect(get: '/users/auth/unlock/new').not_to be_routable
end
it 'routes to #create' do
expect(post: '/users/auth/unlock').to route_to('users/auth/unlocks#create')
end
it 'routes to #show' do
expect(get: '/users/auth/unlock').to route_to('users/auth/unlocks#show')
end
end
end
spec/routing/users/auth/passwords_routing_spec.rb を作成
require 'rails_helper'
RSpec.describe Users::Auth::PasswordsController, type: :routing do
describe 'routing' do
it 'routes to #new' do
# expect(get: '/users/auth/password/new').to route_to('users/auth/passwords#new')
expect(get: '/users/auth/password/new').not_to be_routable
end
it 'routes to #new' do
expect(post: '/users/auth/password').to route_to('users/auth/passwords#create')
end
it 'routes to #edit' do
# expect(get: '/users/auth/password/edit').to route_to('users/auth/passwords#edit')
expect(get: '/users/auth/password/edit').not_to be_routable
expect(get: '/users/auth/password').to route_to('users/auth/passwords#edit')
end
it 'routes to #update' do
# expect(put: '/users/auth/password').to route_to('users/auth/passwords#update')
# expect(patch: '/users/auth/password').to route_to('users/auth/passwords#update')
expect(put: '/users/auth/password').not_to be_routable
expect(patch: '/users/auth/password').not_to be_routable
expect(put: '/users/auth/password/update').to route_to('users/auth/passwords#update')
expect(patch: '/users/auth/password/update').to route_to('users/auth/passwords#update')
end
end
end
spec/routing/users/auth/token_validations_routing_spec.rb を作成
require 'rails_helper'
RSpec.describe Users::Auth::TokenValidationsController, type: :routing do
describe 'routing' do
it 'routes to #validate_token' do
expect(get: '/users/auth/validate_token').to route_to('users/auth/token_validations#validate_token')
end
end
end
動作確認
% rspec spec/routing/users/auth/
F..F.FFFFFFFFF...F..
Failures:
1) Users::Auth::ConfirmationsController routing routes to #new
Failure/Error: expect(get: '/users/auth/confirmation/new').not_to be_routable
expected {:get=>"/users/auth/confirmation/new"} not to be routable, but it routes to {:controller=>"users/auth/confirmations", :action=>"new"}
# ./spec/routing/users/auth/confirmations_routing_spec.rb:7:in `block (3 levels) in '
<省略>
Finished in 0.07622 seconds (files took 1.92 seconds to load)
20 examples, 12 failures
想定通り、失敗(failures)が出ました。
これが無くなるように修正して行きます。
routes修正
修正前
% rails routes
new_users_user_session GET /users/auth/sign_in(.:format) users/auth/sessions#new
users_user_session POST /users/auth/sign_in(.:format) users/auth/sessions#create
destroy_users_user_session DELETE /users/auth/sign_out(.:format) users/auth/sessions#destroy
new_users_user_password GET /users/auth/password/new(.:format) users/auth/passwords#new
edit_users_user_password GET /users/auth/password/edit(.:format) users/auth/passwords#edit
users_user_password PATCH /users/auth/password(.:format) users/auth/passwords#update
PUT /users/auth/password(.:format) users/auth/passwords#update
POST /users/auth/password(.:format) users/auth/passwords#create
cancel_users_user_registration GET /users/auth/cancel(.:format) users/auth/registrations#cancel
new_users_user_registration GET /users/auth/sign_up(.:format) users/auth/registrations#new
edit_users_user_registration GET /users/auth/edit(.:format) users/auth/registrations#edit
users_user_registration PATCH /users/auth(.:format) users/auth/registrations#update
PUT /users/auth(.:format) users/auth/registrations#update
DELETE /users/auth(.:format) users/auth/registrations#destroy
POST /users/auth(.:format) users/auth/registrations#create
new_users_user_confirmation GET /users/auth/confirmation/new(.:format) users/auth/confirmations#new
users_user_confirmation GET /users/auth/confirmation(.:format) users/auth/confirmations#show
POST /users/auth/confirmation(.:format) users/auth/confirmations#create
new_users_user_unlock GET /users/auth/unlock/new(.:format) users/auth/unlocks#new
users_user_unlock GET /users/auth/unlock(.:format) users/auth/unlocks#show
POST /users/auth/unlock(.:format) users/auth/unlocks#create
users_auth_validate_token GET /users/auth/validate_token(.:format) users/auth/token_validations#validate_token
config/routes.rb を修正
- namespace :users do
- mount_devise_token_auth_for 'User', at: 'auth', controllers: {
- registrations: 'users/auth/registrations',
- confirmations: 'users/auth/confirmations',
- sessions: 'users/auth/sessions',
- unlocks: 'users/auth/unlocks',
- passwords: 'users/auth/passwords',
- token_validations: 'users/auth/token_validations'
- }
- end
devise_scope :user doの中に
# devise_token_auth
post 'users/auth/sign_up', to: 'users/auth/registrations#create', as: 'create_user_auth_registration'
put 'users/auth/update', to: 'users/auth/registrations#update', as: 'update_user_auth_registration'
patch 'users/auth/update', to: 'users/auth/registrations#update', as: nil
delete 'users/auth/destroy', to: 'users/auth/registrations#destroy', as: 'destroy_user_auth_registration'
post 'users/auth/confirmation', to: 'users/auth/confirmations#create', as: 'create_user_auth_confirmation'
get 'users/auth/confirmation', to: 'users/auth/confirmations#show', as: 'user_auth_confirmation'
post 'users/auth/sign_in', to: 'users/auth/sessions#create', as: 'create_user_auth_session'
delete 'users/auth/sign_out', to: 'users/auth/sessions#destroy', as: 'destroy_user_auth_session'
post 'users/auth/unlock', to: 'users/auth/unlocks#create', as: 'create_user_auth_unlock'
get 'users/auth/unlock', to: 'users/auth/unlocks#show', as: 'user_auth_unlock'
post 'users/auth/password', to: 'users/auth/passwords#create', as: 'create_user_auth_password'
get 'users/auth/password', to: 'users/auth/passwords#edit', as: 'edit_user_auth_password'
put 'users/auth/password/update', to: 'users/auth/passwords#update', as: 'update_user_auth_password'
patch 'users/auth/password/update', to: 'users/auth/passwords#update', as: nil
get 'users/auth/validate_token', to: 'users/auth/token_validations#validate_token', as: 'user_auth_validate_token'
修正後
% rails routes
create_user_auth_registration POST /users/auth/sign_up(.:format) users/auth/registrations#create
update_user_auth_registration PUT /users/auth/update(.:format) users/auth/registrations#update
PATCH /users/auth/update(.:format) users/auth/registrations#update
destroy_user_auth_registration DELETE /users/auth/destroy(.:format) users/auth/registrations#destroy
create_user_auth_confirmation POST /users/auth/confirmation(.:format) users/auth/confirmations#create
user_auth_confirmation GET /users/auth/confirmation(.:format) users/auth/confirmations#show
create_user_auth_session POST /users/auth/sign_in(.:format) users/auth/sessions#create
destroy_user_auth_session DELETE /users/auth/sign_out(.:format) users/auth/sessions#destroy
create_user_auth_unlock POST /users/auth/unlock(.:format) users/auth/unlocks#create
user_auth_unlock GET /users/auth/unlock(.:format) users/auth/unlocks#show
create_user_auth_password POST /users/auth/password(.:format) users/auth/passwords#create
edit_user_auth_password GET /users/auth/password(.:format) users/auth/passwords#edit
update_user_auth_password PUT /users/auth/password/update(.:format) users/auth/passwords#update
PATCH /users/auth/password/update(.:format) users/auth/passwords#update
user_auth_validate_token GET /users/auth/validate_token(.:format) users/auth/token_validations#validate_token
動作確認
% rspec spec/routing/users/auth/ .................... Finished in 0.0291 seconds (files took 1.77 seconds to load) 20 examples, 0 failures
失敗(failures)が無くなりました!
request spec
requestは変更したい所が多いので、一旦、現状が通るテストを書いて、その後にカスタマイズして行きます。従って、下記は逆に、コメントが新しい期待値になります。
Registrations
解りやすいようにControllerにも記載しているコメント(メソッド、パス、画面や処理名)を記載するようにしています。
前提条件やテストパターンは、検証観点の意味合いで、マトリクス(組み合わせ)に漏れがないように先に考えてから実装しています。
削除予約済み:削除は即時ではなく、一定期間後、バッチで削除するようにカスタマイズしていますので、参考にされる場合はケースから削除して取り込んでください。
ただ、今回はテストを通す為、アカウント削除は一旦、即時削除を期待値としています。
spec/requests/users/auth/registrations_spec.rb を作成
require 'rails_helper'
RSpec.describe 'Users::Auth::Registrations', type: :request do
# POST /users/auth/sign_up アカウント登録(処理)
# 前提条件
# なし
# テストパターン
# 未ログイン, ログイン中, ログイン中(削除予約済み) → データ&状態作成
# パラメータなし, 有効なパラメータ, 無効なパラメータ, ホワイトリストにないURL → 事前にデータ作成
describe 'POST #create' do
let!(:new_user) { FactoryBot.attributes_for(:user) }
let!(:valid_params) do
{
name: new_user[:name],
email: new_user[:email],
password: new_user[:password],
confirm_success_url: "#{FRONT_SITE_URL}sign_in"
}
end
let!(:invalid_params) do
{
name: new_user[:name],
email: nil,
password: new_user[:password],
confirm_success_url: "#{FRONT_SITE_URL}sign_in"
}
end
let!(:invalid_url_params) do
{
name: new_user[:name],
email: new_user[:email],
password: new_user[:password],
confirm_success_url: "#{BAD_SITE_URL}sign_in"
}
end
# テスト内容
shared_examples_for 'OK' do
it '作成・メールが送信される' do
expect do
before_count = ActionMailer::Base.deliveries.count
post create_user_auth_registration_path, params: params, headers: headers
expect(ActionMailer::Base.deliveries.count).to eq(before_count + 1) # メールアドレス確認のお願い
after_user = User.find_by(email: new_user[:email])
expect(after_user).not_to be_nil
expect(after_user.name).to eq(new_user[:name])
end.to change(User, :count).by(1)
end
end
shared_examples_for 'NG' do
it '作成・メールが送信されない' do
expect do
before_count = ActionMailer::Base.deliveries.count
post create_user_auth_registration_path, params: params, headers: headers
expect(ActionMailer::Base.deliveries.count).to eq(before_count)
end.to change(User, :count).by(0)
end
end
shared_examples_for 'ToOK' do # |alert, notice|
it '成功ステータス・JSONデータ' do
post create_user_auth_registration_path, params: params, headers: headers
expect(response).to be_successful
response_json = JSON.parse(response.body)
expect(response_json['status']).to eq('success')
# expect(response_json['status']).to be_nil
# expect(response_json['success']).to eq(true)
expect(response_json['errors']).to be_nil
# expect(response_json['data']['id']).to be_nil
expect(response_json['data']['name']).to eq(params[:name])
# expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
# expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
end
end
shared_examples_for 'ToNG' do # |alert, notice|
it '失敗ステータス・JSONデータ' do
post create_user_auth_registration_path, params: params, headers: headers
expect(response).to have_http_status(422)
response_json = JSON.parse(response.body)
expect(response_json['status']).to eq('error')
# expect(response_json['status']).to be_nil
# expect(response_json['success']).to eq(false)
expect(response_json['errors']).not_to be_nil
# expect(response_json['data']).not_to be_nil # Tips: パラメータなしの場合はnil
# expect(response_json['data']).to be_nil
# expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
# expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
end
end
# テストケース
shared_examples_for '[*]パラメータなし' do
let!(:params) { nil }
it_behaves_like 'NG'
it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[未ログイン]有効なパラメータ' do
let!(:params) { valid_params }
it_behaves_like 'OK'
it_behaves_like 'ToOK', nil, 'devise.registrations.signed_up_but_unconfirmed'
end
shared_examples_for '[ログイン中/削除予約済み]有効なパラメータ' do
let!(:params) { valid_params }
it_behaves_like 'OK'
it_behaves_like 'ToOK', nil, nil
# it_behaves_like 'NG'
# it_behaves_like 'ToNG', 'devise.failure.already_authenticated', nil
end
shared_examples_for '[未ログイン]無効なパラメータ' do
let!(:params) { invalid_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[ログイン中/削除予約済み]無効なパラメータ' do
let!(:params) { invalid_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG', 'devise.failure.already_authenticated', nil
end
shared_examples_for '[*]ホワイトリストにないURL' do
let!(:params) { invalid_url_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG', nil, nil
end
context '未ログイン' do
let!(:headers) { nil }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[未ログイン]有効なパラメータ'
it_behaves_like '[未ログイン]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
context 'ログイン中' do
include_context 'authログイン処理'
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[ログイン中/削除予約済み]有効なパラメータ'
it_behaves_like '[ログイン中/削除予約済み]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
context 'ログイン中(削除予約済み)' do
include_context 'authログイン処理', true
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[ログイン中/削除予約済み]有効なパラメータ'
it_behaves_like '[ログイン中/削除予約済み]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
end
<以降、省略>
※多いので、省略しています。最後に記載するコミット内容(まとめ)を参照してください。
spec/support/application_contexts.rb に追加
FRONT_SITE_URL = 'http://front.localhost.test/'.freeze BAD_SITE_URL = 'http://badsite.com/'.freeze
spec/support/user_contexts.rb に追加
shared_context 'authログイン処理' do |destroy_reserved_flag = false|
include_context 'ユーザー作成', destroy_reserved_flag
let!(:auth_token) { user.create_new_auth_token }
let!(:auth_headers) do
{
'uid' => auth_token['uid'],
'client' => auth_token['client'],
'access-token' => auth_token['access-token']
}
end
end
動作確認
% rspec spec/requests/users/auth/registrations_spec.rb
..............................F.F......F.F.........
Failures:
1) Users::Auth::Registrations PUT #update ログイン中 behaves like [ログイン中]有効なパラメータ behaves like OK 確認待ちメールアドレス・氏名が変更される
Failure/Error: expect(after_user.name).to eq(params[:name])
expected: "user(56)"
got: "user(58)"
(compared using ==)
Shared Example Group: "OK" called from ./spec/requests/users/auth/registrations_spec.rb:256
Shared Example Group: "[ログイン中]有効なパラメータ" called from ./spec/requests/users/auth/registrations_spec.rb:294
# ./spec/requests/users/auth/registrations_spec.rb:171:in `block (4 levels) in '
<省略>
Finished in 3.64 seconds (files took 1.93 seconds to load)
51 examples, 4 failures
ユーザー名が変更できていない。
こちらは独自に追加した項目なので、Devise Token Authの問題ではありません。
アカウント登録では対応しましたが、登録情報変更では対応が漏れました。
気付けるのはテスト駆動の良い所。
項目追加時の対応
序でにConcernで共通化しておきます。2箇所にあると修正漏れが発生する為です。
Concern便利ですね。 → Concernが便利かもと思った話
app/controllers/users/auth/registrations_controller.rb を修正
class Users::Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController
+ include Users::RegistrationsConcern
include DeviseTokenAuth::Concerns::SetUserByToken
skip_before_action :verify_authenticity_token
before_action :configure_sign_up_params, only: %i[create]
+ before_action :configure_account_update_params, only: %i[update]
<省略>
- protected
-
- # If you have extra params to permit, append them to the sanitizer.
- def configure_sign_up_params
- devise_parameter_sanitizer.permit(:sign_up, keys: %i[code name])
- end
end
app/controllers/users/registrations_controller.rb を修正
class Users::RegistrationsController < Devise::RegistrationsController + include Users::RegistrationsConcern <省略> - # If you have extra params to permit, append them to the sanitizer. - def configure_sign_up_params - devise_parameter_sanitizer.permit(:sign_up, keys: %i[code name]) - end - - # If you have extra params to permit, append them to the sanitizer. - def configure_account_update_params - devise_parameter_sanitizer.permit(:account_update, keys: [:name]) - end <省略>
app/controllers/concerns/users/registrations_concern.rb を作成
module Users::RegistrationsConcern
extend ActiveSupport::Concern
protected
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: %i[code name])
end
# If you have extra params to permit, append them to the sanitizer.
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: [:name])
end
end
再度、動作確認
% rspec spec/requests/users/auth/registrations_spec.rb ................................................... Finished in 3.42 seconds (files took 1.89 seconds to load) 51 examples, 0 failures
失敗(failures)が無くなりました!
Confirmations
spec/requests/users/auth/confirmations_spec.rb を作成
require 'rails_helper'
RSpec.describe 'Users::Auth::Confirmations', type: :request do
# POST /users/auth/confirmation メールアドレス確認[メール再送](処理)
# 前提条件
# なし
# テストパターン
# 未ログイン, ログイン中, ログイン中(削除予約済み) → データ&状態作成
# パラメータなし, 有効なパラメータ, 無効なパラメータ, ホワイトリストにないURL → 事前にデータ作成
describe 'POST #create' do
let!(:send_user) { FactoryBot.create(:user, confirmed_at: nil) }
let!(:valid_params) { { email: send_user.email, confirm_success_url: "#{FRONT_SITE_URL}sign_in" } }
let!(:invalid_params) { { email: nil, confirm_success_url: "#{FRONT_SITE_URL}sign_in" } }
let!(:invalid_url_params) { { email: send_user.email, confirm_success_url: "#{BAD_SITE_URL}sign_in" } }
# テスト内容
shared_examples_for 'OK' do
it 'メールが送信される' do
before_count = ActionMailer::Base.deliveries.count
post create_user_auth_confirmation_path, params: params, headers: headers
expect(ActionMailer::Base.deliveries.count).to eq(before_count + 1) # メールアドレス確認のお願い
end
end
shared_examples_for 'NG' do
it 'メールが送信されない' do
before_count = ActionMailer::Base.deliveries.count
post create_user_auth_registration_path, params: params, headers: headers
expect(ActionMailer::Base.deliveries.count).to eq(before_count)
end
end
shared_examples_for 'ToOK' do # |alert, notice|
it '成功ステータス・JSONデータ' do
post create_user_auth_confirmation_path, params: params, headers: headers
expect(response).to be_successful
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(true)
expect(response_json['errors']).to be_nil
expect(response_json['message']).not_to be_nil
# expect(response_json['message']).to be_nil
# expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
# expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
end
end
shared_examples_for 'ToNG' do # |alert, notice|
it '失敗ステータス・JSONデータ' do
post create_user_auth_confirmation_path, params: params, headers: headers
expect(response).to have_http_status(401)
# expect(response).to have_http_status(422)
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(false)
expect(response_json['errors']).not_to be_nil
expect(response_json['message']).to be_nil
# expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
# expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
end
end
# テストケース
shared_examples_for '[*]パラメータなし' do
let!(:params) { nil }
it_behaves_like 'NG'
it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[*]有効なパラメータ' do # Tips: ログイン中も出来ても良さそう
let!(:params) { valid_params }
it_behaves_like 'OK'
it_behaves_like 'ToOK', nil, 'devise.confirmations.send_instructions'
end
shared_examples_for '[*]無効なパラメータ' do
let!(:params) { invalid_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[*]ホワイトリストにないURL' do
let!(:params) { invalid_url_params }
it_behaves_like 'OK'
it_behaves_like 'ToOK', nil, nil
# it_behaves_like 'NG'
# it_behaves_like 'ToNG', nil, nil
end
context '未ログイン' do
let!(:headers) { nil }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[*]有効なパラメータ'
it_behaves_like '[*]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
context 'ログイン中' do
include_context 'authログイン処理'
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[*]有効なパラメータ'
it_behaves_like '[*]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
context 'ログイン中(削除予約済み)' do
include_context 'authログイン処理', true
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[*]有効なパラメータ'
it_behaves_like '[*]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
end
<以降、省略>
※多いので、省略しています。最後に記載するコミット内容(まとめ)を参照してください。
動作確認
% rspec spec/requests/users/auth/confirmations_spec.rb .............................................................................. Finished in 2.95 seconds (files took 1.76 seconds to load) 78 examples, 0 failures
OK!
Sessions
spec/requests/users/auth/sessions_spec.rb を作成
require 'rails_helper'
RSpec.describe 'Users::Auth::Sessions', type: :request do
# POST /users/auth/sign_in ログイン(処理)
# 前提条件
# なし
# テストパターン
# 未ログイン, 未ログイン(削除予約済み), ログイン中, ログイン中(削除予約済み) → データ&状態作成
# パラメータなし, 有効なパラメータ, 無効なパラメータ → 事前にデータ作成
describe 'POST #create' do
shared_context '有効なパラメータ' do
let!(:params) { { email: user.email, password: user.password } }
end
shared_context '無効なパラメータ' do
let!(:params) { { email: user.email, password: nil } }
end
# テスト内容
shared_examples_for 'ToOK' do # |alert, notice|
it '成功ステータス・JSONデータ・ヘッダ' do
post create_user_auth_session_path, params: params, headers: headers
expect(response).to be_successful
response_json = JSON.parse(response.body)
expect(response_json['success']).to be_nil
# expect(response_json['success']).to eq(true)
expect(response_json['errors']).to be_nil
# expect(response_json['data']['id']).to be_nil
expect(response_json['data']['name']).to eq(user.name)
# expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
# expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
expect(response.header['uid']).not_to be_nil
expect(response.header['client']).not_to be_nil
expect(response.header['access-token']).not_to be_nil
end
end
shared_examples_for 'ToNG' do # |alert, notice|
it '失敗ステータス・JSONデータ・ヘッダ' do
post create_user_auth_session_path, params: params, headers: headers
expect(response).to have_http_status(401)
# expect(response).to have_http_status(422)
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(false)
expect(response_json['errors']).not_to be_nil
expect(response_json['data']).to be_nil
# expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
# expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
expect(response.header['uid']).to be_nil
expect(response.header['client']).to be_nil
expect(response.header['access-token']).to be_nil
end
end
# テストケース
shared_examples_for '[*]パラメータなし' do
let!(:params) { nil }
it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[未ログイン]有効なパラメータ' do
include_context '有効なパラメータ'
it_behaves_like 'ToOK', nil, 'devise.sessions.signed_in'
end
shared_examples_for '[ログイン中/削除予約済み]有効なパラメータ' do
include_context '有効なパラメータ'
it_behaves_like 'ToOK', nil, nil
# it_behaves_like 'ToNG', 'devise.failure.already_authenticated', nil
end
shared_examples_for '[未ログイン]無効なパラメータ' do
include_context '無効なパラメータ'
it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[ログイン中/削除予約済み]無効なパラメータ' do
include_context '無効なパラメータ'
it_behaves_like 'ToNG', 'devise.failure.already_authenticated', nil
end
context '未ログイン' do
include_context 'ユーザー作成'
let!(:headers) { nil }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[未ログイン]有効なパラメータ'
it_behaves_like '[未ログイン]無効なパラメータ'
end
context '未ログイン(削除予約済み)' do
include_context 'ユーザー作成', true
let!(:headers) { nil }
# it_behaves_like '[*]パラメータなし' # Tips: 未ログインと同じ
it_behaves_like '[未ログイン]有効なパラメータ'
it_behaves_like '[未ログイン]無効なパラメータ'
end
context 'ログイン中' do
include_context 'authログイン処理'
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[ログイン中/削除予約済み]有効なパラメータ'
it_behaves_like '[ログイン中/削除予約済み]無効なパラメータ'
end
context 'ログイン中(削除予約済み)' do
include_context 'authログイン処理', true
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[ログイン中/削除予約済み]有効なパラメータ'
it_behaves_like '[ログイン中/削除予約済み]無効なパラメータ'
end
end
<以降、省略>
※多いので、省略しています。最後に記載するコミット内容(まとめ)を参照してください。
動作確認
% rspec spec/requests/users/auth/sessions_spec.rb .............. Finished in 1.3 seconds (files took 1.82 seconds to load) 14 examples, 0 failures
OK!
Unlocks
spec/requests/users/auth/unlocks_spec.rb を作成
require 'rails_helper'
RSpec.describe 'Users::Auth::Unlocks', type: :request do
# POST /users/auth/unlock アカウントロック解除[メール再送](処理)
# 前提条件
# なし
# テストパターン
# 未ログイン, ログイン中, ログイン中(削除予約済み) → データ&状態作成
# パラメータなし, 有効なパラメータ, 無効なパラメータ, ホワイトリストにないURL → 事前にデータ作成
describe 'POST #create' do
include_context 'アカウントロック解除トークン作成'
let!(:send_user) { FactoryBot.create(:user) }
let!(:valid_params) { { email: send_user.email, redirect_url: "#{FRONT_SITE_URL}sign_in" } }
let!(:invalid_params) { { email: nil, redirect_url: "#{FRONT_SITE_URL}sign_in" } }
let!(:invalid_url_params) { { email: send_user.email, redirect_url: BAD_SITE_URL } }
# テスト内容
shared_examples_for 'OK' do
it 'メールが送信される' do
before_count = ActionMailer::Base.deliveries.count
post create_user_auth_unlock_path, params: params, headers: headers
expect(ActionMailer::Base.deliveries.count).to eq(before_count + 1) # アカウントロックのお知らせ
end
end
shared_examples_for 'NG' do
it 'メールが送信されない' do
before_count = ActionMailer::Base.deliveries.count
post create_user_auth_unlock_path, params: params, headers: headers
expect(ActionMailer::Base.deliveries.count).to eq(before_count)
end
end
shared_examples_for 'ToOK' do # |alert, notice|
it '成功ステータス・JSONデータ' do
post create_user_auth_unlock_path, params: params, headers: headers
expect(response).to be_successful
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(true)
expect(response_json['errors']).to be_nil
expect(response_json['message']).not_to be_nil
# expect(response_json['message']).to be_nil
# expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
# expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
end
end
shared_examples_for 'ToNG' do |alert, notice|
it '失敗ステータス・JSONデータ' do
post create_user_auth_unlock_path, params: params, headers: headers
expect(response).to have_http_status(422)
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(false)
expect(response_json['errors']).not_to be_nil
expect(response_json['message']).to be_nil
expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
end
end
shared_examples_for 'ToNG(401)' do
it '失敗ステータス・JSONデータ' do
post create_user_auth_unlock_path, params: params, headers: headers
expect(response).to have_http_status(401)
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(false)
expect(response_json['errors']).not_to be_nil
expect(response_json['message']).to be_nil
end
end
# テストケース
shared_examples_for '[*]パラメータなし' do
let!(:params) { nil }
it_behaves_like 'NG'
it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[未ログイン]有効なパラメータ' do
let!(:params) { valid_params }
it_behaves_like 'OK'
it_behaves_like 'ToOK', nil, 'devise.unlocks.send_instructions'
end
shared_examples_for '[ログイン中/削除予約済み]有効なパラメータ' do
let!(:params) { valid_params }
it_behaves_like 'OK'
it_behaves_like 'ToOK', nil, nil
# it_behaves_like 'NG'
# it_behaves_like 'ToNG', 'devise.failure.already_authenticated', nil
end
shared_examples_for '[未ログイン]無効なパラメータ' do
let!(:params) { invalid_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG(401)'
# it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[ログイン中/削除予約済み]無効なパラメータ' do
let!(:params) { invalid_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG(401)'
# it_behaves_like 'ToNG', 'devise.failure.already_authenticated', nil
end
shared_examples_for '[*]ホワイトリストにないURL' do
let!(:params) { invalid_url_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG', nil, nil
end
context '未ログイン' do
let!(:headers) { nil }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[未ログイン]有効なパラメータ'
it_behaves_like '[未ログイン]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
context 'ログイン中' do
include_context 'authログイン処理'
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[ログイン中/削除予約済み]有効なパラメータ'
it_behaves_like '[ログイン中/削除予約済み]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
context 'ログイン中(削除予約済み)' do
include_context 'authログイン処理', true
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[ログイン中/削除予約済み]有効なパラメータ'
it_behaves_like '[ログイン中/削除予約済み]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
end
<以降、省略>
※多いので、省略しています。最後に記載するコミット内容(まとめ)を参照してください。
動作確認
% rspec spec/requests/users/auth/unlocks_spec.rb ................................................... Finished in 1.82 seconds (files took 1.82 seconds to load) 51 examples, 0 failures
OK!
Passwords
spec/requests/users/auth/passwords_spec.rb を作成
require 'rails_helper'
RSpec.describe 'Users::Auth::Passwords', type: :request do
# POST /users/auth/password パスワード再設定[メール送信](処理)
# 前提条件
# なし
# テストパターン
# 未ログイン, ログイン中, ログイン中(削除予約済み) → データ&状態作成
# パラメータなし, 有効なパラメータ, 無効なパラメータ, ホワイトリストにないURL → 事前にデータ作成
describe 'POST #create' do
let!(:send_user) { FactoryBot.create(:user) }
let!(:valid_params) { { email: send_user.email, redirect_url: "#{FRONT_SITE_URL}sign_in" } }
let!(:invalid_params) { { email: nil, redirect_url: "#{FRONT_SITE_URL}sign_in" } }
let!(:invalid_url_params) { { email: send_user.email, redirect_url: BAD_SITE_URL } }
# テスト内容
shared_examples_for 'OK' do
it 'メールが送信される' do
before_count = ActionMailer::Base.deliveries.count
post create_user_auth_password_path, params: params, headers: headers
expect(ActionMailer::Base.deliveries.count).to eq(before_count + 1) # パスワード再設定方法のお知らせ
end
end
shared_examples_for 'NG' do
it 'メールが送信されない' do
before_count = ActionMailer::Base.deliveries.count
post create_user_auth_password_path, params: params, headers: headers
expect(ActionMailer::Base.deliveries.count).to eq(before_count)
end
end
shared_examples_for 'ToOK' do # |alert, notice|
it '成功ステータス・JSONデータ' do
post create_user_auth_password_path, params: params, headers: headers
expect(response).to be_successful
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(true)
expect(response_json['errors']).to be_nil
expect(response_json['message']).not_to be_nil
# expect(response_json['message']).to be_nil
# expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
# expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
end
end
shared_examples_for 'ToNG' do |alert, notice|
it '失敗ステータス・JSONデータ' do
post create_user_auth_password_path, params: params, headers: headers
expect(response).to have_http_status(422)
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(false)
expect(response_json['errors']).not_to be_nil
expect(response_json['message']).to be_nil
expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
end
end
shared_examples_for 'ToNG(401)' do
it '失敗ステータス・JSONデータ' do
post create_user_auth_password_path, params: params, headers: headers
expect(response).to have_http_status(401)
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(false)
expect(response_json['errors']).not_to be_nil
expect(response_json['message']).to be_nil
end
end
# テストケース
shared_examples_for '[*]パラメータなし' do
let!(:params) { nil }
it_behaves_like 'NG'
it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[未ログイン]有効なパラメータ' do
let!(:params) { valid_params }
it_behaves_like 'OK'
it_behaves_like 'ToOK', nil, 'devise.passwords.send_instructions'
end
shared_examples_for '[ログイン中/削除予約済み]有効なパラメータ' do
let!(:params) { valid_params }
it_behaves_like 'OK'
it_behaves_like 'ToOK', nil, nil
# it_behaves_like 'NG'
# it_behaves_like 'ToNG', 'devise.failure.already_authenticated', nil
end
shared_examples_for '[未ログイン]無効なパラメータ' do
let!(:params) { invalid_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG(401)'
# it_behaves_like 'ToNG', nil, nil
end
shared_examples_for '[ログイン中/削除予約済み]無効なパラメータ' do
let!(:params) { invalid_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG(401)'
# it_behaves_like 'ToNG', 'devise.failure.already_authenticated', nil
end
shared_examples_for '[*]ホワイトリストにないURL' do
let!(:params) { invalid_url_params }
it_behaves_like 'NG'
it_behaves_like 'ToNG', nil, nil
end
context '未ログイン' do
let!(:headers) { nil }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[未ログイン]有効なパラメータ'
it_behaves_like '[未ログイン]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
context 'ログイン中' do
include_context 'authログイン処理'
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[ログイン中/削除予約済み]有効なパラメータ'
it_behaves_like '[ログイン中/削除予約済み]無効なパラメータ'
end
context 'ログイン中(削除予約済み)' do
include_context 'authログイン処理', true
let!(:headers) { auth_headers }
it_behaves_like '[*]パラメータなし'
it_behaves_like '[ログイン中/削除予約済み]有効なパラメータ'
it_behaves_like '[ログイン中/削除予約済み]無効なパラメータ'
it_behaves_like '[*]ホワイトリストにないURL'
end
end
<以降、省略>
※多いので、省略しています。最後に記載するコミット内容(まとめ)を参照してください。
動作確認
% rspec spec/requests/users/auth/passwords_spec.rb ....................................................................................................... Finished in 3.59 seconds (files took 2.31 seconds to load) 103 examples, 0 failures
OK!
TokenValidations
spec/requests/users/auth/token_validations_spec.rb を作成
require 'rails_helper'
RSpec.describe 'Users::Auth::TokenValidations', type: :request do
# GET /users/auth/validate_token トークン検証(処理)
# 前提条件
# なし
# テストパターン
# 未ログイン, ログイン中, ログイン中(削除予約済み) → データ&状態作成
describe 'GET #validate_token' do
# テスト内容
shared_examples_for 'ToOK' do |alert, notice|
it '成功ステータス・JSONデータ・ヘッダ' do
get user_auth_validate_token_path, headers: headers
expect(response).to be_successful
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(true)
expect(response_json['errors']).to be_nil
# expect(response_json['data']['id']).to be_nil
expect(response_json['data']['name']).to eq(user.name)
expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
expect(response.header['uid']).not_to be_nil
expect(response.header['client']).not_to be_nil
expect(response.header['access-token']).not_to be_nil
end
end
shared_examples_for 'ToNG' do |alert, notice|
it '失敗ステータス・JSONデータ・ヘッダ' do
get user_auth_validate_token_path, headers: headers
expect(response).to have_http_status(401)
# expect(response).to have_http_status(422)
response_json = JSON.parse(response.body)
expect(response_json['success']).to eq(false)
expect(response_json['errors']).not_to be_nil
expect(response_json['data']).to be_nil
expect(response_json['alert']).to alert.present? ? eq(I18n.t(alert)) : be_nil
expect(response_json['notice']).to notice.present? ? eq(I18n.t(notice)) : be_nil
expect(response.header['uid']).to be_nil
expect(response.header['client']).to be_nil
expect(response.header['access-token']).to be_nil
end
end
# テストケース
context '未ログイン' do
let!(:headers) { nil }
it_behaves_like 'ToNG', nil, nil
end
context 'ログイン中' do
include_context 'authログイン処理'
let!(:headers) { auth_headers }
it_behaves_like 'ToOK', nil, nil
end
context 'ログイン中(削除予約済み)' do
include_context 'authログイン処理', true
let!(:headers) { auth_headers }
it_behaves_like 'ToOK', nil, nil
end
end
end
動作確認
% rspec spec/requests/users/auth/token_validations_spec.rb ... Finished in 0.28778 seconds (files took 1.84 seconds to load) 3 examples, 0 failures
OK!
まとめ
RSpec書くのに朝・夜使って数日掛かりました。
Gemのソースも確認しながら書いたので、Devise Token Authに対する理解も深まりました。
元々、Devise向けのテストも書いていたので、流用して時間削減と同じ挙動に近づける観点が持てました。
320ケース(厳密にはitの実行回数)で、11秒ちょいで継続的に品質担保できるので書いて良かったです。
% rspec spec/requests/users/auth/ Finished in 11.16 seconds (files took 1.9 seconds to load) 300 examples, 0 failures % rspec spec/routing/users/auth Finished in 0.02776 seconds (files took 1.67 seconds to load) 20 examples, 0 failures
記載ソースやコミットのソースは、自由に使って頂いて構いません。
リポジトリのforkも歓迎です。
RSpecをリファクタリングして可読性と速度を上げる のコミット内容
nilと空で挙動が違ったりしたので、テストケースを追加しています。(DeviseMailerのテストも)
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/ab91a582a096015f4731b67af3325584bf68bd30
新しい期待値への対応 のコミット内容
かなり変更していますが、良い感じに仕上がりました。今後のフロント開発で使用する予定。
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/30cae0b530650774127717ec7b44108a94e5dc97
