前回(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も歓迎です。

今回のコミット内容
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/c080465fba98740fc1e26a773bc380beb3b7902d

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

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です