前回(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