Search code examples
testingrspecrequestcapybaraspecifications

undefined method sign_in for rspec


I'm writing a simple controller test to ensure that we get a 200 status upon a GET request. Every answer I've encountered online says to make sure we are including the correct Devise helper in spec_helper.rb, but nothing I've tried seems to have helped.

Error:

NoMethodError:
       undefined method `sign_in' for #<RSpec::ExampleGroups::ApplicationFunnelController::Index::WhenAccessed "returns 200" (./spec/requests/application_funnel_controller_spec.rb:7)>

Controller test:

require 'spec_helper'

RSpec.describe ApplicationFunnelController, type: :request do
  describe "index" do
    context "when accessed" do
      it "returns 200" do
        user = FactoryBot.create(:user)
        group = FactoryBot.create(:group, name: 'Testers')
        user.groups << group
        sign_in user

        get become_a_member_path

        expect(response.status).to eq 200
      end
    end
  end
end

spec_helper.rb:

ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require "database_cleaner"
require "rspec/rails"
require "capybara/rspec"
require "capybara/rails"
require "devise"

class ActiveSupport::TestCase
  # Run tests in parallel with specified workers
  parallelize(workers: :number_of_processors)

  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

RSpec.configure do |config|
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include Devise::Test::ControllerHelpers, type: :view
  config.include Devise::TestHelpers, type: :controller
  config.include Devise::Test::ControllerHelpers, type: :routing
  config.include Devise::TestHelpers, type: :routing
  config.infer_spec_type_from_file_location!

  DEFAULT_CLEANER_STRATEGY = :transaction

  def with_truncation
    DatabaseCleaner.strategy = :truncation
    yield
  ensure
    DatabaseCleaner.strategy = DEFAULT_CLEANER_STRATEGY
  end

  config.include ActiveSupport::Testing::TimeHelpers

  config.before :suite do
    ActiveJob::Base.queue_adapter = :test
    DatabaseCleaner.strategy = DEFAULT_CLEANER_STRATEGY
    unless ENV["SEED"] == "false"
      DatabaseCleaner.clean_with(:truncation, except: %w(ar_internal_metadata schema_migrations))
    end
    Rails.application.load_tasks
  end

  config.before { ActionMailer::Base.deliveries.clear }
end

Solution

  • After a few search, and reading through the docs, I think a found an answer.

    First of all, type: :request in a controller spec will not work as far as I know. There is a difference between Controller Specs & Request Specs. I recommend you read the links provided so you can consider what type of test you want to include in your codebase.

    Now, say you want to sign in a user factory in a Controller spec, what you can do is create a ControllerMacros module, and place it in spec/support:

    # =================================
    # spec/support/controller_macros.rb
    
    module ControllerMacros
      def login_user(user)
        @request.env["devise.mapping"] = Devise.mappings[:user]
        sign_in user
      end
    end
    
    # ====================
    # spec/rails_helper.rb
    
    require_relative 'support/controller_macros'
    
    RSpec.configure do |config|
      # For Devise > 4.1.1
      config.include Devise::Test::ControllerHelpers, :type => :controller
      # Use the following instead if you are on Devise <= 4.1.1
      # config.include Devise::TestHelpers, :type => :controller
      config.extend ControllerMacros, :type => :controller
    end
    
    # ======================================================
    # spec/controllers/application_funnel_controller_spec.rb
    
    RSpec.describe ApplicationFunnelController do
      describe "index" do
        context "when accessed" do
          # You can optionally add this in you example.
          before do
            FactoryBot.create(:user).tap do |user|
              group = FactoryBot.create(:group, name: 'Testers')
              user.groups << group
              login_user user
            end
          end
    
          it "returns 200" do
            get become_a_member_path
    
            expect(response.status).to eq 200
          end
        end
      end
    end
    

    Loading the module this way:

    config.extend ControllerMacros, :type => :controller
    

    should allow you to use the login_user method in your controller specs.

    If it comes to a point where this is annoying to repeat, you can optionally do this:

    module ControllerMacros
      def login_user
        before(:each) do
          @request.env["devise.mapping"] = Devise.mappings[:user]
          user = FactoryBot.create(:user)
          sign_in user
        end
      end
    end
    

    and simply call the method in your controller spec:

    RSpec.describe ApplicationFunnelController do
      login_user
    
      # Tests here...
    end
    

    You can find more about testing Devise with Rspec in their repository wiki: