Chores: A test driven website: part 3 (the revenge)

Update This is part of the Chores series of posts

It’s friday; the floors are piling up with dirty clothes, old homework and beds are unmade. We need Chores, a Ruby on Rails website written with test driven principles in mind and created by the author of this article.

This post is a continuation of parts 1 and 2 in which I am documenting the steps with which I am creating a rails site and writing matching tests with Cucumber and Test::Unit. When we left off last time I had found that I need a Chore model to continue. At a minimum, we are going to need a description for the chore. So let’s get to it:

1
2
3
4
5
6
7
8
9
$ ruby script/generate model Chore description:string
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/chore.rb
      create  test/unit/chore_test.rb
      create  test/fixtures/chores.yml
      create  db/migrate
      create  db/migrate/20090109221900_create_chores.rb

Now I have a decision to make, I know that I want to set the :limit and :null attributes in the migration file. I can make the changes now and then write the tests on the matching validation or I can run the migration now and then write the tests on the validation and modify the columns in the db if necessary. Because I am lazy and because I want to be sure the db constraints are in place I’m going to opt for the first option and then run my migration.

1
2
3
4
5
6
7
8
9
10
11
12
13
class CreateChores < ActiveRecord::Migration
  def self.up
    create_table :chores do |t|
      t.string :description, :null => false, :limit => 255
 
      t.timestamps
    end
  end
 
  def self.down
    drop_table :chores
  end
end

and

$ rake db:migrate
==  CreateChores: migrating ===================================================
-- create_table(:chores)
   -> 0.0160s
==  CreateChores: migrated (0.0160s) ==========================================

Assert yourself!

At the core of unit testing is the assertion. An assertion is a statement that something will always be true. In fact, if an assertion evaluates to false, the test will fail. Test::Unit provides a wide variety of assertions ranging from a vanilla assert to the one that I find myself using most frequently, assert_equal. There are also many more available for use in specific situations.

Test!

Now let’s write some tests to ensure our model validates the data. First I’m going to make sure that the description is required and second I’m going to make sure that the description can be a maximum of 255 characters. So lets open up the file ./test/unit/chore_test.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require 'test_helper'
 
class ChoreTest < ActiveSupport::TestCase
  test "description is required" do
    chore = Chore.new
    assert !chore.valid?
    chore.description = "Clean your room"
    assert chore.valid?
  end
 
  test "a description of 255 characters is valid" do
    chore = Chore.new :description => "This is a really long string that is the description of a chore and it is used to validate that the chore model will accept a string of 255 characters.  This string is longer than the longest string ever which also included some numbers 012345678901234567"
    assert chore.valid?
  end
 
  test "a description of 256 characters is invalid" do
    chore = Chore.new :description => "This is a really long string that is the description of a chore and it is used to validate that the chore model will accept a string of 255 characters.  This string is longer than the longest string ever which also included some numbers 0123456789012345678"
    assert !chore.valid?
  end
end

As you can see I have written 3 simple tests. The first verifies that the description cannot be null, the second proves that a 255 character string is valid, and the third shows that a 256 character string is not valid. Lets run these tests and see what we get.

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
$ rake test:units
(in c:/rails/chores)
c:/Ruby/bin/ruby -Ilib;test "c:/Ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/chore_test.rb"
Loaded suite c:/Ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
 
Started
.FF
Finished in 0.75 seconds.
 
  1) Failure:
test_a_description_of_256_characters_is_invalid(ChoreTest)
    [./test/unit/chore_test.rb:18:in `test_a_description_of_256_characters_is_invalid'
     c:/Ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `__send__'
     c:/Ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `run']:
<false> is not true.
 
  2) Failure:
test_description_is_required(ChoreTest)
    [./test/unit/chore_test.rb:6:in `test_description_is_required'
     c:/Ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `__send__'
     c:/Ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `run']:
<false> is not true.
 
3 tests, 3 assertions, 2 failures, 0 errors
rake aborted!
Command failed with status (1): [c:/Ruby/bin/ruby -Ilib;test "c:/Ruby/lib/r...]
 
(See full trace by running task with --trace)

What does this mean? The summary tells us what happened in a nutshell. At the bottom it says “3 tests”, that means that it ran 3 tests. That is good because we wrote 3 tests. Next, it says “3 assertions”, that means that in running the 3 tests, the testing framework encountered 3 assert statements. But we wrote 4 assertions! Why are there only 3 listed? This is because, within a test, when the framework encounters a failed assertion, it stops running that test. So we must have a test with an assertion that was not executed because another assertion failed before it was executed.

Moving on through the summary, it says “2 failures”. This means that two of the things that we asserted to be true, were in fact false. If we look in the details up above we see that the test to see if 255 characters was valid has passed, this makes sense since we don’t have any validation to limit the length at this point. Finally it says “0 errors”, in this context you can think of errors as exceptions. Basically it means that the code that ran, ran without throwing an exception.

Now let’s make these tests pass. The first failing test shows that the length of the description is not being limited to 255 characters. So let’s do that.

1
2
3
class Chore < ActiveRecord::Base
  validates_length_of :description, :maximum => 255
end

When we run our test we get:

$ rake test:units
(in c:/rails/chores)
c:/Ruby/bin/ruby -Ilib;test "c:/Ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/chore_test.rb"
Loaded suite c:/Ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
 
Started
...
Finished in 1.516 seconds.
 
3 tests, 4 assertions, 0 failures, 0 errors

Huh?

But we never checked to see if the description was null! Why are all of our tests passing? If we look at the api docs for validates_length_of we will see that it does not allow nil by default, and that to allow nil you have to pass the symbol :allow_nil. Well this is an interesting development, let’s make sure that the validation message is going to make sense by adding a test.

  test "validation message for description" do
    chore = Chore.new
    chore.valid?
    assert_equal "Description cannot be blank and must be less than 255 characters long.", chore.errors.on(:description)
  end
$ rake test:units
(in c:/rails/chores)
c:/Ruby/bin/ruby -Ilib;test "c:/Ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/chore_test.rb"
Loaded suite c:/Ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
 
Started
...F
Finished in 0.781 seconds.
 
  1) Failure:
test_validation_message_for_description(ChoreTest)
    [./test/unit/chore_test.rb:24:in `test_validation_message_for_description'
     c:/Ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `__send__'
     c:/Ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `run']:
<"Description cannot be blank and must be less than 255 characters long."> expected but was
<"is too long (maximum is 255 characters)">.
 
4 tests, 5 assertions, 1 failures, 0 errors
rake aborted!
Command failed with status (1): [c:/Ruby/bin/ruby -Ilib;test "c:/Ruby/lib/r...]
 
(See full trace by running task with --trace)

Looks like it’s a good thing that we checked the error message. The current message, “is too long (maximum is 255 characters)”, would not have made any sense if the user had left the description nil. Let’s update our model with the new message

1
2
3
class Chore < ActiveRecord::Base
  validates_length_of :description, :maximum => 255, :message => "Description cannot be blank and must be less than 255 characters long."
end
$ rake test:units
(in c:/rails/chores)
c:/Ruby/bin/ruby -Ilib;test "c:/Ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/chore_test.rb"
Loaded suite c:/Ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
 
Started
....
Finished in 0.797 seconds.
 
4 tests, 5 assertions, 0 failures, 0 errors

Our unit tests are passing. Let’s commit and then take a step back to see where we are.

$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#       modified:   config/routes.rb
#       modified:   db/development.sqlite3
#       modified:   db/test.sqlite3
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       app/controllers/chores_controller.rb
#       app/controllers/home_controller.rb
#       app/helpers/chores_helper.rb
#       app/helpers/home_helper.rb
#       app/models/
#       app/views/
#       db/migrate/
#       db/schema.rb
#       test/fixtures/
#       test/functional/
#       test/unit/
no changes added to commit (use "git add" and/or "git commit -a")
 
$ git add .
$ git commit -am "Added the Chore model"
$ git push origin

Well the easy way to see where we are is to re-run our feature with cucumber

$ cucumber features
Story:  Define chores  # features/define_chores.feature
As a parent
I want to define chores
So that I can assign them to my children
  Scenario: Creating a chore           # features/define_chores.feature:6
    Given I am on the homepage      # features/step_definitions/chores_steps.rb:1
    When I follow "Add Chore"         # features/step_definitions/webrat_steps.rb:8
      Called id for nil, which would mistakenly be 4 -- if you really wanted the
 id of nil, use object_id (ActionView::TemplateError)
      On line #1 of app/views/chores/new.html.erb
 
      1: <% form_for(@chore) do |f| %>
      2:   <%= f.error_messages %>
      3:
      4:   <p>

So chore is nil in the view, but we have a model for it now. The next step is to look back at what we’ve written and see if there are any opportunities for refactoring. Then we will make sure that our controller is setting the @chore instance variable for us. If you are interested in reading more about testing in Rails, here are some links for further reading:

Tags: , ,

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">