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.