1

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
davidaap
  • 1,569
  • 1
  • 18
  • 43

1 Answers1

0

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.

Lam Phan
  • 3,405
  • 2
  • 9
  • 20
  • Thanks, however I also tried this and got into another problem. For some reason, it also wants me to provide a mock for `:info`. This is the message I get: `NoMethodError: unmocked method :info, expected one of [:error]`. However, I am clearly not using any `Rails.logger.info` inside my action. – davidaap May 03 '21 at 14:52
  • @darmendarizp, Rails core call logger.info for requests and you're testing `:request` type (test controller) so it requires mock `:info`. You can check that from this source: https://github.com/rails/rails/blob/main/railties/lib/rails/rack/logger.rb – Lam Phan May 03 '21 at 15:35
  • Thanks, I marked your answer as the accepted one. Now I am having another problem but it is unrelated to this. My `mock.expect :error` needs to be called multiple times (I guess by Rails internally). The test passes if I add a couple more of `mock.expect :error` with a random string as the argument. But I had to basically guess I needed 3 more. And I didn't like this approach. Is there a way to make a expect that works for each call afterwards? Something like `mockImplementation` in Jest? – davidaap May 03 '21 at 17:56