Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Mocking/stubbing an ApplicationHelper method in a view spec. #780

Closed
jdickey opened this Issue · 14 comments

6 participants

@jdickey

There should be an easier/more intuitive way to double (stub or mock) ApplicationHelper methods called by view code from within a view spec (RSpec2, MiniTest, whatever).

Given a view (in HAML):

%table.table.table-striped.table-bordered.table-hover
  %caption
    %h1 All registered users
  %thead
    %tr
      %th Pen Name
      %th Bio
  %tbody
  - @users.each do |user|
    %tr
      %td= user[:name]
      %td.bio!= format_newlines_as_html_paras user[:bio]

what I want to do in my view spec (RSpec2 here):

require 'spec_helper'

describe 'users/index' do

  before :each do
    @users = []
    3.times.each {@users << FactoryGirl.build_stubbed(:user)}
  end

  # ...

  describe 'calls the expected helpers, including' do

    it 'format_newlines_as_html_paras, once' do
      ActionView::Base.any_instance.stub(:format_newlines_as_html_paras).
        and_return "FOODOOFOO"
      render
      rendered.should have_content 'FOODOOFOO'
    end

  end # describe 'calls the expected helpers, including'

end # describe 'users/index'

That spec, as written, does not pass, and I've been flailing around for several hours trying to find a way that does.

What's the point?

By doubling the ApplicationHelper method within the view spec, we remove the dependency on the actual ApplicationHelper method in order for the view spec to pass. We can use expectations to verify that the helper method is called the expected number of times, and we don't care about the method's inputs or outputs at this point; we're asserting the interface, not the implementation logic per se.

Any ideas?

@greggroth

ActionView::Base is further down the inheritance chain than your helper, so your helper's method will be used before the stub. For example:

require 'rspec'

class Foo
end

class Bar < Foo
  def baz
    "Yep"
  end
end

describe Foo do
  it "stubs baz" do
    Foo.any_instance.stub(:baz) { "Nope" }
    expect(Bar.new.baz).to eq("Nope")  # Fails
  end
end
Failures:

  1) Foo stubs baz
     Failure/Error: expect(Bar.new.baz).to eq("Nope")  # Fails

       expected: "Nope"
            got: "Yep"

       (compared using ==)
     # ./test_spec.rb:14:in `block (2 levels) in <top (required)>'

Finished in 0.00108 seconds
1 example, 1 failure

The method defined on Bar is used before the stub. You will have better luck stubbing that helper method if you stub directly on the helper class rather than ActionView::Base.

@JonRowe
Owner

For the record rspec-rails is mostly a wrapper around Rails own test helpers, so if they don't support it chances are we don't either.

@cupakromer
Collaborator

From a philosophical perspective, I'm not sure I agree with stubbing/mocking helpers. For a view spec, you are verifying the output generated. If I talk to a collaborator (i.e. an external service, a model, etc) then I'm ok with potentially mocking those if necessary. However, I view helpers as "internal private methods" to the view.

IMO stubbing private methods is generally a bad idea and should be avoided.

With that being said, you should use view for the stub:

# ApplicationHelper.rb
module ApplicationHelper
  def format_message
    "hello there"
  end
end

# spec/views/foos/new.html.erb_spec.rb
  it "renders new foo form" do
    allow(view).to receive(:format_message).and_return("bye bye")

    render

    expect(rendered).to match(/bye bye/)
  end
@cupakromer cupakromer closed this
@jdickey

@cupakromer Pretty sure I parse 'helpers as "internal private methods"' differently than you do, but that stub-the-view practice is a huge improvement. Thanks!

@SaimonL

With Rspec 3 using allow

require 'rails_helper'

RSpec.describe 'pages/index', type: :view do
  before(:each) do
    # Stub no longer works
    #ApplicationController.any_instance.stub(:is_admin?).and_return(true)
    allow(view).to receive(:is_admin?).and_return(true)
    assign(:pages, [
      Page.create!(
        position: 1,
        title: 'Title',
        navigation: 'Navigation',
        content: 'MyText',
        published: false
      ),
      Page.create!(
        position: 1,
        title: 'Title 2',
        navigation: 'Navigation 2',
        content: 'MyText 2',
        published: false
      )
    ])
  end

  it 'renders a list of pages' do
    render
    assert_select "tr>td", :text => "Title".to_s, :count => 2
    assert_select "tr>td", :text => "Navigation".to_s, :count => 2
    assert_select "tr>td", :text => "MyText".to_s, :count => 2
  end
end

Will get you this error

clear;rspec spec/views/pages/index.html.slim_spec.rb 


pages/index
  renders a list of pages (FAILED - 1)

Failures:

  1) pages/index renders a list of pages
     Failure/Error: allow(view).to receive(:is_admin?).and_return(true)
     NoMethodError:
       undefined method `allow' for #
     # ./spec/views/pages/index.html.slim_spec.rb:6:in `block (2 levels) in '

Finished in 0.11035 seconds (files took 2.23 seconds to load)
1 example, 1 failure

I am stuck and googleing for the solution

@myronmarston

Have you configured rspec-mocks to only allow the old syntax?

https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/old-syntax

@SaimonL

I did that already

# If you're using rspec-core:
RSpec.configure do |config|
  config.mock_with :rspec do |mocks|
    mocks.syntax = :should
  end
end

Turns out that viewer has no business with controller. I had the method is_admin? in the controller method and then used "helper_method :is_admin?" to expose it to the helper.
Viewer should not test the logic of controller methods therefor the stub needs to be done in the viewer level. The solution then becomes:

view.stub(:is_admin?).and_return(true)

Which is the proper way to do it (this worked for me).

@myronmarston

I did that already

Exactly -- you've disabled the new non-monkey patching syntax (using allow) by enabling only :should -- so you got a NoMethodError for allow. This is by design.

@SaimonL

Am I doing this the right way? Is there a proper Rspec 3 way to do this?
what should I set the

mocks.syntax = :should
to?

I know if I don't set it to ":should" then "shoulda-matchers" throws all kinds of error.
I want to learn how to do this the right way.

@myronmarston

If you want both syntaxes to be enabled, set it to mocks.syntax = [:should, :expect]. However, if you don't have a specific reason to use the old :should syntax, I think it's best to stick with the new non-monkey patched syntax (which doesn't require any config to enable).

I know if I don't set it to ":should" then "shoulda-matchers" throws all kinds of error.
I want to learn how to do this the right way.

What errors do you get? I didn't think shoulda-matchers had any dependencies on rspec-mocks. Also, it's had some updates for RSpec 3:

https://github.com/thoughtbot/shoulda-matchers/blob/master/NEWS.md#v-250

...so mocks.syntax = :should being required for should-matchers seems very surprising.

@SaimonL

OK I have done the "bundle update" and a bunch of things updated. I see shuda updated to "shoulda-matchers (2.6.1)" and then I change the configuration to:

  config.mock_with :rspec do |mocks|
    mocks.syntax = :expect
  end

and then changed all the should syntax to expect and wrapped the action in the expect. So far so good. Now the problem is I have one Stub in model, controller, routing and view stub in Viewer none of them work now. I get error messages:

Failure/Error: view.stub(:is_admin?).and_return(true)
     NoMethodError:
       undefined method `stub'

Failure/Error: ApplicationController.any_instance.stub(:is_admin?).and_return(true)
     NoMethodError:
       undefined method `any_instance'

How do I stub those definitions that takes no arguments and I want it to return true.
I see "rspec-mocks" what is that? Should I use that gem?

@cupakromer
Collaborator

Your failing examples are using the old should syntax. Either enable both syntaxes as Myron pointed out, or convert them to the new allow and allow_any_instance alternatives.

@SaimonL

Got it

allow_any_instance_of(Page).to receive(:set_navigation).and_return(false)

So the result is the old should syntax is gone completely from my test and I no longer specify "mocks.syntax = :should" and all the test runs fine.

Thank you guys very much. You guys are the best!

@SaimonL

For those who are moving to Rspec 3:

Change (Instead of Image your your own Model, same goes for position column):

Image.any_instance.should_receive(:update).with({ 'position' => '1' })
# TO
expect_any_instance_of(Image).to receive(:update).with({ 'position' => '1' })

Change (test negative cases):

Image.any_instance.stub(:save).and_return(false)
# TO
allow_any_instance_of(Image).to receive(:save).and_return(false)

For those of you who are using PaperclipStub change your Paperclip Macros to:

module PaperclipMacros
  def stub_paperclip(model)
    before do
      allow_any_instance_of(model).to receive(:save_attached_files).and_return(true)
      allow_any_instance_of(model).to receive(:delete_attached_files).and_return(true)
      allow_any_instance_of(Paperclip::Attachment).to receive(:post_process).and_return(true)
    end
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.