Here’s a little RSpec design question.
As I’ve probably mentioned in various spots, I don’t naturally take to the RSpec massively-mocked style of testing. However, I’m currently on a Rails project that is using that style — unit tests don’t touch the database, functional tests don’t touch the models. It seems to be working for them, they certainly seem to have stuck with it over the course of this rather complex application.
Anyway, today, my pair and I added a new before_filter
to the layout of our application, where it gets called by every controller test in the system. This filter calls some user methods to put some dynamic user data on the screen.
Suddenly, we have failing tests all over the place, the vast majority of them related to mocks, mostly having to do with mocks or stubs that haven’t defined the method called in the filter and therefore thrown a mock expectation exception or, less frequently, a mock that does call these methods, but has an expectation that they will only be called once.
Wading through all these things is kind of daunting, and it’s not doing much to raise my general opinion of mock-heavy testing. That aside — I’m wondering how this is supposed to work. That is, I’m curious as to how a true RSpec expert would answer the following questions:
-
How would this issue — adding a new method call against an existing family of mocks — be handled in a perfectly designed and maintained RSpec test structure? What is the ideal here?
-
Given a system in progress that, while not bad, has had a lot of different people working on it in their own style, what’s the ideal way to proceed from here
Just wondering.
I don’t know if I’d call myself a “true RSpec expert”, but I’ve delivered several projects in it, and I hear where you’re coming from. Controllers are a frequent source of mock break pain because they touch so many things; change your update method from a save to a save!, and suddenly all hell breaks loose.
Two suggestions:
– For what it’s worth, and I know it’s old advice, but the more you have your controllers try to do the suckier it gets. If you need to render additional content, offload that into a dedicated view-rendering object, Presenter-style, and mock that as necessary. If it’s a matter of data munging on an incoming POST, offload more of that into your model (or get a PORO to quack like a model, ActiveModel-style) and let it handle dirty params (within reason). Because those boundaries are more clearly defined and change less often you should hopefully experience less pain, and those classes can be tested more granularly.
– In your specs, offload generalized mock creation into dedicated helper methods or setup blocks. Yes, I know setup blocks are evil, but sometimes the dramatic improvement in test maintainability is worth the tradeoff.
generally when using a before filter that’s going to be used a lot, i’ll stick that setup and examples specific to that in a shared_examples_for block. for instance, authentication.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
add_before_filter_mocks_to_existing.rb
hosted with ❤ by GitHub
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
authentication_helper.rb
hosted with ❤ by GitHub
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
some_controller_spec.rb
hosted with ❤ by GitHub
here, authentication helper an some controller spec is how i would do a project like this from the start.
the other file in the gist shows how you can use shared examples placed after an existing before block that sets up mocks to add stub methods or expectations. that part is not pretty, but works.