Test Driven Development in Rails - Part I (Models)

Posted by caike on March 04, 2009

The following is part one of a series of examples of a simple feature developed using Ruby on Rails. The intent of this post is to show how the test code can (and should, as much as possible) be written before the actual implementation code, which is an agile practice known as Test Driven Development.

I expect this series of examples to show how writing tests is actually a design activity rather than simply code verification. Each new test runs along with all the previous tests. This automation helps developers by giving them constant feedback.

The Steps

According to Kent Beck, the rythm of Test Driven Development can be summed up as follows:

1. Quickly add a test.

2. Run all tests and see the new one fail.

3. Make a little change.

4. Run all tests and see them all succeed.

5. Refactor to remove duplication.

The Tools

There are many testing framework options to choose from in the Ruby world (RSpec, Cucumber, Shoulda, etc.) but in this example I’m going to keep it plain and simple using just Test::Unit for unit testing the models and ActionController for testing controllers as well as testing routes.

After writing each test and ensuring that it fails, we will write the simplest thing that could possibly work. At first it may look ugly and not “the Rails way” of doing things, but we will later enhance its quality using another agile technique known as refactoring.

Rails out-of-the-box expects you to have a database setup and creates default fixtures for each of your models. Although there is a way to get around that, I’ll leave it alone for now.

The problem

Our client wants us to develop an e-commerce app for his musical instrument shop. From our meetings, we agreed that users should be able to buy instruments online. He said that for an instrument to be considered valid it must belong to a category, have a description and have a price. If either of those conditions is not true, then the instrument is not valid.

Making it work

Notice how the meeting revealed a few keywords and concepts to be used in the construction of our domain model and its relationships (instrument, description, category, price, belongs to).

We start off creating our application:

$ rails music_shop
$ cd music_shop

and creating our first model:

$ ./script/generate model instrument

The output shows that a bunch of files were created. At this moment, only three files are important for us: instrument migration file, model and unit test.

Our client told us he wants an instrument with description, price and category, therefore they will be the initial attributes for instrument.

Add the following snippet to some_number_create_instruments.rb located in db/migration.

create_table :instruments do |t|
     t.string       :description
     t.decimal      :price, :precision => 8, :scale => 2
     t.references   :category

     t.timestamps
end

This migration file is going to build the instruments database table using the adapter defined in database.yml, located in the config folder. Unless defined otherwise, Rails uses SQLite as dbms and I suggest that you keep it that way until deployment (or even until SQLite becomes a bottleneck for you app).

Run your migration:

$ rake db:migrate

The database table has been created for you and you don’t need to worry about it anymore.

Now we should focus on the business requirements. Open test/unit/instrument_test.rb and add the following snippet that relates to what the client said about a valid instrument.

  def setup
    @valid_params = {:description => "gibson sg",
                     :price => 100.00, 
                     :category => Category.new}
   end

  test "not valid without description" do
    guitar = Instrument.new(@valid_params.merge(:description => nil))
    assert !guitar.valid?
    assert guitar.errors.on(:description)
  end

 

Let’s run our first test

$ rake test:units

and watch it fail!

1 tests, 0 assertions, 0 failures, 1 errors

It was expected to fail since we have not implemented any validation yet. Actually it has not failed, but spit an error instead:

1) Error:
test_not_valid_without_description(InstrumentTest):
NameError: uninitialized constant InstrumentTest::Category

The error says there is no Category class defined. So lets generate our missing model, run the created migration and then run the test again.

$ ./script/generate model category
$ rake db:migrate
$ rake test:units

No complaining about missing class, but it says there is no category attribute for our Instrument model.

1) Error:
test_not_valid_without_description(InstrumentTest):
ActiveRecord::UnknownAttributeError: unknown attribute: category

Indeed, we haven’t coded anything that says that an instrument belongs to a category. So let’s do that in our Instrument model:

class Instrument < ActiveRecord::Base
  belongs_to :category
end

Run the tests again and notice how the errors are gone. Now there is just a failing test that needs to get out of our way.

2 tests, 2 assertions, 1 failures, 0 errors

Notice how the test code is guiding us through design and it’s telling us what needs to be done, like in a two-way conversation.

The process of starting a feature with failing tests and coding towards making the tests pass is known as “red green refactor”, resembling the red and green bars present in previous unit test framework outputs (JUnit being the most famous). Taking baby steps is a safe way to measure the impact of you actions in the code.

Only one line is needed to make the failure go away. In the app/models/instrument.rb add the following:

class Instrument < ActiveRecord::Base
  belongs_to :category
  validates_presence_of :description
end

Run the test again and watch the failure go away.

1 tests, 1 assertions, 0 failures, 0 errors

I believe that as you gain proficiency with test driving your code, you can go ahead and write a couple of failing tests before going into implementation code - as long as they are related to the same context of the unit under test.

Moving on with our tests, let’s add two more:

 test "not valid without price" do
    guitar = Instrument.new(@valid_params.merge(:price => nil))
    assert !guitar.valid?
    assert guitar.errors.on(:price)
  end

  test "not valid without category" do
    guitar = Instrument.new(@valid_params.merge(:category => nil))
    assert !guitar.valid?
    assert guitar.errors.on(:category_id)
  end

Run it again with rake and watch it fail:

3 tests, 3 assertions, 2 failures, 0 errors

Now only the barely essential to make the above tests pass is added, and the complete instrument model looks like the following:

class Instrument < ActiveRecord::Base
  belongs_to :category                      

  validates_presence_of :description, :price
  validate :has_a_category

  def has_a_category
    if category.nil? && category_id.nil?
      errors.add(:category_id, "must have a category")
    end 
  end
end

Run the tests again and they should all pass

5 tests, 5 assertions, 0 failures, 0 errors

Conclusion

I hope the previous steps showed some of the benefits of test driven development, which is, in my humble opinion, one of the most important agile practices.

It should be taken as a design practice rather than pure verification. Running automated tests keeps the developers in control of the application and makes code easier to maintain and evolve.

Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

Comments

Anti-Spam Protection by WP-SpamFree