Rails Testing: Not Just for the Paranoidby Gregory Brown
It's true that writing tests for your applications means writing more code. However, unless you are excellent at writing bug-free software that never needs to change or be worked on by anyone else, it's safe to say that testing is not optional, but essential while working with Rails.
Ruby is a very adaptive and malleable language. Rails pushes this to its functional limit and introduces a ton of new behaviors: many helpful, some surprising. Without tests to ensure that your application is behaving as you intended it to, it's a near promise that you will get bitten.
The real issue most people have with testing is not that they think it's a bad idea, but that it often means a whole lot more configuration, a whole lot more to learn, and lots of things that smell like extra work. Folks who have been in that crowd will be pleasantly surprised when working with Rails.
I'll start by giving an overview of the testing facilities built into the framework, and then we'll work with them hands-on by layering some new tests and functionality into Tasty, the mini app we've been building across the Understanding ActiveRecord two-part article.
If you're not already partially familiar with Rails testing, you'll want to read the section on test/unit from Understanding ActiveRecord: A Gentle Introduction to the Heart of Rails (Part 2). You'll also want to grab the source for Tasty so you can follow along with the rest of this article.
The Rails Testing Toolbox
Rails offers three kinds of testing: unit tests, functional tests, and integration tests. All three have important roles to play in your application development, and when used properly can weave together a very solid safety net for development.
If you've written some Ruby before, you may already be familiar with test/unit, the built-in unit testing framework. This is a general purpose tool that is quite similar to some of the other xUnit derivatives found in many other languages including Java, C++, Perl, Haskell and probably countless others.
In the context of Rails, unit tests are meant primarily to cover the domain logic in your models, which include things like validations, calculations, search methods, and any other interesting functionality that your models implement. Since Rails sets up a database just for tests, you can test complex interactions in the same type of environment your application actually will run in, without worrying about damaging live data.
Some controllers are bound to be bland, but most of the time, you're going to have some sort of interesting logic that you'd want to test. Functional tests provide a way to verify that the actions for a single controller are working as expected, and allow you to do things such as post data to a specific action and verify the correct response is returned.
If your app is written cleanly enough and there is little or no logic in your views, functional tests can provide pretty solid coverage of your controllers and their interactions with any associated models.
Between units and functionals, the components of your application will be pretty well tested in isolation. Still, in practice any given session with a Rails application will span across several models and controllers. Integration tests provide a way to test those kinds of interactions. Essentially, an integration test is written at the story level, allowing you to verify the correct behavior of your application for a given use case.
For example, an integration test might cover something like "Joe logs in and creates a new Entry, and then Sue checks to see if the Entry shows up in the listing". Keep an eye out for something similar when we begin layering integration testing into the Tasty app in a bit.
Solidifying your models with unit tests
Rails automatically lays out the boiler plate for your tests when you generate models.
For example, when you type
script generate model foo, the following unit test related files are generated:
require File.dirname(__FILE__) + '/../test_helper' class FooTest < Test::Unit::TestCase fixtures :foos # Replace this with your real tests. def test_truth assert true end end
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html one: id: 1 two: id: 2
Though the boilerplate is neat because you get to say "Cool, I didn't have to write that code", it's not really worth anything without some good tests.