Mocking collaborators on controllers / stubbing class methods

I’m fond of mocking collaborators on controllers, to keep’em highly focused on their job when testing (which is forwarding incoming params, sending responses, etc).

The controller is generally the web app’s main integration point, where things are forwarded and global constants are directly referenced, such as classes.

When you mock or stub something on a controller, you usually have to work with global constants, like:

allow(Post).to receive(:new).and_return(post)

This kind of mocking has some potential problems, because it deals with global state. Weird errors may pop up if you try to parallelize your test suite, for example. There are other concerning problems as well.

Recently I’m experimenting with stubbing private methods, exceptionally on controllers. As long as your interfaces are simple, you don’t make a mess and you have some integration tests, it doesn’t feel that bad, I think. Something like:

# Controller
private def build_post
  Post.new
end

# Test
@controller.stub(:build_post).and_return(post)

Stubbing out the build_post method makes the mentioned problem go away, because we are acting on an instance. We can also make getters / setters on the controller, but that alternative actually feels worse.

Do you guys have any thoughts on the matter?

Thanks.

My understanding is that when you stub a object’s methods, the real object isn’t modified, it just gets ‘wrapped’ by RSpec so that any messages to it are intercepted. There shouldn’t be any problem with shared global state.

Hi Andy,

Imagine that we mock Post (a global constant), but two tests are accessing Post at the same time. Post is a global contant, so a stubbed method can potentially break the other test, because an unexpected return value will be delivered by the stubbed method into the other test.

I’ve had problems like this with Minitest::Hell. Once I ran a test suite using it all hell broke loose, because some tests were stubbing out class methods using any_instance (which is provided by the mocha gem).

I don’t know if there’s a way to get around this problem completely. If you use mocks and stubs in your test suite, you will eventually need to stub class methods.

I understand this may not be a problem most of the time (unless you try something crazy like parallelizing your test suite with threads), but it’s a better practice anyway to use mocks and stubs on injected dependencies - in that case, because we are inverting control, the described problem ends up disappearing.

P.S.: Because the controller is the foremost integration point of the application, that’s where you end up stubbing class methods the most.

Sorry if the problem wasn’t clear :slightly_smiling: