Working with Rails

Recommend Brian on Working With Rails


Powered

Building Tempo with Rails, Part II

Posted by Brian Sam-Bodden Wed, 24 Oct 2007 23:00:00 GMT

Rails

Welcome back to part II of this series. Like I mentioned at the end of the first post; now we start the real work.

BDD, TDD and DDD

The sentence below summarizes what our development strategy will be:

"We are going to flesh the domain (DDD) with behaviours using (BDD) that we will express as tests (TDD) before writing any code"

We are going to be using RSpec, a Ruby XUnit framework that facilitates the writing of tests that test behavior rather than state, in the spirit of Behavior-Driven Development (BDD). With RSpec we will write the tests that will guide the development of our application. We will try to stick with Test-Driven Development (TDD) as closely as possible. RSpec can be thought of as a domain specific language for behavior testing via expectations.

If you have been using Test::Unit and you want to use RSpec you don't have to chose one over the other. Although if you have to make a choice, RSpec will give you more readable tests.

If you want to get some background on the ideas behind BDD take a peek at

Installing RSpec

As we did with Restful Authentication plugin we'll use Piston to install RSpec:

piston import svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec 
   vendor/plugins/rspec
Exported r2563 from 'svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec' 
to 'vendor/plugins/rspec'

Installing RSpec On Rails

An RSpec plugin that integrates with Rails exist which gives you the ability to create specs (read behavior tests) for your models, views, controllers and helpers. To install the RSpec On Rails plugin we again use Piston:

piston import svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec_on_rails 
   vendor/plugins/rspec_on_rails
Exported r2563 from 'svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec_on_rails' 
to 'vendor/plugins/rspec_on_rails'

To create the necessary RSpec directories and bootstrap code you need to run the rspec rake task:

script/generate rspec
      exists  spec
      create  spec/spec_helper.rb
      create  spec/spec.opts
      create  previous_failures.txt
      create  script/spec_server
      create  script/spec 

Building Tempo's Domain

Now we are ready to tackle the domain of the application

Initial Domain Model

I know it is a cliché but I actually modelled the domain on a paper towel which I later scanned (Jim Weirich is much more classy and uses fine napkins). Below is my first pass at the nouns that immediately came to mind when thinking about tracking time in the context of a project at a consulting firm:

Tempo Domain Model

Based on the noun list above I created a simple UML-like diagram (in later issues of this blog I'll create something nicer):

Tempo Domain Model

The list below spells out what my thinking was when creating this model:

  1. A User is associated with a Person *
  2. A User can be part of zero or more Projects
  3. A Project belongs to a Client *
  4. A Project has some Project Activities associated which are a subset of a global list of Activities
  5. A TimeEntry represents time entered against a project by a User in the context of a particular Activity

(1*) my thinking here is that I would have non-user entities in the system therefore I wanted to separate the Person/People (3*) don't know yet if a Client is a Person, User or other Entity

Person

Based on my napkin domain model a User has an associated Person. Let's use the Rspect generate script to create a model class for Person with its associated specs:

script/generate rspec_model Person
      exists  app/models/
      create  spec/models/
      create  spec/fixtures/
      create  app/models/person.rb
      create  spec/fixtures/people.yml
      create  spec/models/person_spec.rb
      exists  db/migrate
      create  db/migrate/002_create_people.rb

Just like the normal Rails "generate script" we end up with a Person class in person.rb:

class Person < ActiveRecord::Base
end

But we also get the skeleton code for a Person spec in person_spec.rb:

require File.dirname(__FILE__) + '/../spec_helper'

describe Person do
  before(:each) do
    @person = Person.new
  end

  it "should be valid" do
    @person.should be_valid
  end
end

The basic spec skeleton "describe"s the behavioral contract that a class should abide to. The before method gets executed before each of the "it" or specification methods. In the case about we have the simplest specification method for a Rails model object. In this case it will call the validation method exposed by ActiveRecord.

Before we move forward we need to flesh out the Person class via the ActiveRecord migration. I'm gonna add just the basic fields that we would expect in a Person object (First Name, Last Name and Middle Initial):

class CreatePeople < ActiveRecord::Migration
  def self.up
    create_table :people do |t|
      t.column :first_name, :string
      t.column :last_name, :string
      t.column :middle_initial, :string
    end
  end

  def self.down
    drop_table :people
  end
end

Let's run the migration...

rake db:migrate
(in /Users/bsbodden/Documents/projects/tempo)
== CreatePeople: migrating ====================================================
-- create_table(:people)
   -> 0.0033s
== CreatePeople: migrated (0.0035s) ===========================================

During development I might create a ton of little migrations, you can later glue some of those together and refactor your migrations. In the Person class I want to make the first and last names mandatory but the middle initial optional. Now we can enhance the spec to test for the presence of the required fields.

require File.dirname(__FILE__) + '/../spec_helper'

describe "A person (in general)" do
  before do
    @user = Person.new
  end

  it "should be invalid without a first and last name" do
    @user.should_not be_valid
    @user.first_name = 'Chunky'
    @user.last_name = 'Bacon'
    @user.should be_valid
  end
end

The spec above tell us that a "user should be invalid without a first and last name". I check that right after we call the new method the object should not be valid and that after we set the first and last name it should.

Let's run the test and see what happens:

rake spec
(in /Users/bsbodden/Documents/projects/tempo)
F

1)
'A person (in general) should be invalid without a first and last name' FAILED
expected valid? to return false, got true
./spec/models/person_spec.rb:19:

Finished in 0.100052 seconds

1 example, 1 failure

Ok, the test failed... and that's good. Now let's write enough code to pass the test. To do this we can just piggy back on the power of ActiveRecord and add validations to the Person model:

class Person < ActiveRecord::Base
  validates_presence_of :first_name, :last_name 
end

There is sometimes a fine line between testing your code and inadvertently testing a framework. In this case I think that we are testing that our code correctly uses the validation features of ActiveRecord but some might argue this.

Let's run the tests again:

rake spec
(in /Users/bsbodden/Documents/projects/tempo)
.

Finished in 0.046277 seconds

1 example, 0 failures

User

Great, so we have our first pseudo real spec. The next thing we want to do is associate the Person object to the User object created by the Restful Authentication generator.

From the napkin model we know that a User should have a Person. Let's write a spec that tests that condition:

module UserSpecHelperMethods
  def create_user(options = {})
    User.create({ :login => 'cbacon', 
                  :email => 'chunky@bacon.com', 
                  :password => 'uhmmbacon', 
                  :password_confirmation => 'uhmmbacon'
                }.merge(options))
  end
end

describe User do

  include UserSpecHelperMethods

  before do
    @user = create_user
    @person = Person.new
    @person.first_name = 'Chunky'
    @person.last_name = 'Bacon'
  end

  it "should be invalid without an associated Person" do
    @user.should_not be_valid
    @user.errors.on(:person)
      .should eql("must have an associated person")
    @user.person = @person
    @user.should be_valid
  end
end

Notice that I added a module to my spec class with a helper method that creates a User object with certain fields populated. In the "before" method I create a User and a Person (a valid Person). Finally in the method that tests that a User "should be invalid without an associated Person" I follow the pattern we used before. I test that the object is invalid and then I set the Person object onto the User object and test that they User is now valid. I added a little bit of code temporarily to inspect the errors hash so that I could grab the exact error String and test for it:

@user.errors.each do |error|
  puts error
end

With new test in place we can run the tests again:

rake spec
(in /Users/bsbodden/Documents/projects/tempo)
.F

1)
NoMethodError in 'User should be invalid without an associated Person'
undefined method `person=' for #
./spec/models/user_spec.rb:17:

Finished in 0.054722 seconds

2 examples, 1 failure

As expected, it fails. Let's write just enough code to pass the test, let's create a relationship between User and Person

The ActiveRecord relationship I'm interested in is a has_one, as the code snippet from user.rb below shows:

has_one :person, :dependent => :destroy

The :dependent flag would make sure that the deletes cascade so we are implying that a Person that gets associated with a User should not be left orphaned.

We also need to create a Rails Migration to add the relationship in the database:

script/generate migration add_person_to_user
      exists  db/migrate
      create  db/migrate/003_add_person_to_user.rb

Modify the migration to add a column for the person_id:

class AddPersonToUser < ActiveRecord::Migration
  def self.up
    add_column :users, :person_id, :integer
  end

  def self.down
    remove_column :users, :person_id
  end
end

... and of course, run the migration:

rake db:migrate
(in /Users/bsbodden/Documents/projects/tempo)
== AddPersonToUser: migrating =================================================
-- add_column(:users, :person_id, :integer)
   -> 0.6555s
== AddPersonToUser: migrated (0.6556s) ========================================

... and test again

rake spec
(in /Users/bsbodden/Documents/projects/tempo)
.F

1)
'User should be invalid without an associated Person' FAILED
expected valid? to return true, got false
./spec/models/user_spec.rb:18:

Finished in 0.058424 seconds

2 examples, 1 failure

Add the validation in user.rb to pass the test:

  validates_presence_of :person, 
                        :message => 'must have an associated person'

... and test again:

rake spec
(in /Users/bsbodden/Documents/projects/tempo)
../spec/models/user_spec.rb:26: warning: multiple values for a block parameter (2 for 1)
        from ...
person
must have an associated person
.

Finished in 0.059858 seconds

2 examples, 0 failures

So far we have learned the basics of testing your model using RSpec. In the next installment we will pick a critical piece of functionality and build it using TDD/BDD/DDD.

Posted in ,  | Tags , ,  | no comments | no trackbacks

Comments

Trackbacks

Use the following link to trackback from your own site:
http://www.integrallis.com/blog/trackbacks?article_id=building-tempo-with-rails-part-ii&day=24&month=10&year=2007

Comments are disabled

Tags

agile drools gorm Grails Groovy Java orm Rails Ruby Security spring

Categories

Archives

Syndicate

Twitter