SignOut: Part 3

Posted by koz Monday, February 26, 2007 19:57:00 GMT

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:

  1. A Meridian of “PM” and an hour of 12
  2. A Meridian of “PM” and some other hour
  3. A Meridian of “AM” and an hour of 12
  4. 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.

Comments

Leave a response

  1. noodlMarch 01, 2007 @ 05:00 PM

    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..

  2. JohnBMarch 08, 2007 @ 06:16 AM

    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

  3. JohnBMarch 08, 2007 @ 06:17 AM

    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

  4. KozMarch 09, 2007 @ 12:32 AM

    Hey guys,

    now was defined elsewhere to be Time.now, sorry I missed that.

    ‘Because’ again, was missed from an earlier revision.

    Thanks!

  5. JamesMarch 13, 2007 @ 03:24 PM

    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?

  6. noodlMarch 14, 2007 @ 03:19 PM

    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.

  7. YehudaMay 21, 2007 @ 03:13 PM

    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.

  8. YehudaMay 21, 2007 @ 03:14 PM

    And whoops… I commented on the wrong post… I meant to comment to Part I of this post.