2

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?

Machavity
  • 30,841
  • 27
  • 92
  • 100
lompy
  • 341
  • 1
  • 3
  • 12

3 Answers3

5

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.

Gordon Seidoh Worley
  • 7,839
  • 6
  • 45
  • 82
Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • Thanks for quick reply. Solved with `method_chain_mocker = mock(method_chain: [])` but going to try Ismael answer too. – lompy Jun 30 '13 at 19:28
1

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.

Ismael Abreu
  • 16,443
  • 6
  • 61
  • 75
  • Your solution needs to pass current_user and filter_hash to new initializers. Either way, I've herd of this 'a method does one thing' idea, but isn't that too much lines for just assign variables and call methods on them? And why my tests shouldn't know about theese Objects if that's the essence of GetRequestList class: to init those two and call methods on them? – lompy Jun 30 '13 at 19:43
  • Also found that Request expects only one invocation of send_chain, as it returns Array, my fault. – lompy Jun 30 '13 at 20:10
  • You want your unit tests to focus on the smallest thing possible. So you you don't want to bloat them with knowledge about other classes names. Keeping this knowledge inside a method seems a good solution. – Ismael Abreu Jul 02 '13 at 00:44
  • About the initializer method. Yeah, you can keep it the way you have it instead of extraction to different methods, it's totally up to you. – Ismael Abreu Jul 02 '13 at 00:46
0

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
Richard Wan
  • 125
  • 1
  • 1