TDD - Beyond The Buzzwords

The "Beyond The Buzzwords" series is focused on giving those who are starting to write automated tests a practical overview, so that they begin to get a grasp on the mechanics of doing it. Also, to encourage those who don’t do it yet to begin doing so. That said, the idea is to not be over theoretical and purist, scaring away newcomers - but keeping both feet firmly on the arduous ground in which lies the path to solid automated testing practices.

This is the second article in the series. Today I’m going to talk about TDD - taking a slightly different approach on the bookstore application we developed on the last one.

TDD stands for Test Driven Design - it means to let your tests guide your software development at each and every step of the way to quality and highly maintainable code. It is mainly based on a relentless three step heartbeat-like pattern that must not be disturbed:

Red => Green => Refactoring => Red

This very simple pattern stands at the very core of the TDD mindset. So, let’s take a look at the meaning of each work [if you have not guessed it already]:

  • Red stands for writing a test that describes a single simple and punctual functionality, then running that test to watch it fails. Yes, you should [must] write your tests first, otherwise they’ll nothing but re-hashes of your actual implementation code, therefore testing nothing in terms of domain logic per se.
  • Green stands for writing the bare minimum of code necessary to make the test pass. This is important, don’t jump to implement extra functionality just because you’re already coding anyway - keep it minimal, simple and focused on making the test pass, you can care about design later.
  • Refactoring stands for making your code better, from a design perspective, applying some refactoring principle [Martin Fowler wrote a book on this subject, published in 1999 - with this being a pretty good catalogue of commonly used refactoring patterns]. That must only be done when all your tests are passing, and they should still pass once you’ve done refactoring - otherwise it means that your embellishment  of the code actually broke it.

If you are willing to take the textbook approach to TDD, then the aforementioned definitions mean that not a single line of code shall be written if there isn’t a test failing because of the lack of that very specific line. And this is definitely a statement not to be taken lightly.

While BDD seems to lack support from proper tools for the modern widespread mainstream programming languages (JBehave and NBehave still have ways to go before standing up to cucumber) - TDD, on the other hand, has  plenty of them for virtually any platform you pick. Since our bookstore is a rails application, we’ll be using the ruby option that I like the most: rspec.

Let’s just say we are given the exact same story to work with than last time, in case you’ve forgot it:

As the bookstore owner
I want to be able to register new books including rating and author
So that I don’t need to call the IT guys to do it for me manually every single time

That being said, let’s see how it works on practice:

rails bookstore_tdd --database=mysql
cd bookstore_tdd
rake db:create:all
rake db:migrate
script/generate rspec

The last command shown above shall generate some infrastructure for testing with rspec on your rails application.

Now, let’s create the book model, instead of the default rails generator we’re going to use the rspec one, like this:

script/generate rspec_model book title:string author:string rating:decimal synopsis:text

Also let’s create the books table both on development and on test environment

rake db:migrate
rake db:migrate RAILS_ENV=test

Since we’ve used the rspec generator for the model, it already created an skeleton so that we can test it, you can check it on the spec/models/book_file.rb file. It should look like this:

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

describe Book do
  before(:each) do
    @valid_attributes = {
      :title => "value for title",
      :author => "value for author",
      :rating => 9.99,
      :synopsis => "value for synopsis"
    }
  end

  it "should create a new instance given valid attributes" do
    Book.create!(@valid_attributes)
  end
end

Since the whole code for persistence of the book model is handled by the active record framework, we don’t need to bother to write tests to ensure that it works - since such test suite is already done by those who develop it.

But, for the sake of exemplifying the possibilities, let’s say that our business policy says that it is OK to have a book without synopsis and/or rating, but its title and author are mandatory properties. The previous statement describes a very specific rule to our application - the best way to express it is writting a test to make it clear. Well written tests can virtually replace any need for external documentation - in other words: your tests should describe as clearly as possible the particularities of your application.

Keep in mind that rspec is a very flexible tool, and can be used in many different ways to accomplish the same goal. I’m going to show an approach that I am particularly inclined to use very often. Let’s change the placeholder code that was automatically inserted on the book spec to this:

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

describe Book, "with all attributes properly set" do
  before(:each) do
    @valid_attributes = {
      :title => "value for title",
      :author => "value for author",
      :rating => 9.99,
      :synopsis => "value for synopsis"
    }
  end

  it "should create a new instance given valid attributes" do
    lambda { Book.create!(@valid_attributes) }.should_not raise_error
  end
end

describe Book, "missing rating and synopsis" do
  before(:each) do
    @valid_attributes = {
      :title => "value for title",
      :author => "value for author"
    }
  end

  it "should be OK, since rating and synopsis are optional" do
    lambda { Book.create!(@valid_attributes) }.should_not raise_error
  end
end

describe Book, "missing author" do
  before(:each) do
    @invalid_attributes = {
      :title => "value for title",
      :rating => 9.99,
      :synopsis => "value for synopsis"
    }
  end

  it "should not be allowed, because author is mandatory" do
    # lambda { Book.create!(@invalid_attributes) }.should raise_error <= this is another way to do it
    book = Book.create(@invalid_attributes)
    book.errors_on(:author).should_not be_empty
  end
end

describe Book, "missing title" do
  before(:each) do
    @invalid_attributes = {
      :author => "value for author",
      :rating => 9.99,
      :synopsis => "value for synopsis"
    }
  end

  it "should not be allowed, because title is mandatory" do
    # lambda { Book.create!(@invalid_attributes) }.should raise_error  <= this is another way to do it
    book = Book.create(@invalid_attributes)
    book.errors_on(:title).should_not be_empty
  end
end

That was a lot of code, so let’s take a look at some crucial concepts:

  • With rspec you don’t write tests for your classes, you write a description about what is expected from them - therefore the use of the “describe” syntax
  • Some people defend fewer contexts for each model, I particularly like to create a new describe block for each kind of scenario
  • Rspec defines the methods “should” and “should_not” for every object, so you can evaluate the result using them - a full list of the possibilites can be consulted here

You probably have noticed that each test has been written using the prefix “it” followed by a natural language sentence, right? Because of that, the outcome we get when we run our specs [rake spec] is something like this:

FF..

1)
'Book missing title should not be allowed, because title is mandatory' FAILED
expected empty? to return false, got true
./spec/models/book_spec.rb:59:

2)
'Book missing author should not be allowed, because author is mandatory' FAILED
expected empty? to return false, got true
./spec/models/book_spec.rb:43:

Finished in 0.054108 seconds

4 examples, 2 failures

As I already stated before, from the excerpt shown just above - if you plan your tests to pinpoint each specifical detail of functionality you might even be able to never again need to run a debugging session. By looking at the result shown above we can notice the “FF..” on the beginning, it means that there are two tests failing [F] an two passing [.].

Since it failed, it is time to write the code to make it pass. On the book model, add the validation for the mandatory fields, like this:

class Book < ActiveRecord::Base
  validates_presence_of [:title, :author]
end

Running the specs again show us that it worked:

....

Finished in 0.061223 seconds

4 examples, 0 failures

Homework: write tests to guarantee that duplicated titles are not allowed.

Now we need to add another test, describing our application. Let’s create the books controller, once again, we’re using the rspec generator

script/generate rspec_controller books

Go to the spec/controllers/books_controller_spec.rb and let’s write our test for the action “create”:

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

describe BooksController do
  it "should have an action that allow the creation of new books" do
    controller.should respond_to(:create)
  end
end

Ideally speaking, you will want to keep each “example” (the term rspec uses for each individual test) to the minimum assertions possible. That will make things more traceable later. Run the spec and watch it fail!

Now, time to write the minimum code necessary to make it pass, on the books controller:

def create
end

Run the specs again and see that its all green now.

Now we need to write another test, since clearly just having a method create on our books controller won’t do us any good. What’s missing is that the create method should receive the parameters to create a new book on the database and do it.

But this is a problematic point: if we write the test to actually persist our book on the database, we will be depending on book model logic again - which has already been tested somewhere else. This is not going to be a unit test at all, but a full stack one - which is bound to become slow pretty fast. Not to mention that if the controller tests depend on the model, and you screw up the model, the controller tests will also fail, totally ruining the traceability that your test suite should provide.

The solution for that is another widespread term: dependency injection. Any decent modern language have a whole bunch of frameworks for dependency injection a.k.a. inversion of control. What it means is that your implementation should be flexible enough to depend on the interface of its dependencies, not on their implementation. Putting it in simple terms, let’s say you’re programming an entity on C#, that depends on a repository for persistence, you should create an interface for this repository and have your entity provide some way of replacing the actual implementation with a fake one (mostly for the purpose of decoupled testing). But, since we’re working with ruby, and it has open classes, it is not even needed to bother with providing ways for dependency injection - just keep the concept in mind.

To properly make use of dependency injection, we need to setup fake implementation for the dependencies of our classes. Let’s look at our bookstore: the books controller depends on the book model, so we need to replace the model implementation on the controller with a fake one, that provides us with full control over it. What I’m talking about right now can be mainly categorized between two big concepts of unit testing: mocks and stubs. The difference between the two is very subtle on the first sight, so further reading about these two concepts is strongly advised. There are also some other important ones, like fakes and spies. But that isn’t needed for now, just try to get a grasp on their purpose in this practical example, continuing our bookstore:

describe BooksController, "creating a valid book" do
  before(:each) do
    @params_for_save = { "title" => "advanced joseki", "author" => "honinbou shusaku" }
    @book = mock_model(Book)
    Book.stub!(:new).and_return(@book)
  end

  def call_create
    post :create, { :book => @params_for_save }
  end

  it "should instantiate a new book with the given parameters" do
    Book.should_receive(:new).with(@params_for_save).and_return(@book)
    call_create
  end
end

One thing at a time, let’s look ath the “before each” method first:

  • @params_for_save is just a helper to avoid repetition of the same hash of parameters over and over and over again
  • @book represents a mocked instance of the book model - we’ll see more about this later on
  • Book.stub! says that the Book (app/model/Book.rb) class will be replaced with this stub. When the method “new” is called, this stub will return our mocked book, instead of creating a new instance

What we gained from that? Let’s say the “new” (initialize) method of the original book model does something like checking the database for some condition, or calling an webservice, or whatever… by replacing the Book class implementation with this stub, we’re totally decoupling our controller test from any of that. These aspects of the Book model should have been already tested elsewhere (on the specific Book model spec) - so we can just assume that they will work and focus on what we are testing right now: the controller.

The “call_create” is just a helper method, once again to avoid repetition. What it does is just simulate a post (like a form would do) to the create method, passing the parameters like if it was being passed by the real form.

The example says that in the method we are testing (create), the constructor of the Book class shall be called, passing the parameters received on the post request - and when it is called, it will return our mocked book model. Running the specs now will fail, because our “create” method isn’t calling the constructor of the Book class, as the example describes it should.

To fix that we must instantiate a new book on the controller.

@book = Book.new params[:book]

This will make the specs run successfully! So, time to ellaborate on our examples:

describe BooksController, "creating a valid book" do
  before(:each) do
    @params_for_save = { "title" => "advanced joseki", "author" => "honinbou shusaku" }
    @book = mock_model(Book, :save! => true)
    Book.stub!(:new).and_return(@book)
  end

  def call_create
    post :create, { :book => @params_for_save }
  end

  it "should instantiate a new book with the given parameters" do
    Book.should_receive(:new).with(@params_for_save).and_return(@book)
    call_create
  end

  it "should save the book on the database" do
    @book.should_receive(:save!).and_return(true)
    call_create
  end

  it "should notice the user about the success" do
    call_create
    flash.now[:notice].should match(/.*successfully.*/)
  end
end

Things to notice:

  • The mock_model call on the “before each” method has been slighly modified. That tells our mock that “save!” is a valid method, and when called it should return true. That means that when the controller calls it, during the test, it won’t actually save the book on the database, but just get a true return - just like the actuall Book would do if the save operation were successful
  • There were added two new examples, which I think shouldn’t be a problem to understand. I’ve added two at once for the sake of eliminating the repetition on this article - while developing you should write one test (example, whatever you call it) and watch it fail, make it pass, then write another one and so on… Keep that in mind, because from now on I will quicken up the pace

Running the specs it shall fail - then let’s make it pass:

class BooksController < ApplicationController
  def create
    @book = Book.new params[:book]
    @book.save!
    flash[:notice] = "The book was saved successfully"
    render :text => ""
  end
end

Running the specs now shall give you an all green result.

To wrap it up, let’s see how we can add a error scenario to our controller test - like an invalid book. It will probably look something like this:

describe BooksController, "creating an invalid new book" do
  before(:each) do
    @params_for_save = { "title" => "the god delusion", "author" => "richard dawkins" }
    Book.stub!(:new).and_return(@book = mock_model(Book, :save! => true))
  end

  def call_create
    post :create, { :book => @params_for_save }
  end

  it "should instantiate a new book with the given parameters" do
    Book.should_receive(:new).with(@params_for_save).and_return(@book)
    call_create
  end

  it "should notice the user that the book is invalid" do
    @book.should_receive(:save!).and_raise(StandardError.new("Error saving the book"))
    call_create
  end

  it "should notice the user about the failure" do
    @book.should_receive(:save!).and_raise(StandardError.new("Error saving the book"))
    call_create
    flash.now[:notice].should match(/.*error.*/)
  end
end

Notice how I said that I was going to use an invalid book but used the exact same (business-wise VALID) parameters? That’s the wonder of mocking and stubbing - the controller test doesn’t need to and shouldn’t [must not! if you're purist enough] know what is a valid book at all. It just needs to know how the controller should react given that the book model showed that it is not valid. Once again: the test to check if a book is been properly validated have already been tested on the model level specs.

So, let’s make the tests pass - shall we?

class BooksController < ApplicationController
  def create
    @book = Book.new params[:book]
    @book.save!
    flash[:notice] = "The book was saved successfully"
    render :text => ""
  rescue
    flash[:notice] = "There was an error saving the book"
    render :text => ""
  end
end

That will make all specs pass.

Notice that I didn’t mentioned anything about views - while rspec, not to mention the entire rails structure, gives you a pretty good support for testing them, I usually don’t do it on the unit test level [it probably is due to my lack of ability to do it properly, but the code simply tends to become really messy and not very readable]. So I try to leave it to cucumber.

As usual you can get the code used for this article on github. Please leave your impressions on the comments section.

Tags: , , , , , , , , , , , , , , , , , , , , , , , , ,

{ Jul 31, 2009 - 12:07:24 } BDD - Beyond the Buzzwords | Codevil