Zsolt Fabók

Practical ideas on kanban, lean, scrum, xp, java, programming…

A Step by Step BDD Demonstration with Some Useful Insights

in bdd

According to Stephen Covey, the seventh habit of highly effective people is ”sharpening the saw”. If you have ever been to an agile workshop or conference, you may have already heard this expression. Software craftsmen sharpen their saw at coding dojos where they talk to each other a talk about new things or do Kata exercises together. With Kata exercises a software craftsman trains himself to be better at software development. I’ve found two real reasons why people are doing Kata exercises: first, they want to be able to solve certain situations instinctively, and second, they want to make the right decisions when they work with real code base. The second topic is much more interesting to me, so I decided I’d do a Kata exercise and explain the background of every significant decision I make along the way.

I chose the English numerals exercise that we were doing at Digital Natives, with some minor modifications. The original exercise is very simple: the application shall receive a number, for example 1997, and it shall return that number in written form, i.e. one thousand nine hundred and ninety-seven. We added two things to the original exercise: the application shall be a ruby on rails application, and it shall support numbers from 1 up to 1,000,000,000. Besides the implementation I’m going to put more emphasis on the following topics and practices:

  • how the cucumber -> fail -> rspec -> rspec green -> cucumber green -> refactor cycle works

  • how to implement an algorithm with BDD/TDD

  • how to recognize large steps during development and how to break them down into smaller ones

  • when and how to refactor code and test cases

My first decision is that I will do BDD/TDD as described in the RSpec Book: my development will be driven by cucumber scenarios and I’ll use rspec for filling in the details and describing the algorithm I’m using for converting the numbers. And moreover, I won’t start the application until it is done - I’m curious whether it is possible to build a web application using BDD and never start the application itself.

The Life Before Twenty-one

My first scenario in the numerals.feature:

1
2
3
4
5
Scenario: Display a number below 12 in written form
  Given I am on the main page
   When I enter 10 into my field
    And I press submit
   Then I see "ten" as a result

Usually, I’m quite lazy with the names of the scenarios. I often add an irrelevant name at first, because I tend to spend a lot of time with naming and this “habit” consumes a lot of time which is definitely a waste. I just give a name which more or less describes what I want at the moment. Later, when I have more scenarios I’ll come back and refactor the scenario and add a better name.

I need this scenario so that I can set up the environment and have the skeleton of the whole application.

The main_controller.rb looks like this:

1
2
3
4
5
class MainController < ApplicationController
  def index
    @result = "ten"
  end
end

The matching index.html.erb:

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
</head><body>
  <%= form_tag do %>
    <%= text_field_tag :number, nil, {:class => "number"} %>
    <%= submit_tag "Submit", :class => "submit" %>
  <% end %>

---

  <%= @result %>
</body>
</html>

And the routes.rb:

1
2
3
Numerals::Application.routes.draw do
  root :to => 'main#index
end

This was the smallest step I found reasonable for my application. Now, the cucumber scenario says that it is working and I believe it - remember, I decided not to start the application until it is done or I really-really have to -, so it is time to move forward. According to wikipedia, there are numbers which cannot be put together as a combination of other numbers. From 1 to 20 they follow each other in a nice pattern, so I created a new scenario for them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  Scenario Outline: Display uncomplicated numbers as words
    When I enter <number> into my field
     And I press submit
    Then I see <word> as a result

    Examples:
      | number | word      |
      | 1      |"one"      |
      | 2      |"two"      |
      | 3      |"three"    |
      | 4      |"four"     |
      | 5      |"five"     |
      | 6      |"six"      |
      | 7      |"seven"    |
      | 8      |"eight"    |
      | 9      |"nine"     |
      |10      |"ten"      |
      |11      |"eleven"   |
      |12      |"twelve"   |
      |13      |"thirteen" |
      |14      |"fourteen" |
      |15      |"fifteen"  |
      |16      |"sixteen"  |
      |17      |"seventeen"|
      |18      |"eighteen" |
      |19      |"nineteen" |
      |20      | "twenty"  |</word></number>

As you can see, it covers my very first scenario, so I’ll get rid of that one right after I have a green bar again - my laziness in naming paid off. The implementation follows the simplicity of the scenario:

1
2
3
4
5
6
7
8
9
10
11
class MainController < ApplicationController
  def index
    uncomplicated_numbers = {
      1 => "one", 2 => "two",   3 => "three", 4 => "four", 5 => "five",
      6 => "six", 7 => "seven", 8 => "eight", 9 => "nine", 10 => "ten",
      11 => "eleven",   12 => "twelve",  13 => "thirteen",  14 => "fourteen",
      15 => "fifteen",  16 => "sixteen", 17 => "seventeen", 18 => "eighteen",
      19 => "nineteen", 20 => "twenty" }
    @result = uncomplicated_numbers[params[:number].to_i]
  end
end

Now I have a green bar, and possibly a working application, but I’m asking myself - a new technique I learnt this weekend - what will happen when the params[:number] variable is empty? How can it be empty? The second question is so much better than the first one, because we are doing BDD and the how is much more important than the what. Actually, I can write a scenario for this issue and check out what’s going to happen:

1
2
3
4
5
6
# ...
Scenario:Do not display anything when the user pressed submit without giving a number
  Given I am on the main page
   When I press submit
   Then I do not see any errors just the contents of the main page
# ...

It fails - what a pity -, but after implementing this, it will work:

1
2
3
4
5
6
7
8
9
10
11
12
class MainController < ApplicationController
  def index
   uncomplicated_numbers = {
     # ...
    }
    unless params[:number].blank?
      @result = uncomplicated_numbers[params[:number].to_i]
    else
      @result = ""
    end
  end
end

You may have observed that I haven’t written any rspec test cases so far. There is a reason for that: if I started writing rspec test cases now, I would duplicate the testing functionality, because the cucumber scenarios already cover everything I need, so I’m going to leave them out. When I really need them - like for the algorithm - I’ll start writing them, but until then I’m good with cucumber scenarios.

Twenty-one Changes The Game

Our next number which our application shall support is 21 (twenty-one):

1
2
3
4
5
# ...
Scenario: Convert a number with a hyphen to a word
  When I convert 21 with my converter
  Then I should get "twenty-one"
# ...

I created a separate scenario with a very “descriptive” name again, because this is the first number which is written as a combination of two other numbers. This means that it is time to start with the previously mentioned algorithm. The first rspec test case in the main_controller_spec.rb:

1
2
3
4
5
6
7
8
require 'spec_helper'

describe MainController do
  context "the concatenates algorithm" do
    it "should add an hyphen when the number is between 21 and 99" do
    end
  end
end

After filling out the test case, it looks like this:

1
2
3
4
5
6
# ...
it "should add an hyphen when the number is between 21 and 99" do
  post(:index, { :number => 21 } )
  response.body.should =~ /twenty-one/
end
# ...

There is nothing wrong with that test case, but it does two things:

  • it uses the rails framework for sending the number and returning the result

  • it uses the algorithm for the conversion

I have a cucumber scenario which has already tested the first part, so I don’t see the point in doing the same with rspec. It is a duplication, which I consider waste. Usually, people try to eliminate waste, which implies that the waste had already been there, but now I can avoid creating waste which is much better. At the moment, the only thing I can do is stop working on the new scenario and refactor the existing code so that I have a new structure where the business logic is separated from the rails framework.

The First Refactoring

Unfortunately, I cannot do refactoring now, because I have a red cucumber scenario and a red rspec test case, so I take one step back and make them pending. Now everything is green again, I can start refactoring. When we did the exercise at Digital Natives, we had two rival groups: the first group followed a top-down approach where the team started like I did: implemented the view, filled out the controller and at the end, moved the business logic out of the framework to the lib folder. The other group followed a bottom-up approach: they created the business logic first and built the rails application around it. Now, I need a third approach where I still have the ”top” of the application, meaning that the controllers and views are in place, but I have to move the business logic out of the controller and put it under lib - the ”bottom” - where it belongs anyway.

So I created a new file under lib with the name numerals_converter.rb and moved the controller’s contents - the business logic - there using the move method refactoring technique:

1
2
3
4
5
6
7
8
9
10
11
12
13
class NumeralConverter
  def convert(number)
    uncomplicated_numbers = {
      1 => "one",
      2 => "two",
      # ...
      19 => "nineteen",
      20 => "twenty"
    }

    uncomplicated_numbers[number]
   end
end

The main_controller.rb still has the logic:

1
2
3
4
5
6
7
8
9
10
11
require 'numeral_converter'

class MainController < ApplicationController
  def index
    unless params[:number].blank?
      @result = NumeralConverter.new.convert(params[:number].to_i)
    else
      @result = ""
    end
  end
end

This is much better, but two separate things deserve separate tests. So I refactored the test cases as well and ended up having two features: one for the rails part and one for the business logic.

The numeral_conversion.feature appears (mind that I’m using Background from here on):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Feature: English numeral converter

  Background:
    Given I have a converter

  Scenario Outline: convert uncomplicated numbers to words
    When I convert <number> with my converter
    Then I should get <word>

    Examples:
       | number | word      |
       # ...
       |19      |"nineteen" |
       |20      | "twenty"  |

 Scenario: Convert a number with hyphen to a word
   pending
   When I convert 21 with my converter
   Then I should get "twenty-one"</word></number>

And the user_interface.feature:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Feature: Provide a simple form for the user
         so that she can use the converter

  Background:
    Given I am on the main page

  Scenario: Display a number as a word
    Given I am on the main page
     When I enter 10 into my field
      And I press submit
     Then I see it as a word in the result

  Scenario: Do not display anything when the user pressed
            submit without giving a number
    Given I am on the main page
     When I press submit
     Then I do not see any errors just the contents of the main page

  Scenario: Do not display anything when the user gave something else
            than a number
    Given I am on the main page
     When I enter something else than a number into my field
      And I press submit
     Then I do not see any errors just the contents of the main page

There are slight changes in the names of the scenarios and steps but basically nothing has changed. If you closely check the user_interface.feature, you may find a new scenario which tests what happens when the user enters a non-number.

Leaving the Path

Adding that scenario doesn’t seem wise, because I had a clear goal which was finishing the refactoring. The green test cases suggest that everything works, but if I forget to take care of this important step, I’ll be doomed later. Actually, I think it is a good practice to leave the path when you find something to take care of, if that means this only one thing, and that thing does not lead to another. I leave the scenario there, and implement the necessary parts in the controller:

1
2
3
4
5
6
7
8
9
class MainController < ApplicationController
  def index
    unless params[:number].blank? || params[:number].is_a?(Numeric)
      @result = NumeralConverter.new.convert(params[:number].to_i)
    else
      @result = ""
    end
  end
end

Now everything works again, it was only one small detour, and I don’t think I’ll have to touch the controller again.

The Devil is in the Details

Back to the numerals_convertion.feature and the twenty-one scenario. Let’s remove the pending and write the rspec test case - mind that it moved to the numeral_converter_spec.rb:

1
2
3
4
5
6
# ...
it "should add an hyphen when the number is between 21 and 99" do
  converter = NumeralConverter.new
  converter.convert(21).should == "twenty-one"
end
# ...

Something is still wrong. When I tried to write the matching code snippet, I had to start writing if’s and creating new methods, but nothing really justified these actions. The devil is in the details as my former colleague used to say: the test case above tests three things, not one. The word twenty-one consists of three different things:

  • a number (twenty)

  • a hyphen (-)

  • another number (one)

So the algorithm shall find the leading number first, add a hyphen, and finally add the closing number. These are three different steps, meaning three different test cases. I deleted the test case which started all the refactoring and added this one to the numeral_conversion_rspec.rb:

1
2
3
4
5
6
7
8
9
describe NumeralConverter do
 context "in case the number is between 21 and 99" do
   subject {NumeralConverter.new}
   it "should find the proper leading uncomplicated number" do
     converter = NumeralConverter.new
     converter.convert(21).should =~ /^twenty/
   end
  end
end

As you can see, the test case only checks how the converted number should start - the first step in the algorithm. The rest is irrelevant from the point of view of this test case. Testing that the result ends with -one is the duty of the other test cases!

In the following form the numeral_conversion.rb turns the new rspec test case green:

1
2
3
4
5
6
7
8
9
10
11
12
class NumeralConverter
    @@uncomplicated_numbers = {
     1 => "one",
     2 => "two",
     # ...
    19 => "nineteen",
    20 => "twenty"
  }

  def convert(number)
    "#{@@uncomplicated_numbers[number - last_number = number % 10]}"
  end

The rspec is green, but what about the cucumber scenarios?

1
2
3
4
5
6
7
8
9
10
~/Code/ruby/dina-coding-dojo/numerals % rspec spec --format=progress
.
Finished in 2.04 seconds
1 example, 0 failures, 0 pending
~/Code/ruby/dina-coding-dojo/numerals % cucumber --format=progress
.--.F.F.F.F.F.F.F.F.F...F.F.F.F.F.F.F.F.F....F...............
24 scenarios (19 failed, 5 passed)
77 steps (19 failed, 58 passed)
0m0.383s
~/Code/ruby/dina-coding-dojo/numerals %

So my application no longer works, the cucumber is red. It seems that I have to take another detour. I’m not happy. In the latest version of the numeral_conversion.rb, I had to differentiate between the incoming numbers and apply the algorithm only if the input number needed to be assembled. I called these numbers complicated, so those numbers which can “stay” alone are uncomplicated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class NumeralConverter
  @@uncomplicated_numbers = {
    # ...
  }

  def convert(number)
    if uncomplicated_number?(number)
      @@uncomplicated_numbers[number]
    else
     "#{@@uncomplicated_numbers[number - last_number = number % 10]}"
   end
  end

  private
  def uncomplicated_number?(number)
    @@uncomplicated_numbers.keys.include?(number)
  end
end

Now the “build is back to normal”. The rspec is green and I have only one failing cucumber scenario which started the twenty-one saga:

1
2
3
4
5
6
7
8
9
10
~/Code/ruby/dina-coding-dojo/numerals % cucumber --format=progress
.--..........................................F...............

expected "twenty-one"
     got "twenty"

24 scenarios (1 failed, 23 passed)
77 steps (1 failed, 76 passed)
0m0.380s
~/Code/ruby/dina-coding-dojo/numerals %

Back to the rspec test cases. The next one:

1
2
3
4
5
6
# ...
it "should add a hyphen after the leading uncomplicated number" do
  converter = NumeralConverter.new
  converter.convert(21).should =~ /^[a-z]+-/
end
# ...

As you can see, this test case does not care about how the number starts. The only thing that matters is that after letters there is a hyphen.

The code:

1
2
3
4
5
6
7
8
9
# ...
def convert(number)
  if uncomplicated_number?(number)
    @@uncomplicated_numbers[number]
  else
    "#{@@uncomplicated_numbers[number - last_number = number % 10]}-"
  end
end
# ...

Mind the ‘-’ at the end of line 6!

And finally the whole context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
describe NumeralConverter do
 context "in case the number is between 21 and 99" do
   subject {NumeralConverter.new}
   it "should find the proper leading uncomplicated number" do
     subject.convert(21).should =~ /^twenty/
   end

   it "should add a hyphen after the leading uncomplicated number" do
     subject.convert(21).should =~ /^[a-z]+-/
   end

   it "should concatenates the closing uncomplicated number after the hyphen" do
     subject.convert(21).should =~ /^[a-z]+-one$/
   end
 end
end

And the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class NumeralConverter
  @@uncomplicated_numbers = {
    # ...
  }
  def convert(number)
    if uncomplicated_number?(number)
      @@uncomplicated_numbers[number]
    else
      last_number = number % 10
      "#{@@uncomplicated_numbers[number - last_number]}-
         #{@@uncomplicated_numbers[last_number]}"
    end
  end
end

I’m happy to announce that we are all green (cucumber + rspec)!

Retrospective

A lot of things have happened since the “twenty-one scenario” appeared on the horizon. Let’s summarize the most important ones:

  • Always follow the BDD cycle proposed in the RSpec book

  • Run all the test frameworks you are using

  • If you add several lines to your code your test case can most probably be broken down into smaller steps

  • Don’t be afraid to leave your path, but don’t stray too far away from it - I recommend one ”detour step

  • Always refactor your test cases

  • Don’t create waste

Continue with the Remaining Uncomplicated Numbers

I see two options now:

  • I can jump to the next challenge and implement the logic for 101, or

  • I can cover all the numbers between 21 and 99

I need some rest, so I’ll go with the second one. It looks easier. I simply add the remaining uncomplicated numbers between 21 and 99 to the proper scenario and that’s it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  Scenario Outline: convert uncomplicated numbers to words
    When I convert <number> with my converter
    Then I should get <word>

    Examples:
      | number | word      |
      | 1      |"one"      |
      | 2      |"two"      |
      # ...
      |19      |"nineteen" |
      |20      | "twenty"  |
      |30      | "thirty"  |
      |40      | "forty"   |
      |50      | "fifty"   |
      |60      | "sixty"   |
      |70      | "seventy" |
      |80      | "eighty"  |
      |90      | "ninety"  |</word></number>

You can imagine the rest.

Dealing with the Hundreds

100 is a uncomplicated number so I’ll just add it to the first scenario like I did with 30. Learning from the previous mistakes, I’ll take very small steps. A number between 101 and 999 follows the following structure:

  1. an uncomplicated number which represents the hundreds (no nineteen hundreds!)

  2. then comes an ’and

  3. and the rest should be handled as described above

The scenario looks like this:

1
2
3
4
5
# ...
Scenario: Indicate the hundred between 101 and 999
  When I convert 719 with my converter
  Then I should get "seven hundred nineteen"
# ...

And the first rspec test case:

1
2
3
4
5
context "in case the number is between 101 and 999" do
  it "should start with the uncomplicated number of the hundreds" do
    subject.convert(719).should =~ /^seven hundred/
  end
end

The code:

1
2
3
4
5
6
7
8
9
10
11
12
13
# ...
def convert(number)
  if uncomplicated_number?(number)
    @@uncomplicated_numbers[number]
  elsif number > 100
    "#{@@uncomplicated_numbers[number / 100]} hundred"
  else
    last_number = number % 10
    "#{@@uncomplicated_numbers[number - last_number]}-
       #{@@uncomplicated_numbers[last_number]}"
  end
end
# ...

The rspec is green, but I had a problem before: although the rspec was green, the cucumber was red:

1
2
3
4
5
6
7
~/Code/ruby/dina-coding-dojo/numerals % cucumber --format=progress

.--.............................................................F...............
(::) failed steps (::)

expected "seven hundred and nineteen"
     got "seven hundred"

Nice, I’m getting closer. The next rspec test case puts ’and’ into the picture:

1
2
3
4
5
# ...
it "should add 'and' after the hundreds" do
  subject.convert(719).should =~ /^[a-z]+ hundred and/
end
# ...

No surprises there or in the code (mind the ‘and’ at the end):

1
2
3
4
5
# ...
elsif number > 100
  "#{@@uncomplicated_numbers[number / 100]} hundred and"
else
# ...

Finally, the last test case:

1
2
3
4
5
# ...
it "should concatenates the rest as described before" do
  subject.convert(719).should =~ /^[a-z]+ hundred and nineteen$/
end
# ...

The matching code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ...
def convert(number)
  if uncomplicated_number?(number)
    @@uncomplicated_numbers[number]
  elsif number > 100
    rest = number % 100
    "#{@@uncomplicated_numbers[number / 100]} hundred and #{convert(rest)}"
  else
    last_number = number % 10
    "#{@@uncomplicated_numbers[number - last_number]}-
       #{@@uncomplicated_numbers[last_number]}"
  end
end
# ...

Mind that the new code does exactly what the rspec test case states!

What happens if I want to convert 200?

1
2
3
4
5
# ...
Scenario: Convert 200 to two hundred
  When I convert 200 with my converter
  Then I should get "two hundred"
# ...
1
2
3
4
5
6
7
~/Code/ruby/dina-coding-dojo/numerals % cucumber --format=progress
.--................................................................F...............

(::) failed steps (::)

expected "two hundred"
     got "two hundred and -"

Oh-oh, something is wrong. There is a clause I forgot, let’s make it right:

1
2
3
4
5
# ...
it "should not concatenate anything if there are no tens or ones" do
  subject.convert(200).should =~ /hundred$/
end
# ...

And the code:

1
2
3
4
5
6
7
8
# ...
elsif number > 100
   rest = number % 100
   result = "#{@@uncomplicated_numbers[number / 100]} hundred"
   result += " and #{convert(rest)}" if rest > 0
   result
 else
# ...

Matching the Scenarios and the Contexts

We are all green now. Before going any further, let’s have a look at the scenario and context names - the range they cover is given in parentheses:

scenarios:

  • “Convert uncomplicated numbers to words” (1, 2, … 50, … 100)

  • “Convert a number with hyphen to a word” (21 -> 99)

  • “Indicate the hundred between 101 and 999” (101 -> 999)

  • “Convert 200 to two hundred” (200)

contexts:

  • none (1, 2, …, 50, … 100)

  • “in case the number is between 21 and 99” (21 -> 99)

  • “in case the number is between 101 and 999” (101 -> 999)

Quite close, but this is not really maintainable. Later on I’ll want to be able to tell which rspec test cases belong to a certain scenario. I can live with the fact that I have no rspec for the uncomplicated numbers, because the cucumber outline covered them very nicely, and I don’t need a test duplication, but the names I see above do not meet my standards. Hence, the new scenario names are:

  • “Convert a complicated number between 21 and 99”

  • “Convert a complicated number with tens and/or ones between 101 and 999”

  • “Convert a complicated number without tens and/or ones between 101 and 999”

Let’s talk a bit about duplication. Every rspec test case I wrote so far is duplication, but I’ll keep them anyway, because they tell me how the current algorithm works. If I decide to go in a different direction, I’ll use the existing cucumber scenarios, but different rspec test cases. And nevertheless, they show the progress in my example.

Above Thousands

The algorithm seems a bit simpler above 1000, because there are recognizable groups (marked with ‘,’ and ‘|’):

  • 1,997: one thousand | nine hundred and ninety-seven

  • 10,715,302,842: ten billion | seven hundred and fifteen million | three hundred and two thousand | eight hundred and fourty-two

So the steps of the algorithm:

  1. find a group (for example the thousands)

  2. apply the existing logic on this group

  3. continue with the next group (for example millions)

The next scenario:

1
2
3
4
5
# ...
Scenario: Convert a complicated number above 1001
  When I convert 1997 with my converter
  Then I should get "one thousand nine hundred and ninety-seven"
# ...

The execution result:

1
2
3
4
5
6
7
~/Code/ruby/dina-coding-dojo/numerals % cucumber --format=progress
.--.................................................................F...............

(::) failed steps (::)

expected "one thousand nine hundred and ninety-seven"
     got "nineteen hundred and ninety-seven"

Close enough. The next rspec context:

1
2
3
4
5
context "in case the number is above 1001" do
  it "should group the parts of the number along the thousands" do
    subject.convert(1997).should =~ /^one thousand/
  end
end

It’s enough to test that the result will start with “one thousand” because the other test cases will guarantee that the rest of the name is converted properly.

With the following code I’m all green:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ...
def convert(number)
  if uncomplicated_number?(number)
    @@uncomplicated_numbers[number]
  elsif number > 1000
    rest = number % 1000
    result = "#{@@uncomplicated_numbers[number / 1000]} thousand"
    result += " #{convert(rest)}" if rest > 0
  elsif number > 100
    rest = number % 100
    result = "#{@@uncomplicated_numbers[number / 100]} hundred"
    result += " and #{convert(rest)}" if rest > 0
    result
  else
    last_number = number % 10
    "#{@@uncomplicated_numbers[number - last_number]}-
     #{@@uncomplicated_numbers[last_number]}"
  end
end
# ...

It gets uglier, but I have a bigger problem: the scenarios aren’t maintainable any-more, so I’ll refactor them before continuing the coding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Feature: English numeral converter

  Background:
    Given I have a converter

  Scenario Outline: Convert numbers below 20
    When I convert <number> with my converter
    Then I should get <name>

    Examples:
      | number | name      |
      |  1     |"one"      |
      |  2     |"two"      |
      ...
      | 19     |"nineteen" |
      | 20     | "twenty"  |

  Scenario Outline: Convert numbers between 21 and 99
    When I convert <number> with my converter
    Then I should get <name>

    Examples:
    | number | name          |
    | 21     | "twenty-one"  |
    | 30     | "thirty"      |
    ...
    | 90     | "ninety"      |

  Scenario Outline: Convert numbers between 100 and 999
    When I convert <number> with my converter
    Then I should get <name>

    Examples:
      | number   | name                        |
      | 100      | "hundred"                   |
      | 200      | "two hundred"               |
      | 719      | "seven hundred and nineteen"|

  Scenario Outline: Convert numbers between 1000 and 1000000
    When I convert <number> with my converter
    Then I should get <name>

    Examples:
      | number    | name                                        |
      | 1000      | "thousand"                                  |
      | 1997      | "one thousand nine hundred and ninety-seven"|</name></number></name></number></name></number></name></number>

What has changed:

  • No more “uncomplicated” attribute  in the feature file - I didn’t like it at all

  • Better grouping and better overview

  • Easier to extend

I’ll postpone the refactoring of the class until the scenario is finished. Let’s add the next example:

1
2
3
4
5
6
7
# ...
Examples:
  | number    | name                                             |
  |  1000     | "thousand"                                       |
  |  1997     | "one thousand nine hundred and ninety-seven"     |
  | 19197     | "nineteen thousand one hundred and ninety-seven" |
# ...

It is still green, add the next one:

1
2
3
4
5
6
7
8
# ...
Examples:
  | number    | name                                                           |
  |   1000    | "thousand"                                                     |
  |   1997    | "one thousand nine hundred and ninety-seven"                   |
  |  19197    | "nineteen thousand one hundred and ninety-seven"               |
  | 403197    | "four hundred and three thousand one hundred and ninety-seven" |
# ...

Now it fails:

1
2
3
4
5
6
7
8
~/Code/ruby/dina-coding-dojo/numerals % cucumber --format=progress

.--........................................-....F...............

(::) failed steps (::)

expected "four hundred and three thousand one hundred and ninety-seven"
     got " thousand one hundred and ninety-seven"

Good, it is red indeed. The handling of the hundred thousands is not working. The problem is that I didn’t apply the existing algorithm on the thousands. Let’s make it right:

1
2
3
4
5
# ...
it "should apply the conversion logic on the thousands group" do
  subject.convert(403197).should =~ /^four hundred and three/
end
# ...

The code gets somewhat cleaner, but only a little bit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ...
def convert(number)
  if uncomplicated_number?(number)
    @@uncomplicated_numbers[number]
  elsif number > 1000
    rest = number % 1000
    result = "#{convert(number / 1000)} thousand"
    result += " #{convert(rest)}" if rest > 0
  elsif number > 100
    rest = number % 100
    result = "#{@@uncomplicated_numbers[number / 100]} hundred"
    result += " and #{convert(rest)}" if rest > 0
    result
  else
    last_number = number % 10
    "#{@@uncomplicated_numbers[number - last_number]}-
       #{@@uncomplicated_numbers[last_number]}"
  end
end
# ...

Now the project is green again.

The Last Scenario

The last thing I have to take care of is the repetition of the ”groups”. Let’s add the last scenario:

1
2
3
4
5
6
7
8
9
# ...
Scenario Outline: Convert numbers between 1000 and 1000000
  When I convert <number> with my converter
  Then I should get <name>

  Examples:
    | number   | name       |
    | 1000000  | "million"  |
# ...</name></number>

After adding 1,000,000 to the list of number in the NumberConverter class, the scenario above turns green. Let’s add the next candidate:

1
2
3
4
5
6
7
8
9
10
# ...
Scenario Outline: Convert numbers between 1000 and 1000000
  When I convert <number> with my converter
  Then I should get <name>

  Examples:
    | number   | name          |
    | 1000000  | "million"     |
    | 10000000 | "ten million" |
# ...</name></number>

It fails of course. The next rspec stays in the last context, because it belongs there based on the algorithm (the steps again: find a group, apply the existing logic and get the next group):

1
2
3
4
5
6
7
context "in case the number is above 1001" do
  # ...

  it "should repeat the conversion on the millions and billions etc" do
    subject.convert(10001001).should =~ /^ten million one thousand one/
  end
end

It was a bit harder, but the following code produces an all green result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ...
def convert(number)
  if uncomplicated_number?(number)
    @@uncomplicated_numbers[number]
  elsif number > 1000
    group_index = (Math.log10(number) / 3).to_i
    second_part_as_uncomplicated = (10 ** (group_index * 3))
    the_first_part_of_the_number = number / second_part_as_uncomplicated
    rest = number % second_part_as_uncomplicated

    result = "#{convert(the_first_part_of_the_number)}
        #{@@uncomplicated_numbers[second_part_as_uncomplicated]}"
    result += " #{convert(rest)}" if rest > 0
    result
  elsif number > 100
    rest = number % 100
    result = "#{@@uncomplicated_numbers[number / 100]} hundred"
    result += " and #{convert(rest)}" if rest > 0
    result
  else
    last_number = number % 10
    "#{@@uncomplicated_numbers[number - last_number]}-
       #{@@uncomplicated_numbers[last_number]}"
  end
end
# ...

It’s Alive!

The moment I was waiting for:

And the result:

Thanks to one of my first decisions that I wouldn’t start the rails application, there is one thing left I have to do. Have a look at the second screenshot. In case of larger numbers the user may want to see the original number so that she can compare the results. This means a change in an existing scenario in user_interface.feature:

1
2
3
4
5
6
7
8
# ...
Scenario: Display a number as a word
  Given I am on the main page
   When I enter 10 into my field
    And I press submit
   Then I see it as a name in the result
    And I see the original number as well
# ...

After creating the proper steps and changing the controller and the view, the user can see the result:

The change in the controller:

1
2
3
4
5
6
7
8
9
10
class MainController < ApplicationController
  def index
    unless params[:number].blank? || params[:number].is_a?(Numeric)
      @result = NumeralConverter.new.convert(params[:number].to_i)
    else
      @result = ""
    end
    @original_number = params[:number]
  end
end

The new div in the view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
</head><body>
<%= form_tag do %>
  <%= text_field_tag :number, nil, {:class => "number"} %>
  <%= submit_tag "Submit", :class => "submit" %>
<% end %>

---

<%= @original_number %>:
   <%= @result %>
</body>
</html>

It’s done and ready for site build.

Summary

This is one of my longest posts but if you are reading this line then I hope you found the demonstrated ideas and techniques useful. Here are they again, in case you forgot them:

  • Write a cucumber scenario, an rspec test case, refactor the code, and refactor the test cases

  • Don’t be afraid to delete code or test case

  • Write maintainable code and test cases

    • Structure the scenarios and rspec context properly

    • Don’t create waste unnecessarily

  • Write focused test cases

  • Make adetour” when necessary
  • Ask yourself questions so that you validate that you are on the right path

I have every step in a git repository and I tried to commit at the bigger milestones so that you can compare the different stages of the implementation. The repository is: https://github.com/ZsoltFabok/coding-dojo/tree/bdd_with_cucumber and the code is under the numerals folder:

1
2
3
4
5
6
7
8
9
61dbbd7 Print out the original number after the user pressed submit
f731426 Now the application can handle numbers up to 999999999
3bce615 Now the application can handle numbers up to 999999
0c962bc Now the application can handle numbers up to 999
1e6b856 Covered all the numbers between 21 and 99
8318d4a After the "twenty-one" scenario Deleted some unused test cases
1b87aac Refactored the code so that the UI and the logic is at different locations and tested differently,
af76e34 Added guard for faster development Numbers from 1 till 20 are covered, and finished at 21
b2e0549 First state: one green cucumber test which returns a hard coded result

I hope you find this demonstration useful. Happy coding :-)

Related posts you may find also interesting:

Comments