Search code examples
ruby-on-railsminitest

How to mock a chain of methods in Minitest


It looks like the only source of information for stubbing a chain of methods properly are 10+ years ago:

https://www.viget.com/articles/stubbing-method-chains-with-mocha/

RoR: Chained Stub using Mocha

I feel pretty frustrated that I can't find information of how to do this properly. I want to basically mock Rails.logger.error.

UPDATE: I basically want to do something as simple as

def my_action
  Rails.logger.error "My Error"
  render json: { success: true }
end

And want to write a test like this:

it 'should call Rails.logger.error' do
  post my_action_url
  ???
end

Solution

  • I think maybe you misunderstood the term chain of methods in this case, they imply the chain of ActiveRecord::Relation those be able to append another. In your case Rails.logger is a ActiveSupport::Logger and that's it. You can mock the logger and test your case like this:

    mock = Minitest::Mock.new
    mock.expect :error, nil, ["My Error"] # expect method error be called
    Rails.stub :logger, mock do
     post my_action_url
    end
    mock.verify
    

    Beside that, I personally don't like the way they test by stub chain of method, it's so detail, for example: i have a test case for query top ten users and i write a stub for chain of methods such as User.where().order()..., it's ok at first, then suppose i need to refactor the query or create a database view top users for optimize performance purpose, as you see, i need to stub the chain of methods again. Why do we just treat the test case as black box, in my case, the test case should not know (and don't care) how i implement the query, it only check that the result should be the top ten users.

    update

    Since each request Rails internal call Logger.info then if you want ignore it, you could stub that function:

    def mock.info; end
    

    In case you want to test the number of method called or validate the error messages, you can use a spy (i think you already know those unit test concepts)

    mock = Minitest::Mock.new
    def mock.info; end
    spy = Hash.new { |hash, key| hash[key] = [] }
    mock.expect(:error, nil) do |error|
     spy[:error] << error
    end
    
    Rails.stub :logger, mock do
     post my_action_url
    end
    
    assert spy[:error].size == 1 # call time
    assert spy[:error] == ["My Error"] # error messages
    

    It's better to create a test helper method to reuse above code. You can improve that helper method behave like the mockImplementation in Jest if you want.