Welcome to Giant Robots Smashing Into Other Giant Robots — a weblog about development, business, design and technology — written by thoughtbot.
Battle Royale - Testing
We’ve been havin’ ourselves an old fashioned, knuckle to the teeth fight-out here at the TB saloon, and that chair flyin through that there window was some of us takin’ it outside, so to speak. Cowboy Ted said he eats his steak with the fork in the left hand, and the rest of the TB corral eats ours with the right. And that there just about does it for this analogy.
...Partner.
What’s up, TB? You were my hope and the source of my sustained belief that I, too, could one day work in a place as cool as yours! Please, God, don’t break up the band!!—you, our loyal fans
Now don’t you worry your cute little head. We’re still sitting at the same dinner table every week, and we still have a blast during our morning pillow fights. Siblings just sometimes have arguments, and that’s how they grow and become strong boys and girls. It’s natural, see?
Here’s the kumbaya, group-hug side of things: I think (tearin’ up here) that it’s just super cool that I work with people who are so dedicated and passionate about the quality of their code, that they will argue with each other for hours on end about details as small as this. And, taking a step back, this detail is small.
But I, too, am one of the passionate geeks, so here’s my drunken swings to add to the fray:
Overmocking makes tests brittle
Let’s look at the controller action from the last post:
1 2 3 |
def search @users = User.find :all, :conditions => ['name like ?', "%#{params[:q]}%"] end |
And the proposed test:
1 2 3 4 5 6 7 8 9 10 |
def test_should_find_all_users_whos_name_matches_the_given_search_phrase_on_GET_to_search users = [] User.expects(:find).with(:all, :conditions => ['name like ?', '%a%']).returns users get :search, :q => 'a' assert_equal users, assigns(:users) assert_response :success assert_template 'search' end |
So what happens if we decide that the call to find should really be a search_by_name method on the User model? We refactor, the controller action becomes:
1 2 3 |
def search @users = User.search_by_name(params[:q]) end |
And our tests break. Note that the functionality is the same, and normal tests would have passed to prove it. This means that every time we refactor, we have to change this and every other functional test on search, easily five times the work. This would also happen if we added a call to User.count (for whatever reason). That is seriously unagile.
By now you should have noticed that the test name above is misleading…
Overmocking gives a false sense of security
The test above is named test_should_find_all_users_whos_name_matches_the_given_search_phrase_on_GET_to_search, but that’s not actually what it’s testing. A more accurate name would be test_should_call_User_find_with_all_and_conditions_set_to_name_like_params_q_on_GET_to_search. This isn’t just about accurate naming, but about your expectations on what a test means. If I refactor the search method again:
1 2 3 |
def search @users = User.find :all, :conditions => ['name == ?', "%#{params[:q]}%"] end |
The test would fail, and the fixed test would be:
1 2 3 4 5 |
def test_should_call_User_find_with_all_and_conditions_set_to_name_equal_params_q_on_GET_to_search ... User.expects(:find).with(:all, :conditions => ['name == ?', '%a%']).returns users ... end |
Now the tests pass, the test does what it espouses to do, and your application is broken (did you catch the bug? ‘==’ isn’t valid SQL). This is they key: Effective tests only care about the user-visible behavior of an action, regardless of how it goes about doing it. This is the essence of modern computer science, and what all that talk in college about “black boxes”, “objects”, “APIs” and “contracts” was trying to show you. As a coworker put it: our tests shouldn’t care whether our method calls find, or produces a fairy princess to do the work for us. Our tests should only care that the work gets done.
Interaction testing is TDD, but backwards
The above tests are examples of what the kids in the know call Interaction Testing.
The problem with overdoing this method of testing is that it encourages you to make assumptions about how your method will work before you write it. The whole point of TDD is to force you to take a step back, pretend you’re a client of your own API, and write only the code necessary for that client. Writing a test that asserts that find is called before you write the method encourages you to use find instead of find_by_id, which is not something the client cares about. This is not the purpose of TDD.
So you’re hatin’ on Interaction Testing?
Absolutely not. Interaction testing is necessary if you don’t have full control of or access to a module your code works with. If you work in a large shop, it’s completely legit to test the interactions between your team’s module and the modules of the team down the hall. It’s the only practical way to keep coding without waiting on them.
Likewise, if you’re application works against Amazon’s API, or does some intense filesystem access, you had better mock that stuff out for your tests. Nobody’s arguing against these points.
So are there problems with rails testing?
Rails testing is worlds easier than in any other framework I’ve dealt with, but of course there are problems.
Speed
rake can be dog slow on a large application. Using in memory sqlite3 databases roughly halves your test times. Also, the rspec folk have a really good idea with their spec server, which loads the rails environment and serves access to tests through DRB. These are much better solutions than sacrificing developer time through brittle and ineffective testing.
Sugar
Rails tests tend to be very wet. We’ve been working on some testing helpers that are addressing that. I’m not quite ready to release it (soon, soon), but here’s a quick peek:
1 2 3 4 5 6 7 8 9 10 |
class UserTest < Test::Unit::TestCase should_require_attributes :username, :hashed_password, :name should_require_unique_attributes :username should_not_allow_values_for :username, 'b.ad', 'b/ad', 'b ad', 'b#ad', 'b&d', 'b,ad' should_ensure_length_in_range :username, 3..10 should_have_many :ratings should_have_many :rated_books, :through => :ratings should_belong_to :company should_have_have_one :friend end |
We’re hoping to do the same thing to controller tests fairly soon, and we promise to let you (our loyal fans) know when we do.
About this entry
You're reading an entry on GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS, the company weblog of thoughtbot, inc.
- Author:
- Tammer Saleh
- Published:
- March 17th 02:50 PM
- Updated:
- March 17th 02:53 PM
- Sections:
- Development Technology
thoughtbot is hiring
We are hiring web developers and web designers in both Boston and New York, NY.
What are we up to?
We built Shoulda, an eclectic set of additions to Test::Unit; Paperclip to manage uploaded files without hassle; Jester, a REST/ActiveResource client library written in Javascript, and Squirrel, an enhancement for ActiveRecord's find syntax; — amongst some other projects.

Chad (President) and Jon (CTO) co-authored a technical book titled Pro Active Record: Databases with Ruby and Rails, which explores the ins and outs of the ActiveRecord ruby library. You can buy it today at Amazon.com.
About thoughtbot, inc.
We are a small web application development consulting business, with offices in Boston, MA and New York, NY. If you're looking to find a team for your next web development project or your new web application — get in touch.
5 comments
Jump to comment form