Stub/Mock Recaptcha in Rspec

Viewed 195

I have a controller concern that I would like to test. I would like to stub or mock the verify_recaptch methods. One with event_name and other parameters (v3) and one without parameters (v2). However, I can not seem to stub/mock either.

Any solutions would be great. I had tried a few strategies mentioned on other posts, but none work. related: mocking/stubbing a controller recaptcha method with rspec in rails

Controller Concern

# frozen_string_literal: true

module FormCaptcha
  extend ActiveSupport::Concern

  def wrap_recaptcha(event_name, success_proc, failure_proc, minimum_score: 0.5, render_action: "new")
    success = verify_recaptcha(action: event_name, minimum_score: minimum_score, secret_key: '123')
    checkbox_success = verify_recaptcha unless success
    if success || checkbox_success
      success_proc.call
    else
      @show_checkbox_recaptcha = true unless success
      failure_proc.call
    end
  end

  private

  def show_checkbox_recaptcha
    @show_checkbox_recaptcha || params[:show_checkbox_recaptcha]
  end
end

Spec

# frozen_string_literal: true

require "rails_helper"

RSpec.describe FormCaptcha do
  class FormCaptchaTest 
    include FormCaptcha
    
    def create
      success_proc = proc {
        'success'
      }

      failure_proc = proc {
        'failure'
      }

      wrap_recaptcha('test_action', success_proc, failure_proc)
    end
  end

  describe "wrap_recaptcha" do
    before do
      @form_control = FormCaptchaTest.new
    end
    
    it "wrap_recaptcha should be included" do
      expect(@form_control).to respond_to(:wrap_recaptcha)
    end
    
    it "Verfy_recaptch false" do
      expect_any_instance_of(FormCaptchaTest).to receive(:verify_recaptcha).and_return(false)
      expect(create).to eql('failure')
    end
    
    it "Verfy_recaptch true" do
      expect_any_instance_of(FormCaptchaTest).to receive(:verify_recaptcha).and_return(true)
      expect(create).to eql('success')
    end
  end
end

Rspec results

FormCaptcha
  wrap_recaptcha
    wrap_recaptcha should be included
    Verfy_recaptch false (FAILED - 1)
    Verfy_recaptch true (FAILED - 2)

Failures:

  1) FormCaptcha wrap_recaptcha Verfy_recaptch false
     Failure/Error: expect(create).to eql('failure')
     
     ArgumentError:
       wrong number of arguments (given 0, expected 1+)
     # /Users/aa/.rvm/gems/ruby-2.7.1/gems/factory_bot-5.1.1/lib/factory_bot/strategy_syntax_method_registrar.rb:19:in `block in define_singular_strategy_method'
     # ./spec/controllers/concerns/form_captcha_spec.rb:33:in `block (3 levels) in <top (required)>'

  2) FormCaptcha wrap_recaptcha Verfy_recaptch true
     Failure/Error: expect(create).to eql('success')
     
     ArgumentError:
       wrong number of arguments (given 0, expected 1+)
     # /Users/aa/.rvm/gems/ruby-2.7.1/gems/factory_bot-5.1.1/lib/factory_bot/strategy_syntax_method_registrar.rb:19:in `block in define_singular_strategy_method'
     # ./spec/controllers/concerns/form_captcha_spec.rb:38:in `block (3 levels) in <top (required)>'

Finished in 0.22492 seconds (files took 3.46 seconds to load)
3 examples, 2 failures

Failed examples:

rspec ./spec/controllers/concerns/form_captcha_spec.rb:31 # FormCaptcha wrap_recaptcha Verfy_recaptch false
rspec ./spec/controllers/concerns/form_captcha_spec.rb:36 # FormCaptcha wrap_recaptcha Verfy_recaptch true
1 Answers

expect(create).to eql('success') is not calling FormCaptchaTest#create. Without supplying an object it is self.create which is the imported FactoryBot#create.

     ArgumentError:
       wrong number of arguments (given 0, expected 1+)
     # /Users/aa/.rvm/gems/ruby-2.7.1/gems/factory_bot-5.1.1/lib/factory_bot/strategy_syntax_method_registrar.rb:19:in `block in define_singular_strategy_method'
                                           ^^^^^^^^^^^^^^^^^

Furthermore, your test never actually runs wrap_recaptcha. And it doesn't have to mock for the whole class.

  describe "wrap_recaptcha" do
    # Use `let` to make test objects on the fly instead of instance variables.
    let(:form_control) { FormCaptchaTest.new }

    # this test is redundant, if the method doesn't exist the other tests will fail
    it "wrap_recaptcha should be included" do
      expect(form_control).to respond_to(:wrap_recaptcha)
    end

    # Use contexts to set up your conditions separate from the tests.
    # It helps organize and describe the test, and makes the situation reusable.
    context 'when verify_recaptcha fails' do
      # This will be run before each example.
      before {
        # Use allow for mocks.
        allow(form_control).to receive(:verify_recaptcha).and_return(false)
      }

      # Run the method, check what it returned.
      it 'runs the failure proc' do
        expect(form_control.capture).to eq 'failure'
      end
    end
    
    context 'when verify_recaptcha succeeds' do
      before {
        allow(form_control).to receive(:verify_recaptcha).and_return(true)
      }

      it 'runs the failure proc' do
        expect(form_control.capture).to eq 'success'
      end
    end
  end

I'd suggest a further change. It's odd that the wrap_recaptcha tests don't run wrap_recaptcha, they run a test wrapper around a wrapper. You can instead take advantage of a subject. This avoids having to build up a big test class which, as your test grows, might start interfering with itself.

RSpec.describe FormCaptcha do
  class FormCaptchaTest 
    include FormCaptcha
  end

  let(:form_control) { FormCaptchaTest.new }

  describe "wrap_recaptcha" do
    # You can run this explicitly as `subject`.
    subject do
      form_control.wrap_recaptcha(
        'test_action',
        proc { 'success' },
        proc { 'failure' }
      )
    end

    context 'when verify_recaptcha fails' do
      before {
        allow(form_control).to receive(:verify_recaptcha).and_return(false)
      }

      it "should be 'failure'" do
        expect(subject).to eq 'failure'
      end
    end
    
    context 'when verify_recaptcha succeeds' do
      before {
        allow(form_control).to receive(:verify_recaptcha).and_return(true)
      }

      # Or use the "one-liner" syntax equivalent to the above.
      it { is_expected.to eq 'success' }
    end
  end
end

Each test defines what it needs to avoid tests interfering with each other.

Related