lompy
lompy

Reputation: 351

How to stub method on instance variable?

So I have this simple ruby class:

class GetRequestList
  def initialize(current_user, filter_hash)
    @authorizer = RequestAuthorizer.new(current_user)
    @filter    = RequestFilter.new(filter_hash)
  end
  def generate
    Request.send_chain(@authorizer.method_chain)
           .send_chain(@filter.method_chain)
  end
end

And I want to test that Request receives two send_chain methods in isolation of RequestAuthorizer and RequestFilter implementations. To do that I'm trying to use some stubs:

require 'test_helper'

class GetRequestListTest < ActiveSupport::TestCase

  test "request should be filtered by filter and role" do
    Request.expects(:send_chain).twice.returns([build(:request)])
    RequestFilter.stubs(:new)
    RequestFilter.any_instance.stubs(:method_chain).returns([])
    RequestAuthorizer.stubs(:new)
    RequestAuthorizer.any_instance.stubs(:method_chain).returns([])
    assert GetRequestList.new(:current_user, :filter).generate.size == 1
  end
end

You see what is wrong. stubs(:new) returns nil and there is no instances of RequestAuthorizer and RequestFilter in instance variables of GetRequestList and we get an error. I can't figure out how to stub methods on instance variables. Any suggestions?

Upvotes: 2

Views: 5653

Answers (3)

Richard Wan
Richard Wan

Reputation: 125

have you tried exposing the instance variable so you could stub it?

GetRequestList.new(:current_user, :filter).tap do |it|
  def it.authorizer
    @authorizer
  end
  install_stubs(it.authorizer)
end.generate

Upvotes: 0

Frederick Cheung
Frederick Cheung

Reputation: 84172

Instead of stubbing out new to return no value, have it return something e.g.

mock_request_filter = mock()
RequestFilter.stubs(:new).returns(mock_filter)

This also enables you to get read of the stubs on any_instance - just set them on mock_request_filter instead.

Upvotes: 5

Ismael
Ismael

Reputation: 16730

This is why you should wrap instance variables inside methods. Take a look at this approach. This way your tests doesn't know about RequestFilter or RequestAuthorizer. And now you also asset that you get them as params. Notice that I also wrapped authorizer and filter initialization inside a method. You could also wrap both inside another one if your main initializer method gets more stuff on it.

class GetRequestList
  def initialize(current_user, filter_hash)
    initialize_authorizer
    initialize_filter
  end

  def generate
    Request.send_chain(authorizer_method_chain)
           .send_chain(filter_method_chain)
  end

  private

  def initialize_authorizer
    @authorizer = RequestAuthorizer.new(current_user)
  end

  def initialize_filter
    @filter = RequestFilter.new(filter_hash)
  end

  def authorizer_method_chain
    @authorizer.method_chain
  end

  def filter_method_chain
    @filter.method_chain
  end
end

and the test

require 'test_helper'

class GetRequestListTest < ActiveSupport::TestCase

  test "request should be filtered by filter and role" do
    get_request_list = GetRequestList.new(:current_user, :filter)
    get_request_list.stubs(:initialize_authorizer)
    get_request_list.stubs(:initialize_filter)

    get_request_list.stubs(:authorizer_method_chain).returns(:authorizer_method_chain)
    get_request_list.stubs(:filter_method_chain).returns(:filter_method_chain)

    Request.expects(:send_chain).with(:authorizer_method_chain).returns([build(:request)])
    Request.expects(:send_chain).with(:filter_method_chain).returns([build(:request)])

    assert get_request_list.generate.size == 1
  end
end

I also used a symbol to replace the authorizer and filter objects because you don't even need them to be mocks. It could also be some other thing like 1 and 2, but keeping symbols or strings lets you kinda name things properly.

Upvotes: 1

Related Questions