BDD - Beyond the Buzzwords

This post is going to be the first of a series "Beyond The Buzzwords" of two (maybe three). This being a subject that has been getting some hype everywhere now doesn’t seem to be the kind of thing you would normally see here. We generally like to talk about the state of the art, and about things you can’t just google for - replicating knowledge like oh so many other blogs out there do, doesn’t particularly appeal to any of us.

But, since a great friend of mine asked for some very specific material about tests on rails, I decided to direct the effort to make something a little more presentable and put it here, so that anyone can benefit from it (and/or, most likely, criticize it).

Intro

Virtually any of us who ever worked with the whole PMI, CMMI overhead pestering the thinking process at each and every step might have found himself thinking something on the lines of: What the heck am I doing all this soon-to-be totally obsolete documentation for? How can I expect to know how the software will be written before starting to write it?

I know, the classic methodologies are not necessarily bad. They just tend to be really good for the mediocre minds, and really bad for the smart ones. Since there are a gazillion more of the first kind in this world we live on…

KaizenAs an answer to that sort of overly defensive mentality, came the agile manifesto. Ever since, agile methodologies and buzzwords have been popping up furiously each and everyday. The vast majority of them coming from the well known Toyota’s Kaizen mentality.

At the very core of any reliable agile culture for software development [alongside many other things] lies the love for testing. Testing early, testing often, testing all the fucking time. Yes, love, you really got to eager for it every second of your development cycle.

It is often said that very good programmers are not seldom very bad at testing, and they simply couldn’t hate any more than they do. My former self is a pretty good example of that - a lightning fast programmer, a good understating of design patterns and practices but, testing? If you asked me a year ago, I would have said: that’s work for some monkey, not ME. the UI is a sketchy prototype, but the game mechanics is rock solid already

But that was the old fashioned me, which was used to seeing manual testing, and thought actual automated testing was something that could only be seen on textbooks and academic projects. Once you get the grasp on the value and importance of testing, you just won’t be willing to ever do anything for serious without it. These days I find myself doing a test first approach for virtually anything that I truly wish to work - like the AI for a relatively simple board game I’m working with on my spare time right now.

Two of the most widespread terms for testing are BDD and TDD. I’ll be talking about these two on the two articles of this series - if the need arises I might even write a third to sum it up. This first article is all about BDD.

BDD stands for Behavior Driven Design (Development - nobody seems to know anymore which one is the right term, I use "Design" because I like it). This practice suggest a deeper relationship between the developers and the domain experts (your clients, most likely). The basic idea is to get both sides talking the same language so that they can, together, determine how the software that will be developed should behave.

To accomplish that, any framework that claims to be a tool for BDD shall provide a language that can be both understandable by only slightly technical people and programmatically viable to interpret on automated tests.

The ruby language holds one of the most remarkable of such frameworks out there: cucumber. For the next few paragraphs I will go through a very simple implementation of a bookstore (only the "add new book" functionality actually) using ruby on rails, from a BDD approach using cucumber.

If we were on an agile environment, most likely the development team would get a story to work on like this:

As the bookstore owner
I want to be able to register new titles to sell, including rating and synopsis
So that I can extend the list of products for sale without depending on manual intervention from the IT guys

After talking with the story owner, we decided that the relevant fields that a book entry should have are:

  • Title
  • Author
  • Rating
  • Synopsis

So now, let’s go to the fun part: code! [YATTA]

pre-reqs

To properly follow this series of articles you will most likely need the following (along with all their dependencies):

  • Ruby
  • Gem
  • Rails
  • Cucumber
  • MySQL
  • ZenTest (not for this article - but get it already!)
  • Rspec

For the installation process of the aforementioned requirements on your platform of choice you are on your own though. A simple google search will most likely get you there - don’t forget to also install all their pre-reqs. You should be able to follow the steps below on any OS - I’m particularly using Ubuntu with GMate (a bunch of customizations for GEdit) as the text editor.

I would strongly advice against trying it on any Windows platform though. Don’t get me wrong, I love Microsoft products, but ruby just don’t get along well with Windows, not to mention there are not a single text editor nor IDE that feel good to work with it on the platform [but that's only my personal opinion - there are always those kids that love netbeans…]. If you must [you shouldn't], try to at least go with JRuby, it runs way smoother than the one click installer ruby on Windows.

1

First things first, creating the application and the databases (using mysql because I’m too lazy to install sqlite on my ubuntu VM):

rails --database=mysql bookstore
cd bookstore
rake db:create:all
rake db:migrate

2

Adding cucumber support:

script/generate cucumber

This should generate some basic infrastructure for your application.

3

Writing the first feature:

gedit features/register_new_books.feature &

Here is where the fun will begin. Remember our story? We’re going to translate it to cucumber:

First the feature overview, it is just for the sake of documenting, so you can write it however you feel its best:

Feature: Register new books
In order to make my bookstore expansible
As a bookstore owner
I want to be able to register new books

Now, we’re going to use a scenario outline, to define how this specific feature should behave when used:

Scenario Outline: User register a new book successfully
Given I am on the new book register page
Then I should see "Title"
And I should see "Author"
And I should see "Rating"
And I should see "Synopsis"
When I fill in "book[title]" with "<book_title>"
And I fill in "book[author]" with "<book_author>"
And I fill in "book[rating]" with "<book_rating>"
And I fill in "book[synopsis]" with "<book_synopsis>"
And I press "Save"
Then I should not see "<book_title>"
And I should not see "<book_author>"
And I should not see "<book_rating>"
And I should not see "<book_synopsis>"
And I should see "successfully"
And the book with the title "<book_title>" should have been saved on the database

One very important point to get here, is that if you use the constructs "I should see __", "I fill in __ with __", "I press __" - these, alongside with a couple more are predefined steps that webrat provides you out of the box. You can check them all on the webrat_steps.rb file.

Please note that the <> enclosed strings are placeholders, so that one can execute that set of steps for multiple examples of actual data. Now, we need to define those examples, in a mostly intuitive way:

Examples:
| book_title          | book_rating | book_synopsis     | book_author         |
| the god delusion    | 9           | too soft          | richard dawkins     |
| the antichrist      | 9           | ubermesnch        | friedrich nietzsche |
| holy bible          | 0           | childish folklore | charlatans          |

4

Giving the feature it’s first go, just type:

rake features

The output should be a colorful (if you’re on a decent OS/Command Line/Bash):

Feature: Register new books
  In order to make my bookstore expansible
  As a bookstore owner
  I want to be able to register new books

  Scenario Outline: User register a new book successfully
    Given I am on the new book register page
    Then I should see "Title"
    And I should see "Author"
    And I should see "Rating"
    And I should see "Synopsis"
    When I fill in "book[title]" with "<book_title>:"
    And I fill in "book[author]" with "<book_author>:"
    And I fill in "book[rating]" with "<book_rating>:"
    And I fill in "book[synopsis]" with "<book_synopsis>:"
    And I press "Save"
    Then I should be on the new book register page
    And I should see "Title"
    And I should see "Rating"
    And I should see "Author"
    And I should see "Synopsis"
    And I should not see "<book_title>"
    And I should not see "<book_author>"
    And I should not see "<book_rating>"
    And I should not see "<book_synopsis>"
    And I should see "successfully"                    

    Examples:
      | book_title       | book_rating | book_synopsis  | book_author         |
CLUE  | the god delusion | 9           | too soft | richard dawkins     |
==>   Can't find mapping from "the new book register page" to a path.
==>   Now, go and add a mapping in
==>   /home/macskeptic/workspace/rails/bookstore/features/support/paths.rb (RuntimeError)
==>   /home/macskeptic/workspace/rails/bookstore/features/support/paths.rb:22:in `/^I am on (.+)$/'
CLUE  features/register_new_books.feature:7:in `Given I am on the new book register page'
      | the antichrist   | 9           | ubermesnch     | friedrich nietzsche |
      Can't find mapping from "the new book register page" to a path.
      Now, go and add a mapping in
      /home/macskeptic/workspace/rails/bookstore/features/support/paths.rb (RuntimeError)
      /home/macskeptic/workspace/rails/bookstore/features/support/paths.rb:22:in `/^I am on (.+)$/'
      features/register_new_books.feature:7:in `Given I am on the new book register page'
      | holy bible       | 0           | childish folklore    | charlatans          |
      Can't find mapping from "the new book register page" to a path.
      Now, go and add a mapping in
      /home/macskeptic/workspace/rails/bookstore/features/support/paths.rb (RuntimeError)
      /home/macskeptic/workspace/rails/bookstore/features/support/paths.rb:22:in `/^I am on (.+)$/'
      features/register_new_books.feature:7:in `Given I am on the new book register page'

3 scenarios (3 failed)
60 steps (3 failed, 57 skipped)

As you can see, it failed, since we got nothing on our application yet, just the feature specification. But it gave us a clue about what to do next, as I pointed above.

The phrase above is a very interesting concept - you write about the expected behavior before doing anything related to the code that will make it happen.

5

Since it said there is no "the new book register page" path, we’re going to add it, so that we solve the current problem and then we can try running the feature once again:

gedit features/support/paths.rb &

Here we will just define the missing path, adding something like this:

when /the new book register page/
'/books/new'

6

Running the feature again changes the error:

No route matches "/books/new" with {:method=>:get} (ActionController::RoutingError)
(eval):2:in `/^I am on (.+)$/'
features/register_new_books.feature:7:in `Given I am on the new book register page'

Once again, the error gives us a pretty nice hint about what to do next.

7

We need a route that matches books/new… that is a controller for books, with a method called new, so on to id:

gedit app/controllers/books_controller.rb &

Adding the method that should look something like this:

class BooksController < ApplicationController

def new
render :text => ""
end

end

I’ve chosen to render an empty text so that it won’t complain about there is no template yet.

8

Running the feature again changes the error:

expected the following element's content to include "Title":
(Spec::Expectations::ExpectationNotMetError)
features/register_new_books.feature:8:in `Then I should see "Title"'

3 scenarios (3 failed)
60 steps (3 failed, 54 skipped, 3 passed)

Could this error messages be any more helpful? I doubt it!

9

Since the last executing told us that "Title" could not be seen on the page, we will do the bare minimum code necessary to make it happy. Normally I would suggest that you do it for each term on the page (Title, Author, Synopsis and Rating), but for the sake of succinctness I will do the four on a single step (try not to do things like that if you’re beginning to adopt BDD just now, keep the steps SIMPLE, ridiculously SIMPLE)

Here:

gedit app/controllers/books_controller.rb &

We will change the rendering to:

render :text => "Title Synopsis Author Rating"

Remember: do only the bare minimum so that the current step can be successful.

10

Running the feature again changes the error:

Could not find field: "book[title]" (Webrat::NotFoundError)
(eval):2:in `/^I fill in "([^\"]*)" with "([^\"]*)"$/'
features/register_new_books.feature:12:in `When I fill in "book[title]" with "<book_title>"'

3 scenarios (3 failed)
60 steps (3 failed, 42 skipped, 15 passed)

We’re getting better, 5 steps passed already for each execution of the scenario outline (since we have 3 examples, 15 steps overall).

I guess you’re getting the grasp about what to do next, right? Hence and repeat…

11

Since the current step wants to fill some field on the page, we will most likely not find a way to implement it without creating a template.

We could create a pure HTML template, that would most likely be the best approach here, theoretically speaking - but once again, I have no intent of forcing a purist mentality over other people. You shall choose how much leeway you want to give to your steps, but keep in mind that you should always try your utmost to keep it to the minimum.

Since I’m planning to already use rails helpers to create a form for a model, I will need a model at this point. It is already too much implementation for a single step!

That’s when you can see how BDD per se falls short, lacking on dept on implementation heavy tasks. I mean, your client will most likely care that when he access a link, a page is shown with a given set of characteristics and a given behavior. But certainly he couldn’t care less about what sort of classes, controllers, models or whatever else techie stuff you need to make it happen.

Therefore, here’s a pretty good point to introduce the importance of TDD, so that’s going to be the main entry point of the next article. For now, please just put a bookmark here on your mind and bear with the inadequately huge implementation for this step.

Creating the view:

gedit app/views/books/new.html.erb &

Something like this:

<% form_for @book, :url => { :action => 'create' } do |f| -%>
<% field_set_tag 'New Book' do -%>
<p>
<%= f.label 'title'%><br />
<%= f.text_field 'title'%>
</p>
<p>
<%= f.label 'author'%><br />
<%= f.text_field 'author'%>
</p>
<p>
<%= f.label 'rating'%><br />
<%= f.text_field 'rating'%>
</p>
<p>
<%= f.label 'synopsis'%><br />
<%= f.text_area 'synopsis'%>
</p>
<p>
<%= f.submit 'Save' %>
</p>
<% end -%>
<% end -%>

Creating the model:

script/generate model book title:string author:string rating:decimal synopsis:text
rake db:migrate

Adapting the controller to use the view:

gedit app/controllers/books_controller.rb &

It will probably look like this now:

def new
@book = Book.new
end

12

Running the feature changes the error:

No action responded to create. Actions: new (ActionController::UnknownAction)
/usr/lib/ruby/1.8/benchmark.rb:308:in `realtime'
(eval):2:in `/^I press "([^\"]*)"$/'
features/register_new_books.feature:16:in `And I press "Save"'

3 scenarios (3 failed)
48 steps (3 failed, 18 skipped, 27 passed)

As you can see, the data input was successful - now we are stuck to the save button functionality, it redirects to the create action, which doesn’t exist as of now. So, you know what to do.

13

Adapting the controller:

gedit app/controllers/books_controller.rb &

With the simplest implementation possible to make the step pass:

def create
render :text => ""
end

Running the feature again should give an error about the word "successfully" not being present on the screen after clicking save.

So, let’s fix it:

def create
render :text => "successfully"
end

14

Running the feature again tells us that we don’t know yet what it means to the book to be on the database:

TODO (Cucumber::Pending)
features/register_new_books.feature:22:in `And the book with the title "<book_title>" should have been saved on the database'

3 scenarios (3 pending)
48 steps (3 pending, 45 passed)

15

So, we need to tell it, how to check if the book was saved on the database:

gedit features/step_definitions/book_steps.rb &

Here we will define a step, like this:

Then /the book with the title "([^\"]*)" should have been saved on the database/ do |book_title|
Book.find_by_title(book_title).should_not be_nil
end

The code above should be self explanatory by just reading it out loud, we will get into more details about this on the next post.

The regular expression will be matched against the steps on your feature description. Also, to make the assertion I’m using the rspec synthax - don’t worry about it right now, I’ll get into more details about it on the next article, since it is more directed to the TDD practice.

Here I shall give some more "do what I tell you, not what my shown code does" kind of talk: It is quite questionable to put such low level assertion on a feature. Your client might not even know what a database is. And the feature’s intent is to be readable and discussable by him too. So, there would probably be a lot better to go to the book listing page and check if your book is there as an assertion that it has been saved indeed.

Why I didn’t do that? Because I’m lazy and didn’t want to implement the listing page, and the few more steps needed to make the assertion - but that can be considered mandatory homework for you ;).

16

Now that cucumber knows what it means to check if the book has been saved to the database, when we run the feature again we get:

expected nil? to return false, got true (Spec::Expectations::ExpectationNotMetError)
features/register_new_books.feature:22:in `And the book with the title "<book_title>" should have been saved on the database'

3 scenarios (3 failed)
48 steps (3 failed, 45 passed)

That means, the book is not on the database.

17

So, what is left is to change the create action to actually do something:

gedit app/controllers/books_controller.rb &

Something like this will do the trick:

def create
Book.create(params[:book])
render :text => "successfully"
end

I know, you should check for errors and all that stuff - just don’t focus on the implementation I’m giving you (which is as succinct as possible, so that it fits better to be put inline here on codevil), but on the bigger picture concept behind it all.

18

Now, what is left is run the feature one last time, and rejoice:

Feature: Register new books
  In order to make my bookstore expansible
  As a bookstore owner
  I want to be able to register new books

  Scenario Outline: User register a new book successfully
    Given I am on the new book register page
    Then I should see "Title"
    And I should see "Author"
    And I should see "Rating"
    And I should see "Synopsis"
    When I fill in "book[title]" with ""
    And I fill in "book[author]" with ""
    And I fill in "book[rating]" with ""
    And I fill in "book[synopsis]" with ""
    And I press "Save"
    Then I should not see "<book_title>"
    And I should not see "<book_author>"
    And I should not see "<book_rating>"
    And I should not see "<book_synopsis>"
    And I should see "successfully"
    And the book with the title "<book_title>" should have been saved on the database

    Examples:
      | book_title       | book_rating | book_synopsis     | book_author         |
      | the god delusion | 9           | too soft          | richard dawkins     |
      | the antichrist   | 9           | ubermesnch        | friedrich nietzsche |
      | holy bible       | 0           | childish folklore | charlatans          |

3 scenarios (3 passed)
48 steps (48 passed)

Finally, all steps passing successfully.

END

That concludes this first article, I hope it has been of use to you guys beginning to do automated tests. As usual, you can download the full code used in this example at my github.

Please leave your impressions on the comments section.

Recommended [very short] reading: http://dannorth.net/introducing-bdd

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

5 People have left comments on this post



» stJhimy said: { Jul 30, 2009 - 03:07:37 }

Thanks =)
One of the best tutorials i ever read

» stJhimy said: { Jul 30, 2009 - 04:07:23 }

Just one thing, when i run “rake features” for the first time, ask for the gem “webrat”, just get it and everything will be fine.

» Diogo Kamioka said: { Aug 20, 2009 - 01:08:43 }

Big MAC!
Really nice and ez to follow guide! Just finished my hands-on lesson, following this steps!

Interesting, now as I said to you I finally can try to play with this Rails thing… just one thing…
When i ran the “rake features” i get this message:

*** The ‘features’ task is deprecated. See rake -T cucumber ***

After executing this command i got:

dkamioka@dkamioka-laptop:~/www/bookstore$ rake -T cucumber
(in /home/dkamioka/www/bookstore)
rake cucumber # Alias for cucumber:ok
rake cucumber:all # Run all features
rake cucumber:ok # Run features that should pass
rake cucumber:wip # Run features that are being worked on

Maybe it would be nice to update your post! ;)

Anyway, nice post!!! Next is the TDD :D

» macskeptic said: { Sep 5, 2009 - 03:09:53 }

Interesting, mr. Key…

At the time I’ve written this post, the “rake features” wasn’t deprecated yet. Things really move fast on the rails sphere, don’t they?

Thanks for the heads up =)

{ Jul 26, 2009 - 11:07:17 } TDD - Beyond The Buzzwords | Codevil