How I refine a test spec while writing getting it to green

January 09, 2009

In most of the examples I've read on TDD they show the Red-Green-Refactor cycle as


  1. Write a failing test

  2. Write just enough code to make the test pass

  3. Refactor

  4. Repeat



I absolutely do believe in this cycle and live it every day however I think there's a small detail that differs from what I do. When I move from step 1 to step 2, I keep my test window open and will switch back and forth between the test and code windows and refining the test while writing the code that makes the test pass. Its only as I write the code that I realize what calls I need to mock out in the test which also adds additional expectations to my test. I'm sure this is how most people work but I've never seen it written up so I'm going to try going through an example here.

Let's say I have a social networking application with a page showing friend requests where you can accept or reject each request. Something like this


Friend Requests:

Accept request from Pat?
Accept request from Gourav?



Now I'm ready to implement an action in the Friend Controller that receives a posted form with a list of friend requests to accept and reject. I might first go into my friends_controller_spec.rb and write something like


describe FriendController do
it 'should approve one friend requests' do
post :update_requests, :approved_requests => ['123']
response.should be_redirect
response.should redirect_to(:back)
flash[:notice].should == "Friend requests approved."
end
end



Now I have a failing test so I start implementing the code


class FriendController < ApplicationController
def update_requests
FriendRequest.approve(params[:approve_requests])
flash[:notice] = "User access changes saved."
redirect_to :back
end
end



I'm done but my test is still failing...why? I haven't added the approve method to my FriendRequest model yet. I could go ahead and implement that method to get this test to pass but I don't want to do that because then this test would be testing more than just one unit. This is a unit test for the update_requests method of my FriendController and should be isolated from bugs in the rest of my application. Time to go back to the spec and add a mock to isolate this spec from the model and impose another expectation on my spec (that the method be called with the correct argument).


describe FriendController do
it 'should approve one friend requests' do
FriendRequest.expects(:approve).with(['123'])
request.env['HTTP_REFERER'] = "http://some.site.com"

post :update_requests, :approved_requests => ['123']
response.should be_redirect
response.should redirect_to(:back)
flash[:notice].should == "Friend requests approved."
end
end



Now my spec passes and I can do my refactoring then go back to the beginning and write my next expectation.

The point of this example is that when I first wrote the spec I hadn't yet thought about how I would implement update_requests so did not yet know that I would need to mock FriendRequest.approve. I do this all the time and typically find myself go back and forth between the code and spec many times with the spec telling me what code I need to write and the code telling me how to refine the spec.

One last point (and a teaser for a future post). You have probably noticed that although my spec passes if I try to run the application it will not work because I still haven't written the FriendRequest.approve method. In a future post I hope to discuss how to interleave integration testing into this process to ensure that the classes we're writing in isolation also integrate so that the application satisfies its business function.