Posts Tagged ‘Chores’

Chores: a test driven website – Part 9 (the lost episode)

Sunday, May 10th, 2009

This is part of the Chores series of posts.

Well it’s been a while since National Testing month and there has been a lot going on both in my world and in the Ruby Community. I’ve been doing a good deal of reading, deployed a Monorail website and been generally busy. Unfortunately, I haven’t focused on building Chores, my test driven chore delivering website.

Today I’ve changed all that by dusting off chores and updating my gems. The first thing that I noticed was that I needed to add some configuration for Webrat for it to function within Cucumber. I needed to add the following to the env.rb file:

Webrat.configure do |config|
  config.mode = :rails
end

Next I see that I’m getting an error on the new action for Children because the @child variable is nil. Let’s write a functional test to fix that.

Here is my first pass at the functional test and controller

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
require 'test_helper'
class ChildrenCanCrud < ActionController::TestCase
  tests ChildrenController
 
  specify "test that it should respond to index" do
    get :index
    assert_response :success
    assert_not_nil assigns(:children)
  end
 
  specify "that it should pass child on new" do
    get :new
    assert_response :success
    assert_not_nil assigns(:child)
  end
 
  specify "that it should create child" do
    assert_difference 'Child.count' do
      post :create, :child => { :parent => Identity.make, :identity => Identity.make }
    end
 
    assert_redirected_to children_url
  end
 
end

./app/models/child.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Child < ActiveRecord::Base
  attr_accessor :open_identifier
  before_create :set_identity
 
  belongs_to :identity,             :foreign_key => :child_id,
                                    :class_name => 'Identity'
 
  belongs_to :parent,               :foreign_key => :parent_id,
                                    :class_name => 'Identity'
 
  validates_presence_of :open_identifier,  :message => 'can not be blank.',
                                           :on => :create
  validates_presence_of :identity,         :message => 'can not be blank.',
                                           :on => :update
  validates_presence_of :parent,    :message => 'can not be blank.'
 
  def set_identity
    self.identity = Identity.find_or_make_if_valid @open_identifier
  end
end

./app/models/identity.rb

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class Identity < ActiveRecord::Base
  include OpenIdAuthentication
 
  validates_presence_of :identifier,        :message => "can not be blank."
 
  validates_uniqueness_of :identifier,      :message => "is already in use, please use another."
 
  validates_length_of :identifier,          :maximum => 100,
                                            :allow_nil => true,
                                            :message => "is too long."
 
  validate_on_create :valid_id
 
  has_many :parents,             :foreign_key => :parent_id,
                                 :class_name => 'Child'
  has_many :children,            :foreign_key => :child_id,
                                 :class_name => 'Child'
 
  def open_id
    return nil if self.identifier.nil?
    self.identifier[7..-2]
  end
 
  def identifier= identifier
    raise "Cannot update identifier." unless new_record?
    set_identifier identifier
  end
 
  def open_id= open_id
    raise "Cannot update open_id." unless new_record?
    set_identifier open_id
  end
 
  def valid_id
    errors.add(:identifier, "is not a valid Open ID.") if @invalid_id
  end
 
  def self.find_by_open_id open_id
    id = OpenIdAuthentication::normalize_identifier(open_id)
    find_by_identifier open_id
  end
 
  def self.find_or_make_if_valid open_id
    begin
      id = self.find_by_open_id Identity.normalize_identifier(open_id)
    rescue OpenIdAuthentication::InvalidOpenId
      return nil
    end
 
    unless id
      id = Identity.new :identifier => open_id
      id.save
    end
    id
  end
 
    def Identity.normalize_identifier id
    begin
      identifier = OpenIdAuthentication::normalize_identifier(id)
    rescue OpenIdAuthentication::InvalidOpenId
      identifier = nil
    end
    identifier
  end
 
  protected
  def normalize_id id
    identifier = Identity.normalize_identifier id
    unless identifier
      @invalid_id = true
      identifier = id
    end
    identifier
  end
 
  def set_identifier id
    write_attribute(:identifier, normalize_id(id))
  end
end

and

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ChildrenController < ApplicationController
  def index
    @children = Child.find(:all)
  end
 
  def new
    @child = Child.new
  end
 
  def create
    @child = Child.new(params[:child])
    if @child.save
      flash[:message] = "Child added."
      redirect_to children_url
    else
      flash[:error] = "Error saving Child."
      render :action =>  "new"
    end
  end
end

update to ./test/unit/child_test.rb

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
34
35
36
37
38
require 'test_helper'
 
class ChildShouldBeValidated < ActiveSupport::TestCase
 
  specify "that a parent is required" do
    child = Child.new
    child.open_identifier = valid_open_id
    invalid child
    child.parent = Identity.make
    valid child
  end
 
  specify "that a child is required" do
    child = Child.new :parent => Identity.make
    invalid child
    child.open_identifier = valid_open_id
    valid child
  end
 
  specify "message for a null identity" do
    child = Child.new :parent => Identity.make
    validation_message_for child, :open_identifier, "can not be blank."
  end
 
  specify "message for a null parent" do
    child = Child.new :identity => Identity.make
    validation_message_for child, :parent, "can not be blank."
  end
end
 
class ChildShouldCreateIdentity < ActiveSupport::TestCase
 
  specify "that open_identifier is used to pass the OpenId" do
    child = Child.new :parent => Identity.make
    child.open_identifier = valid_open_id
    assert child.save
  end
end

./test/functional/children_controller_test.rb

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
require 'test_helper'
 
class ChildrenCanCrud < ActionController::TestCase
  tests ChildrenController
 
  def setup
    @identity = Identity.make
  end
 
  specify "test that it should respond to index" do
    get :index, nil, build_session_hash_for(@identity)
    assert_response :success
    assert_not_nil assigns(:children)
  end
 
  specify "that it should pass child on new" do
    get :new, nil, build_session_hash_for(@identity)
    assert_response :success
    assert_not_nil assigns(:child)
  end
 
  specify "that it should create child" do
    assert_difference 'Child.count' do
      post :create, {:child => { :open_identifier => "cheese.example.com" }}, build_session_hash_for(@identity)
    end
 
    assert_redirected_to children_url
  end
 
end

./test/test_helper.rb

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
34
35
36
37
38
39
40
41
42
43
44
45
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require File.expand_path(File.dirname(__FILE__) + "/blueprints")
require 'test_help'
require 'machinist' #if you installed as gem rather than plugin
require 'faker'
 
class ActiveSupport::TestCase
  self.use_transactional_fixtures = true
  self.use_instantiated_fixtures  = false
 
  def invalid model
    assert !model.valid?
  end
 
  def valid model
    assert model.valid?
  end
 
  def validation_message_for model, column, message
    invalid model
    assert_equal message, model.errors.on(column)
  end
 
  def valid_identifier
    "http://test.example.com/"
  end
 
  def build_session_hash_for identity
    @session_hash = {'identity_id' => identity.id}
  end
 
  def valid_open_id
    "test.example.com"
  end
 
  def invalid_open_id
    "bad_id"
  end
 
end
 
def specify *args, &block
  test(*args, &block)
end

.. interestingly enough I find that my view that I wrote at some point expects a field called nickname

1
2
3
4
5
6
7
8
9
<% form_for(@child) do |f| %>
  <%= f.error_messages %>
  <%= f.label :nickname %>
  <%= f.text_field :nickname %>
  <%= f.label :open_identifier %>
  <%= f.text_field :open_identifier %>
  <%= f.submit "Add" %>
 
<% end %>

So I quickly create and run a migration to add the field.

~/working/chores (chores9)$ ruby script/generate migration add_nickname_to_child nickname:string
      exists  db/migrate
      create  db/migrate/20090402191006_add_nickname_to_child.rb

which can should be migrated with.

rake db:migrate

Here is the design I settled on.

As you can see, I’ve pushed it out to github. I’ve tagged it with “post_9″. I am hoping this will help those that want to follow along and reduce the amount of code that I need to post while writing this up. Maybe I’ll write up a post on git and github.

Update: I apologize this is all getting muddled in my brain as well. Between a change in rails versions and the passage of time. I hope to get this all straightened out in the next post.

Chores: a test driven website – Part 8 (this time it's personal)

Saturday, January 31st, 2009

This is part of the Chores series of posts.

I’m now attacking the Idenitity model. My idea for this is a model that contains a single field. That field is a valid, properly formatted, immutable OpenID. Because I’m going to need to validate OpenIDs I’m going to install the Open ID Plugin as follows.

$ ruby script/plugin install git://github.com/rails/open_id_authentication.git
Initialized empty Git repository in c:/rails/chores/vendor/plugins/open_id_authentication/.git/
remote: Counting objects: 35, done.←[K
remote: Compressing objects: 100% (31/31), done.←[K
remote: Total 35 (delta 4), reused 21 (delta 2)[K
Unpacking objects: 100% (35/35), done.
From git://github.com/rails/open_id_authentication
 * branch            HEAD       -> FETCH_HEAD
$ rake open_id_authentication:db:create
(in c:/rails/chores)
      exists  db/migrate
      create  db/migrate/20090125184610_add_open_id_authentication_tables.rb
$ rake db:migrate
(in c:/rails/chores)
==  AddOpenIdAuthenticationTables: migrating ==================================
-- create_table(:open_id_authentication_associations, {:force=>true})
   -> 0.0310s
-- create_table(:open_id_authentication_nonces, {:force=>true})
   -> 0.0160s
==  AddOpenIdAuthenticationTables: migrated (0.0470s) =========================

Here is my test for the Identity model

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
require 'test_helper'
 
class IdentityShouldBeValidatedTest < ActiveSupport::TestCase
  specify "that an open ID is required on create" do
    identity = Identity.new
    invalid identity
    identity.open_id = "test.example.com"
    valid identity
  end
 
  specify "message for an overly long identifier" do
    identity = Identity.new :identifier => hundred_and_one_character_identifier
    validation_message_for identity, :identifier, "is too long."
  end
 
  specify "message for a null identifier" do
    identity = Identity.new
    validation_message_for identity, :identifier, "can not be blank."
  end
 
  specify "message for a poorly formatted" do
    identity = Identity.new :identifier => invalid_open_id
    validation_message_for identity, :identifier, "is not a valid Open ID."
  end
 
  specify "message for a duplicate identifier" do
    Identity.make :identifier => valid_identifier
    identity = Identity.new :identifier => valid_identifier
    validation_message_for identity, :identifier, "is already in use, please use another."
  end
 
  specify "that 100 character identifier is valid" do
    identity = Identity.new :identifier => hundred_character_identifier
    valid identity
  end
 
  specify "that identifier is unique" do
    Identity.make :identifier => valid_identifier
    identity = Identity.new :identifier =>  valid_identifier
    invalid identity
  end
 
  specify "that open_id must have the correct format" do
    identity = Identity.new
    identity.open_id = invalid_open_id
    invalid identity
  end
 
  specify "that open_id uniqueness extends to identifier when validated" do
    Identity.make  :identifier => valid_identifier
    identity = Identity.new :open_id => "test.example.com"
    invalid identity
  end
 
  protected
    def hundred_character_identifier
      "http://This.is.a.hundred.character.description.one.hundred.chatacters.is.not.too.long.but.it.of.com/"
    end
 
    def hundred_and_one_character_identifier
      "http://This.is.a.hundred1.character.description.one.hundred.chatacters.is.not.too.long.but.it.of.com/"
    end
 
    def invalid_open_id
      "bad_id"
    end
 
end
 
class IdentityOpenIdShouldSetIdentifierTest < ActiveSupport::TestCase
  specify "that open_id sets identifier" do
    identity = Identity.new
    assert_nil identity.identifier
    identity.open_id = "test.example.com"
    assert_not_nil identity.identifier
  end
 
  specify "that identifier sets open_id" do
    identity = Identity.new
    assert_nil identity.open_id
    identity.identifier = "http://test.example.com/"
    assert_not_nil identity.open_id
  end
 
  specify "that open_id is nicely formatted for display" do
    identity = Identity.new :identifier => "http://test.example.com/"
    assert_equal valid_open_id, identity.open_id
  end
end
 
class IdentityShouldBeImmutableTest < ActiveSupport::TestCase
  specify "that open_id is immutable" do
    identity = Identity.make
    assert_raise RuntimeError do
      identity.open_id = "monkey.example.com"
    end
  end
 
  specify "that identifier is immutable" do
    identity = Identity.make
    assert_raise RuntimeError do
      identity.identifier = "http://monkey.example.com/"
    end
  end
end
 
class IdentityShouldBeFoundByOpenID < ActiveSupport::TestCase
  specify "that open_id can be used to find a record" do
    identity = Identity.make :identifier => valid_identifier
    identity_from_db = Identity.find_by_open_id valid_open_id
    assert_equal identity.identifier, identity.identifier
  end
 
  specify "that display formatted open_id can be used to find a record" do
    identity = Identity.make :identifier => valid_identifier
    identity_from_db = Identity.find_by_open_id valid_identifier
    assert_equal identity.identifier, identity.identifier
  end
end

Here is the code that passes the test
./test/test_helper.rb

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
34
35
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require File.expand_path(File.dirname(__FILE__) + "/blueprints")
require 'test_help'
require 'faker'
 
class ActiveSupport::TestCase
  self.use_transactional_fixtures = true
  self.use_instantiated_fixtures  = false
 
  def invalid model
    assert !model.valid?
  end
 
  def valid model
    assert model.valid?
  end
 
  def validation_message_for model, column, message
    invalid model
    assert_equal message, model.errors.on(column)
  end
 
  def valid_identifier
    "http://test.example.com/"
  end
 
  def valid_open_id
    "test.example.com"
  end
end
 
def specify *args, &block
  test(*args, &block)
end

./app/models/identity.rb

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Identity < ActiveRecord::Base
  include OpenIdAuthentication
 
  validates_presence_of :identifier,        :message => "can not be blank."
 
  validates_uniqueness_of :identifier,      :message => "is already in use, please use another."
 
  validates_length_of :identifier,          :maximum => 100,
                                            :allow_nil => true,
                                            :message => "is too long."
 
  validate_on_create :valid_id
 
  def open_id
    return nil if self.identifier.nil?
    self.identifier[7..-2]
  end
 
  def identifier= identifier
    raise "Cannot update identifier." unless new_record?
    set_identifier identifier
  end
 
  def open_id= open_id
    raise "Cannot update open_id." unless new_record?
    set_identifier open_id
  end
 
  def valid_id
    errors.add(:identifier, "is not a valid Open ID.") if @invalid_id
  end
 
  def self.find_by_open_id open_id
    id = OpenIdAuthentication::normalize_identifier(open_id)
    find_by_identifier open_id
  end
 
  protected
  def normalize_id id
    begin
      identifier = normalize_identifier(id)
    rescue OpenIdAuthentication::InvalidOpenId
      identifier = id
      @invalid_id = true
    end
    identifier
  end
 
  def set_identifier id
    write_attribute(:identifier, normalize_id(id))
  end
end

./test/blueprints.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
require 'machinist'  # if you installed machinist from gem
 
Chore.blueprint do
  description "Put the dirty clothes in the hamper"
end
 
Identity.blueprint do
  identifier "http://#{Faker::Name.first_name}.#{Faker::Internet.domain_name}/"
end
 
Child.blueprint do
  child { Identity.make }
  parent { Identity.make }
end

This one took a little work to do and I’m going to revisit it in a little while to see if it needs any refactoring.

Chores: a test driven website – part 5 (the admission)

Friday, January 16th, 2009

This is part of the Chores series of posts

While committing Chores to git after the last refactoring, I’m thinking to myself. Who am I kidding? I’m going to write a post on functional testing? But I suck at functional testing! Well, we’ll see how it goes.

First, I’d like to install a gem called Machinist (this is also available as a plugin, if you prefer). Machinist is a drop in replacement for fixtures, allowing you to make (save) validated test data with a little config and a method call. You can read all about it in the readme, linked above. To install the gem simply:

$ gem install notahat-machinist

And be sure to configure your gem by adding the following to config/environments/test.rb

config.gem "notahat-machinist", :lib => 'machinist', :version => '0.3.1'

In fact, something I failed to cover is configuring your gems. Something you might want to take a minute to do.

Blueprints

Machinist uses “blueprints” to create records in the database for testing. These blueprints tell machinist how to create a valid record / object. These are stored in a blueprints file. So lets create a file in test/ called blueprints.rb that contains:

1
2
3
Chore.blueprint do
  description "Put the dirty clothes in the hamper"
end

and require the blueprint file in our test helper (test/test_helper.rb)

1
2
require 'machinist'
require File.expand_path(File.dirname(__FILE__) + "/blueprints")

On to the functional tests, or testing of the controllers. I used the rails scaffold functional tests as the starting point for my functional tests for chore. Here’s how the first run ended up. You might notice that there is not test for update, that’s because I’m not sure update is needed at the moment and since our cucmber feature (remember we are trying to get that working) didn’t say anything about updating, I’m just leaving it off for now.

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
require 'test_helper'
 
class ChoresCanCrud < ActionController::TestCase
  tests ChoresController
 
  specify "test that it should respond to index" do
    get :index
    assert_response :success
    assert_not_nil assigns(:chores)
  end
 
  specify "that it should pass chore on new" do
    get :new
    assert_response :success
    assert_not_nil assigns(:chore)
  end
 
  specify "that it should create chore" do
    assert_difference 'Chore.count' do
      post :create, :chore => { :description => 'My Chore' }
    end
 
    assert_redirected_to chores_url
  end
 
  specify "that it will destroy a chore, muahahahahaha" do
    chore = Chore.make
    assert_difference 'Chore.count', -1 do
      delete :destroy, :id => chore.id
    end
    assert_redirected_to chores_url
  end
end

The corresponding Chores controller looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ChoresController < ApplicationController
  def index
    @chores = Chore.find(:all)
  end
 
  def new
    @chore = Chore.new
  end
 
  def create
    @chore = Chore.new(params[:chore])
    @chore.save
    redirect_to chores_url
  end
 
  def destroy
    chore = Chore.find(params[:id])
    chore.destroy
    redirect_to chores_url
  end
end

Additionally, I had to make a couple views to get the tests to run. They are skeletal at this point, barely more than an empty file. In fact ./app/views/chores/index.html.erb is an empty file and ./app/views/chores/new.html.erb is

1
2
3
4
5
6
7
8
9
10
11
<% form_for(@chore) do |f| %>
  <%= f.error_messages %>
 
  <p>
    <%= f.label :description %><br />
    <%= f.text_field :description %>
  </p>
 
  <%= f.submit "Add" %>
  </p>
<% end %>

Looking back at the functional tests, this seems a little on the light side. For instance the tests passed without ever checking to see what happened with save of Chore. I’m going to go back and add a couple mote tests. Adding the following two tests

1
2
3
4
5
6
7
8
9
  specify "that it should send a message that chore was created" do
    post :create, :chore => { :description => 'My Chore' }
    assert_equal "Chore added.", flash[:message]
  end
 
  specify "that it should send a message that chore creation failed" do
    post :create, :chore => { :description => nil }
    assert_equal "Error saving Chore.", flash[:error]
  end

changes the create event to be

1
2
3
4
5
6
7
8
9
10
  def create
    @chore = Chore.new(params[:chore])
    if @chore.save
      flash[:message] = "Chore added."
      redirect_to chores_url
    else
      flash[:error] = "Error saving Chore."
      render :action =>  "new"
    end
  end

Well that should be enough to move on with our cucumber feature and a solid beginning for our chore controller. Running rake features again shows us that we indeed have passed the next step. It is now looking for a field that isn’t there.

$ 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
    And I fill in the chore with "My first chore"  # features/step_definitions/chores_steps.rb:5
      Could not find field labeled "chore[name]" (RuntimeError)
      c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/flunk.rb:4:in 'flunk'
      c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/locators.rb:16:in 'field_labeled'
      c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/locators.rb:10:in 'field'
      c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/scope.rb:183:in 'locate_field'
      c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/core/scope.rb:41:in 'fills_in'
      c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/rails.rb:88:in 'send'
      c:/Ruby/lib/ruby/gems/1.8/gems/webrat-0.3.4/lib/webrat/rails.rb:88:in 'method_missing'
      c:/Ruby/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/integration.rb:498:in '__send__'
      c:/Ruby/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/integration.rb:498:in 'method_missing'
      ./features/step_definitions/chores_steps.rb:6:in 'And /^I fill in the chore with "(.*)"$/'
      features/define_chores.feature:9:in `And I fill in the chore with "My first chore"'
    And I press Add                                # features/step_definitions/chores_steps.rb:9
    Then I should see "Chore added."               # features/step_definitions/webrat_steps.rb:83
    And I should see "My first chore"              # features/step_definitions/webrat_steps.rb:83
 
 
1 scenario
2 steps passed
1 step failed
3 steps skipped

It appears that I had intended to name the field Chore.name, but when it came around time to name the field, I named it Chore.description. I’ll go ahead and modify my step definition to read ‘chore[description]‘ and try again.

Much better this time. Looks like the feature is looking for the flash message to be displayed on screen. Currently we aren’t doing that, so I’ll need to drop that in.

$ 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
    And I fill in the chore with "My first chore"  # features/step_definitions/chores_steps.rb:5
    And I press Add                                # features/step_definitions/chores_steps.rb:9
    Then I should see "Chore added."               # features/step_definitions/webrat_steps.rb:83
      expected: /Chore added./m,
           got: "\n" (using =~)
      Diff:
      @@ -1,2 +1,2 @@
      -/Chore added./m
      +"\n" (Spec::Expectations::ExpectationNotMetError)
      ./features/step_definitions/webrat_steps.rb:84:in `Then /^I should see "(.*)"$/'
      features/define_chores.feature:11:in `Then I should see "Chore added."'
    And I should see "My first chore"              # features/step_definitions/webrat_steps.rb:83
 
 
1 scenario
4 steps passed
1 step failed
1 step skipped

Well, this isn’t beautiful but if I add this layout (as app/views/layout/application.html.erb), the flash will display and the feature step will pass and we can move on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Chores
</title>
</head>
<body>
 
<%= flash[:error] %>
<%= flash[:message] %>
 
<%= yield %>
 
</body>
</html>

that passes our next step, and we are onto the step that says ‘And I should see “My first chore”‘. This means that we are expecting it to have redirected us to index and that the new record that we just added should display on screen. Thus far I have placed nothing on the index view, so I will add the following.

1
2
3
<% @chores.each do |chore| %>
  <%=h chore.description %>
<% end %>

and now cucumber gives us some of it’s green cucumbery goodness with a passing test.

$ 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
    And I fill in the chore with "My first chore"  # features/step_definitions/chores_steps.rb:5
    And I press Add                                # features/step_definitions/chores_steps.rb:9
    Then I should see "Chore added."               # features/step_definitions/webrat_steps.rb:83
    And I should see "My first chore"              # features/step_definitions/webrat_steps.rb:83
 
 
1 scenario
6 steps passed

Now I’ll commit to git and script/server to take a look at what our testing has created. It looks good, once I remove index.html from the public directory, I can see the Add Chore link from the home page. Clicking on it gives me a validated form that allows me to enter a chore entry and save it. We also have tests that tell us that it’s all working well and functioning as expected.

We have taken a look at implementing a feature with Test::Unit, webrat and cucumber and it’s really not all that difficult to do. Really the process from this point on is rinse, repeat, refactor. I’m not sure at this point if I’ll return to post on Chores or not. It’s been interesting documenting my process as I go along, but it’s also been a weight slowing me down. In any case, I hope you have enjoyed this series on test-driven development and maybe have decided to try it out for yourself. I hope so, and if you have kids, maybe check out chores when it’s done.

Chores: a test driven website, part 4 (judgement day)

Wednesday, January 14th, 2009

Update This is part of the Chores series of posts

Back to building Chores, which started back here at the beginning. After you have a test running, it’s time for refactoring. Refactoring being the process of improving code without changing it’s functionality. You can do this in a variety of ways, commonly you will want to do this to keep it DRY (remove duplication) and improve readability. Once you have refactored your application or your tests, you will want to run the tests to make sure that they are all still passing.

This is one of the great benefits of automated testing. You can improve your code systematically and still be (for me, reasonably, since I’m still improving my testing skills and gaining confidence) sure that everything is still functioning as it was.

After a look through our code I didn’t see anything to change in the model and have modifed the unit test to read as follows.

require 'test_helper'
 
class ChoreShouldBeValidated < ActiveSupport::TestCase
 
  specify "that a description is required" do
    chore = Chore.new
    invalid chore
    chore.description = "Clean your room"
    valid chore
  end
 
  specify "that a description of 255 characters is valid" do
    chore = Chore.new :description => max_description
    valid chore
  end
 
  specify "that a description of 256 characters is invalid" do
    chore = Chore.new :description => max_description + "8"
    invalid chore
  end
 
  specify "the validation message for description" do
    chore = Chore.new
    invalid chore
    assert_equal "Description cannot be blank and must be less than 255 characters long.", chore.errors.on(:description)
  end
 
  protected
    def max_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"
    end
 
    def invalid model
      assert !model.valid?
    end
 
    def valid model
      assert model.valid?
    end
end

You’ll notice the spec style specify rather than test block. This can be achieved by simply adding the following to your test_helper file after the Test::Unit::TestCase class.

def specify *args, &block
  test(*args, &block)
end

I could move the valid and invalid methods to the test helper at this point as well, but so far there is no reason to so I am leaving it as it is, for now.

At this point I’m pretty happy with how it looks and reads. It’s similar to a BDD spec, but is still using the standard Test::Unit. However, looking back at this, I’m still not happy with the not null validation and the length validation being combined so I’m going to separate them out.

After making the changes and some refactors, my Chore model and matching test look like this:

1
2
3
4
5
6
7
8
9
class Chore < ActiveRecord::Base
  validates_length_of   :description,
                        :maximum => 255,
                        :allow_nil => true,
                        :message => "Description cannot be longer than 255 characters."
 
  validates_presence_of :description,
                        :message => "Description cannot be blank."
end
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
require 'test_helper'
 
class ChoreShouldBeValidated < ActiveSupport::TestCase
 
  specify "that a description is required" do
    chore = Chore.new
    invalid chore
    chore.description = "Clean your room"
    valid chore
  end
 
  specify "that a description of 255 characters is valid" do
    chore = Chore.new :description => max_description
    valid chore
  end
 
  specify "that a description of 256 characters is invalid" do
    chore = Chore.new :description => overly_long_description
    invalid chore
  end
 
  specify "message for a null description" do
    chore = Chore.new
    validation_message_for chore, :description, "Description cannot be blank."
  end
 
  specify "message for an overly long description" do
    chore = Chore.new :description => overly_long_description
    validation_message_for chore, :description, "Description cannot be longer than 255 characters."
  end
 
  protected
    def max_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"
    end
 
    def overly_long_description
      max_description + "8"
    end
 
    def invalid model
      assert !model.valid?
    end
 
    def valid model
      assert model.valid?
    end
 
    def validation_message_for model, column, message
      invalid model
      assert_equal message, model.errors.on(column)
    end
end

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

Friday, January 9th, 2009

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: