Rails wasn’t designed with testing in mind, but people seem to be doing their best with what they’ve got. What you’ll find below is my attempt at synthesizing all the helpful testing wisdom that people have shared with me over the years into a coherent approach to testing Rails applications. Perhaps you’ll find it useful.
Types of Tests
Acceptance tests are end-to-end tests that exercise the applications functionality from an external perspective, often the user’s.
Integration tests test more than one component, often in conjunction with code you don’t own.
Unit tests are responsible for exercising a single unit, typically an object in Ruby.
Nearly all the test I write fall into one of those buckets, and I insist that I know what bucket they fall into before I write them.
For most TDD advocates, TDD doesn’t just mean a single red-green-refactor loop: there’s an outer “acceptance test” loop and inner “unit test” loops.
The two types of tests do not serve the same purpose. You use the acceptance tests to specify and verify the functionality of what you’re implementing. You use the unit tests to guide you a good design for that software.
I write unit tests. I write lots of them. I break down logic into little objects that do one thing and play nicely with others. I try to make it so they aren’t particularly coupled to which others they’re told to play with.
I use mocks heavily to test the relationship of my objects to their peers and collaborators.
Some people argue that “true” unit tests are difficult or impossible to write for Rails’ main concepts: controllers and models. I’ll talk about controller tests below.
I find that if you aim to keep your domain logic out of the models as much as possible and keep them focused on querying and manipulating database state, you get tests that are still very valuable.
These tests fit the definition of “integration test” from the GOOS book
, but still serve roughly the same purpose as unit tests. You’re still focused on the API of your object and trying to keep its coupling to a minimum. It’s just that you end up considering the database state to be both an input and output of your object’s behaviour.
I write the bulk of my acceptance tests using Capybara, but any browser-driving tool will do here. (For API applications, I use only request specs for acceptance. More on that below.) Sadly, browser-driving tests can be very slow. For this reason, maintaining a large suite of them can be painful.
As an aside: sometimes people cite reliability as a reason these kinds of specs can be a problem. While it’s relatively easy to make flaky browser-driving specs, you can avoid most common sources of these kinds of problems with only a few tricks. I only see reliability to be an issue in organizations that won’t let the team spend time to fix these issues.
Depending on the size of the app, the surface area of its functionality, and the organization’s appetite for browser-driving tests, you may not want to cover every controller action and job in the app with browser-driving tests.
Here, we can make a little compromise. I cover most, if not all, of the controller actions and jobs in the app with integration specs.
Rails web applications are typically very transactional. There are two main kinds of operations they perform: serving request and processing background jobs.
This makes controller (or request) and background job tests a nice place to ensure that your objects are all wired up correctly and accomplishing the desired outcome. In these tests I avoid mocking or stubbing anything out unless absolutely necessary, since that would defeat the purpose of these tests.
These are definitely not unit tests, though. If there’s significant complexity in the behaviour of the underlying objects, I leave it to the unit tests to specify. These integration tests should just assert that the endpoint or job in question does roughly what it’s supposed to do.
I follow the parts of the mockist TDD style laid out in the GOOS book that work well in a Rails app and make small compromises where I need to.
I follow the outer “ATDD
” loop as much as possible by writing acceptance tests for most features, especially important one. I integration test my jobs and endpoints to make up the ground when a full browser-driving integration test isn’t worth it.
I stick to the inner unit test TDD loop when building out the features, but make allowances for some different kinds of tests that aren’t “true” unit tests, but are helpful for testing Rails applications.
There are other allowances that I find useful, but in smaller applications I don’t need to make them, so they’ll be a topic for another day. I hope you found this useful. If you have differing thoughts on Rails applications should be tested, I’d love to hear them! Give me a shout over on Twitter