Getting Jiggy With Testing
I’ve been developing Rails applications since version 1.2. Having worked through several major Rails upgrades, I’ve learned the true value of writing tests. Simply put, tests provide confidence that features will work as designed, and will continue to work in the future. Read on and I’ll delve into my own process of deciding what to test.
Testing that fails
In the very beginning of my Rails journey, I approached testing with a fairly simple assumption:
Because Rails follows the MVC pattern, if the website behaves the way I want it to work, then the controllers and models also work as expected. Therefore, I only really need to write integration tests and can get by with relying on Cucumber.
I was a Rails newb at that time, so I knew that I wasn’t a great Ruby developer quite yet, and being able to write tests in Cucumber’s domain-specific language was alluring. I wrote tests that really only poked and prodded the view layer for “things that should be there” and checked that pushing buttons yielded the results I wanted. I presumed that I could tease out problems merely by clicking the same way a user would.
Unsurprisingly, this process broke down pretty fast. If I wanted to fix any test failures, I had to read the stack trace. And if the problem started in the model layer, I had to read the stack trace line by line until I reached a particular line that seemed suspect. As you might imagine, this was tedious and led to lots of “shotgun coding” where I would change some code in one place and pray that the error either went away or changed into something else.
Furthermore, a single failure at a lower level, like the model level or database level, would cause a large amount of the test to fail. This made it frustrating to debug the true source of a problem without once again reading the stack traces line by line.
Conclusion: It’s not practical to rely solely on integration tests.
It’s no coincidence that this realization came to me about a month after I joined the team at Highgroove. Faced with building an extremely complicated app with a large test suite, I realized that there was no way an integration test could conduct the level of testing required in order for me to feel assured that everything was working as it should be. So I made another assumption: If the controller and model level are tested thoroughly enough, it’s reasonable to think that the view layer is working just fine, too.
Conclusion: It’s not practical to rely solely on controller and model unit tests.
How I test now
After swinging from one extreme to the other, I ended up landing on a strategy in the middle of my two previous methods. In order to reach 100 percent test coverage, I use elements of both of my previous strategies. I test only code that I wrote, and I test the code directly.
This means that:
- A method in a model will necessitate a unit test.
- A controller action will necessitate a controller test.
- A conditional statement in a view template necessitates an integration test.
- I don’t test declarative statements provided by Rails or other gems (i.e., I don’t test that a model validates a name).
A bit of caution about #5: I advise this method only if you are an expert Rails developer and already know what the Rails declarations accomplish. If you do not have much experience with Rails, it’s quite helpful to write tests for validations, associations or other gem code so that you can understand what’s happening within your own application. Otherwise, you should rest assured that the gem’s authors wrote a test suite already and that the gem operates as documented.
Your tests should provide you with the confidence that you’re delivering code that works as intended, old and new features both. As your confidence and proficiency with Rails increases, the need to test declarative statements drops significantly. For example, I find myself testing model validity less and less as I have little need to test whether Rails’ built-in validations work properly.
I have developed my own particular style of testing, but I’m curious about how you test. Do you do the five things above? If not, which testing philosophy do you follow?
Image credit: tomcensani