Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stubbing a helper method from inside helper specs

I'm building a Rails application and formulating tests using RSpec.

I wrote tests for a method I'm creating called current_link_to. This method is supposed to check whether the current page corresponds to the path I pass it and add the current class to the generated link in case it does.

Here is the spec:

require "spec_helper"

describe ApplicationHelper do
  describe "#current_link_to" do
    let(:name)     { "Products" }
    let(:path)     { products_path }
    let(:rendered) { current_link_to(name, path) }

    context "when the given path is the current path" do
      before { visit(path) }

      it "should return a link with the current class" do
        # Uses the gem "rspec-html-matchers" (https://github.com/kucaahbe/rspec-html-matchers)
        expect(rendered).to have_tag("a", with: { href: path, class: "current" }) do
          with_text(name)
        end
      end
    end

    context "when the given path is not the current path" do
      before { visit(about_path) }

      it "should return a link without the current class" do
        expect(rendered).to have_tag("a", with: { href: path }, without: { class: "current" } ) do
          with_text(name)
        end
      end
    end
  end
end

I then tried implementing my method following the spec:

module ApplicationHelper
  def current_link_to(name, path, options={})
    options.merge!({ class: "#{options[:class]} current".strip }) if current_page?(path)

    link_to(name, path, options)
  end
end

However, the tests fail with the following error:

Failure/Error: let(:rendered) { current_link_to(name, path) }

RuntimeError: You cannot use helpers that need to determine the current page unless your view context provides a Request object in a #request method

Since I don't really need the current_page? helper method to perform checks on the request, I decided that it would make sense to stub it.

I tried the following methods, but none of them worked:

  1. helper.double(:current_page? => true)
    Seems to stub the helper.current_page? method, but it's not the same method that's being called by my function.
  2. allow(ActionView::Helpers::UrlHelper).to receive(:current_page?).and_return(true)
    The stub seems not to be effective at all

While writing this question I stumbled onto the solution. I managed to stub the current_page? method using this in a before block:

allow(self).to receive(:current_page?).and_return(true)

It worked, however this solution raised more questions than it really answered. I am now baffled over how this works, as it seems weird that self in a before block would respond to current_page? and that said method would in fact be exactly the same one my helper is calling.

Even after reading documentation and trying to figure out how this works by littering my code with puts calls, the following doubts still haunt me:

  • Why are helper methods available directly in the specs, when the RSpec docs mention that they should instead be available as methods on the helper object available in all helper specs?
  • How does stubbing the current_page? method on self in a RSpec before block somehow reflect onto the actual method that gets called by my helper? Does self in my helper for some reason reference the same self you can find in the before block? Is RSpec or Rails including and mixing stuff under the covers?
  • If the same self encompasses my spec and my helpers, what exactly does self refer to in this case and why is it the same everywhere?

It would be great if someone could help me figure this out because this is blowing my mind up, and I'm scared of using code that I don't really understand.

like image 550
Gabriele Cirulli Avatar asked Feb 22 '14 16:02

Gabriele Cirulli


1 Answers

With respect, you're testing a little too much functionality here. The trick is to test only the bits you need to test.

In this instance, you only need to test that the current class is added when it needs to be, and isn't when it doesn't need to be.

This code should do the trick for you:

require 'rails_helper'

# Specs in this file have access to a helper object that includes
# the ApplicationHelper. 
RSpec.describe ApplicationHelper, type: :helper do

  describe 'current_link_to' do
    let(:subject) { helper.current_link_to('some_name', 'some_path', options = {}) }

    context 'where the path is current' do
      before do
        allow(helper).to receive(:current_page?).and_return true
      end
      it 'should include the current class' do
        expect(subject).to match /current/
      end
    end

    context 'where the path is not current' do
      before do
        allow(helper).to receive(:current_page?).and_return false
      end
      it 'should not include the current class' do
        expect(subject).to_not match /current/
      end
    end
  end
end

I've been a little glib and only tested for the presence of 'current' in the returned string. You could test for something like 'class="current"' if you want to be more precise.

The other key is the comment at the top of the page, which Rails inserts into blank helper specs for you:

# Specs in this file have access to a helper object that includes
# the ApplicationHelper. 

That means that you can use 'helper' where in your comment above you were using 'self', which makes things a little clearer (imho)

Hope it helps!

like image 89
TerryS Avatar answered Oct 16 '22 09:10

TerryS