A well established and accepted design principle in the Rails community is “skinny controller, fat model”. This involves placing an application’s domain logic in models and keeping the request handling controllers simple and sparse (i.e. “skinny”). The result is a more modularized, reusable, rich domain model. The following is an example of using TDD to achieve this style of design.
Let’s imagine an app that uses a 3rd party service to send SMS messages to its users. To no longer receive SMS messages from the site, a user would text ‘STOP’ to this 3rd party service. Upon receiving a ‘STOP’ SMS message from a user, the service performs an HTTP POST containing the user’s mobile phone number (in XML) to a callback url on our site. From here we can update that user’s SMS preferences.
Let’s start with the controller spec.
describe SmsNotificationsController do describe '#unsubscribe' do before do @sms_user = Factory :sms_user xml = <<-EOS <?xml version="1.0"?> <phone_number> #{@sms_user.mobile_phone_number} </phone_number> EOS request.env['RAW_POST_DATA'] = xml post :unsubscribe end it 'unsubscribes the user from SMS notifications' do @sms_user.reload @sms_user.sms_notifications.should_not be end it 'is successful' do response.should be_success response.body.should be_blank end end end
Running the spec tells us our next step…
Let’s implement just enough to figure out what to do next…
config/routes.rb
... resources :sms_notifications do collection do post :unsubscribe end end ...
And then run the spec again…
Ok great. Another small step…
class SmsNotificationsController < ApplicationController def unsubscribe end end [/sourcecode] And back to the spec...
Ok now its time to party. In order to get our specs passing our first response might be to implement something similar to the following:
class SmsNotificationsController < ApplicationController def unsubscribe document = Nokogiri::XML.parse request.raw_post phone_number = document.xpath('/phone_number').first.text user = User.find_by_phone_number phone_number user.unsubscribe_sms_notifications! head 200 end end [/sourcecode] This code will get our spec passing but its in the wrong place. This code is domain logic, it belongs in the model.
Let’s rewrite the action but this time take a much lazier approach in our implementation.
class SmsNotificationsController < ApplicationController
def unsubscribe
user = User.find_by_sms_unsubscribe_xml request.raw_post
user.unsubscribe_sms_notifications!
head 200
end
end
[/sourcecode]
Even though User
doesn’t understand .find_by_sms_unsubscribe_xml
or its instances #unsubscribe_sms_notifications!
, I wrote this wishing these methods existed. By being lazy, I’ve defined a nice high-level interface to my domain. This is very similar to the way test first development helps you design an object’s interface. Our domain logic is now where it belongs and the models have become more interesting.
Running our specs tells us what to do next…
At this point, we go down another level and start the tdd process over again for User.find_by_sms_unsubscribe_xml
. Once that’s passing, we come back up to the controller and run our specs again…
We then repeat the same process for User#unsubscribe_sms_notifications!
before coming back up to the controller spec. The end result is a rich domain model with lazy, skinny controllers.