Search code examples
ruby-on-railsactiverecorderror-handlingrspec

How to expect raise ActiveRecord::RecordNotFound rspec?


How to get the test pass for this error?

Rspec controller and result

context 'invalid confirmation_token' do
      subject do
        post signup_step5_path,
             params: {
               user: {
                 password: 'hoge',
                 password_confirmation: 'hoge',
                 confirmation_token: 'wrong_token'
               }
             }
      end

      let(:user) { User.find_by(confirmation_token: 'testtesttest') }

      it 'does not update user attributes and never create an end_point record' do
        expect { subject }.raise_error(ActiveRecord::RecordNotFound)

expected ActiveRecord::RecordNotFound but nothing was raised

controller-method I rescue ActiveRecord::RecordNotFound and render 404 page in the private method.


class Users::SignupController < ApplicationController
  layout 'devise'

  rescue_from ActiveRecord::RecordNotFound, with: :render404


def step5
    @user = User.find_by(confirmation_token: step5_params[:confirmation_token])
    raise ActiveRecord::RecordNotFound unless @user
    .....
    end

private

def render404(error = nil)
    logger.info "Rendering 404 with exception: #{error.message}" if error
    render file: Rails.root.join('public/404.ja.html'), status: :not_found
  end

end


Solution

  • First its probably a good idea to explain that the exception matchers will only actually match uncaught exceptions. Thats because its basically just a rescue statement and rescues the exception as it bubbles up the call stack and its intended to test that a peice of code raises an exception which its up to the consumer to catch - that is an example of testing the behavior.

    Testing that code raises and rescues a exception on the other hand is testing how it does its job.

    def foo
      raise SomeKindOfError
    end
    
    def bar
      begin 
        raise SomeKindOfError
      rescue SomeKindOfError
        puts "RSpec will never catch me!"
      end
    end
    
    describe "#foo" do
      it "raises an exception" do
        expect { foo }.to raise_exception(SomeKindOfError)
      end
    end
    
    describe "#bar" do
      it "rescues the exception" do
        expect { bar }.to_not raise_exception(SomeKindOfError)
      end
    end
    

    When you use rescue_from its basically just syntactic sugar for using an around_action callback to rescue the given exception:

    class ApplicationController
      around_action :handle_errors
    
      private
    
      def handle_errors
        begin 
          yield
        rescue SomeKindOfError
          do_something
        end
      end
    end
    

    While RSpec did at one point have bypass_rescue for controller specs the use of controller specs is greatly discouraged by both the Rails and RSpec teams and you're really just testing the implementation instead of the behavior.

    Instead you should test what the actual controller does instead of how it does it.

    context 'invalid confirmation_token' do
      # explicit use of subject is a code smell
      before do
        post signup_step5_path,
          params: {
            user: {
              password: 'hoge',
              password_confirmation: 'hoge',
              confirmation_token: 'wrong_token'
            }
          }
      end
      let(:user) { User.find_by(confirmation_token: 'testtesttest') }
      
      it 'does not update the users password' do
        expect(user.valid_password?('hoge')).to be_falsy
      end
      
      it 'returns a 404 - NOT FOUND' do
        expect(response).to have_http_status(:not_found)
      end
      
      # using Capybara in a feature spec is a better way to do this.
      it 'renders something' do
        expect(response.body).to match("Oh Noes!") 
      end
    end