Replacing RSpec controller specs, part 2: Feature specs
One of the things I admire about the Rails team is their willingness to remove features that no longer make sense to keep in the framework. In Rails 5.0, two overused testing helpers got the axe:
assert_template. These methods are commonly used when testing controllers, but often result in tests that know more about the underlying application code than they should. As DHH pointed out in the discussion about this change, “testing the innards of the controller is just not a good idea.” I agree. Does a browser care which view file was responsible for the HTML it got back from its request to your server? Does it need to know which instance variables got assigned as Ruby worked to craft its response? No–and chances are, your tests don’t need to know this, either.
If you must use these helpers in Rails 5, you can restore them via the rails-controller-testing gem. rails-controller-testing will keep your test suite happy as you upgrade existing apps to Rails 5.0, but it’s not recommended for new apps–and even if an upgrade to the latest and greatest Rails isn’t on your immediate horizon, you can begin to rethink and refactor your test suite to move away from controller specs entirely.
In part one of this series, I converted a spec that verified an application’s authentication layer, from a controller spec to an RSpec request spec. In this post, I’ll share another option. Like before, I’m going to take an example from Everyday Rails Testing with RSpec, modify it slightly for clarity outside of the book’s context, and talk about how we can future-proof it. The original sample code is a Rails 4.1 app, and is available on GitHub.
From controller specs to feature specs
Here we’ve got coverage of a controller’s
show actions, split across six tests and making heavy use of both
assert_template, which gets called in RSpec via
expect(response).to render_template(...). It’s also stubbing out a lot of the
Contact model’s behavior, boosting the test’s speed and isolation, at the risk of making the test more difficult to read. And some of the examples are already hitting the database to create test data, anyway.
Now, these examples are somewhat contrived, because they came from a book in which I tried to show different testing techniques with a relatively small set of files, but I’ve seen controller tests similar to these in real, production applications. Let’s replace them with a single feature spec. We’ll define the scenario in which a visitor accesses the site, views the full directory, filters by letter, and finally clicks a contact’s name in the resulting list to view more details about that individual. Unlike the request spec example from part one of this series, we can use Capybara methods like
click_link to simulate browser interactions, and give us a nice, readable spec.
I like this style of test much better. It’s easier to read through and reason about–I can read a single scenario to understand this feature of the application. It provides a higher level of coverage, across multiple endpoints. It’s a more realistic use case. One more thing to like about it: Since the test coverage is now decoupled from a specific controller, it helps us refactor a potential design flaw in the controller. The heavy use of
assigns in the original controller test suggests that the controller is doing more than it should. Sure enough, the
index action, albeit small, has logic that can be extracted from the controller:
The actual extraction is beyond what I want to cover here, but I may dig into that in a future post. The outdated “fat models, skinny controllers” paradigm would suggest adding a method to the Contact model. Newer Rails conventions might move the code to a concern. Or a hexagonal approach could prompt you to create a standalone object dedicated to the app’s filter-by-letter feature.
For now, I’ll leave decisions like that up to you. The nice thing about testing at a higher level, though, is that you can perform these types of refactors with a reliable safety net. They ensure that your app’s many moving parts are talking to each other, without the risk of incorrectly mocking out incorrect behavior.
assert_template were extracted from Rails for a reason, and it’s time to move on from both them and, eventually, controller-level testing. Replacing controller tests with feature-style integration tests requires a little more thought than using request specs, but I believe the effort pays off in terms of readability, flexibility, and long-term test health. Thanks for reading!
blog comments powered by
Rails testing made simple
Learn to test Rails apps the way
I learned, building up tests step-by-step, in
Everyday Rails Testing
Expanded to include exclusive content and a complete sample Rails application.
Learn more »