SignOut: Part 3
In addition to the REST refactoring for SignOut, we thought we’d cover a few more changes we’d suggest.
The biggest thing which jumped out at me was that SignOut has no tests. If you’ve never written a test in rails all the different tools and frameworks available can make it seem quit daunting. It’s often hard to tell if you should you use rspec, heckle, autotest, rcov and where to start. So in this article we’ll take the set_away action from EmployeeController and write a few tests for it, using just the stuff you get for free with rails.
The action as it stands has some reasonably involved code for handling decoding time_back from the request parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def set_away @employee = Employee.find(params[:id]) if params[:date][:meridian] == "PM" if params[:date][:hour].to_i == 12 hour = params[:date][:hour] else hour = (params[:date][:hour].to_i + 12).to_s end else if params[:date][:hour].to_i == 12 hour = "0" else hour = params[:date][:hour] end end time_back = Time.mktime(params[:date][:year], params[:date][:month], params[:date][:day], hour, params[:date][:minute] ) @employee.update_attributes(:reason => params[:employee][:reason], :time_out => Time.now, :time_in => time_back) end |
So there are four distinct cases that we need to ensure are tested:
- A Meridian of “PM” and an hour of 12
- A Meridian of “PM” and some other hour
- A Meridian of “AM” and an hour of 12
- A Meridian of “AM” and some other hour
First up we need some fixtures data for an employee, lets pretend I work there and create a test fixture for koz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
koz: id: 1 firstname: Michael lastname: Koziarski company_email: koz@example.com personal_email: michael@koziarski.com extension: 665 cellphone: +64 21 555 1337 homephone: +64 4 555 1337 time_out: time_in: reason: created_on: <%= 5.days.ago.to_date.to_s(:db) %> modified_on: <%= Date.today.to_s(:db) %> initials: MAK |
Now the tests in EmployeeControllerTest:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class EmployeeControllerTest < Test::Unit::TestCase def setup @controller = EmployeeController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end # Ensure that the employees table gets reloaded as needed fixtures :employees # we can DRY up the test data by providing a default date #and then just overriding what we need def default_date {:meridian=>"PM", :year=>now.year, :month=>now.month, :day=>now.day, :minute=>"0", :hour=>"12"} end # Test the first case, PM and 12 def test_set_away_for_pm_and_12_oclock # the default data matches this test case, post :set_away :id=>employees(:koz).id, :date=>default_date # ensure no errors ocurred assert_response :success # reload our fixture to match the database changes koz = employees(:koz).reload # make sure the time_back attribute has been set assert time_back = koz.time_back assert_equal 0, time_back.minute # 12PM is 12 midday, so the hour should be 12 assert_equal 12, time_back.hour # We provided a reason here, make sure the controller has set it assert_equal "Because", koz.reason end end |
We could continue to write tests like that, but instead we can get a little clever and let ruby write the tests for us:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def default_date {:meridian=>"PM", :year=>now.year, :month=>now.month, :day=>now.day, :minute=>"0", :hour=>"12"} end TEST_SCENARIOS= {0=>{:hour=>"12", :meridian=>"AM"}, 5=>{:hour=>"5", :meridian=>"AM"}, 18=>{:hour=>"6", :meridian=>"PM"}, 12=>{:hour=>"12", :meridian=>"PM"}} TEST_SCENARIOS.each do |hour, hash_overide| # Make sure we give our tests useful names, otherwise it's hard to tell what failed name = "test_with_hour_#{hash_overide[:hour]}_and_meridian_#{hash_overide[:meridian]}" define_method(name) do post :set_away :id=>employees(:koz).id, :date=>default_date.merge(hash_override) # ensure no errors ocurred assert_response :success # reload our fixture to match the database changes koz = employees(:koz).reload assert time_back = koz.time_back assert_equal 0, time_back.minute # Make sure the hour matches our expected hour assert_equal hour, time_back.hour end end end |
Now you can refactor the set_away action without having to worry about introducing new date related bugs.


Great write up as usual but where does ‘now’ come from in EmployeeControllerTest? Do you define that as a test helper method? Not a bad idea if so..
Did I miss some magic setting of “Because”? Why would this work? # We provided a reason here, make sure the controller has set it assert_equal “Because”, koz.reason
Did I miss some magic setting of “Because”? Why would this work?
# We provided a reason here, make sure the controller has set it
assert_equal “Because”, koz.reason
Hey guys,
now was defined elsewhere to be Time.now, sorry I missed that.
‘Because’ again, was missed from an earlier revision.
Thanks!
Hi Koz, great article. I’m baffled by these lines in the test fixture though:
created_on: <%= 5.days.ago.to_date.to_s(:db) %> modified_on: <%= Date.today.to_s(:db) %>
What is :db and how did 5 get a days method?
James, see:
http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Numeric/Time.html
and
Hrm, can’t find the docs for Date#to_s(:db) but it’s very clever. ActiveRecord’s current connection is used to determine the correct sql date format iirc.
A common case (in my app) of having to deal with url-munging of a legitimate variety has to do with URLs sent by email, then forwarded.
Specifically, when a user is in an appropriate distribution list, they get emails when the objects in question are changed. If they then forward that email to someone else using the app, who does not have permissions to view the object, they might click on the link and be brought into no-man’s land.
And whoops… I commented on the wrong post… I meant to comment to Part I of this post.