Tales about Aviation, Coaching, Farming, Software Development

Cucumber: When Programmers Have a Dream

Sometimes software developers have a dream. They fantasize about working closely together with the real customer instead of being just one wheel in the big machine that produces custom software. So far this has been difficult to achieve.

Traditionally testing in software development has been an activity that happened after the fact. People do it at the end of a project (very bad), at the end of a project phase (bad), at the end of an iteration (still bad) or at the end of a day spent writing production code which is as bad as any other variation just mentioned. Because the need for good testing is not debated, over time a lot of sophisticated testing tools have been developed and sold for good money to customers who want to improve the quality of their software. Because of the importance of testing we find testers on every larger team in any organization that is serious about software development. And to make testers more effective management buys them these sophisticated tools.

A tester in many organizations is a person who is prevented from writing code. At times intentionally because some deem it valuable that a tester does not know nor understands the inner workings of the application he is testing. He should explore it instead from the outside and think as an end-user (the customer) would.

Often these skilled individuals are turned into human robots operating the test tool management has chosen for them. There they execute test scripts against software that appears on their computer screens after business analysts figured out precisely what the needs for the business are and programmers have written production code that exactly meets the specifications written in accurate language in a larger number of Word documents - at least one for each feature of the application.

I think that we should not waste the skills of testers. Testers are pretty good at understanding business requirements and as they don't think about how to implement something they can be our constant reminder and guarding of the what. So instead of using their skills at the very end - when it is usually to late - we should use their skills as early as possible. In this post I'd like to show how we can achieve test first using Cucumber for a web application. The technique is not restricted to web applications but can work with any type of application.

Cucumber is a testing tool that allows to describe behavior of an application in natural language and in a way that business people, testers and programmers can understand. Features can be described in plain English and many other languages spoken by people around the world. A typical feature description looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Feature: As photographer I want to take a picture of the elves lined up and
  ordered by height

  Scenario: Elves lined up side by side
    Given I am in the forest
    And the elves are not lined up
    When I call the following elves to line up:
    | elf    | height |
    | Kadra  | 6' 20" |
    | Kaslin | 6' 35" |
    | Savah  | 5' 15" |
    Then they line up in the following order:
    | Kaslin |
    | Kadra  |
    | Savah  |

Wouldn't it be nice to use this feature description as the starting point for development? There is not yet any application. Any code has been written yet. All there is is a text file - let's call it elf_photograph.feature - and nothing else.

After all we are talking to a photographer in this example and she will not really understand what we as programmers are babbling about, if we were talking about HTML, Java, Ruby, application servers and databases. All our babbling may at the very least be seen as distracting. If worse comes to worst, the business person might even believe we are talking about these things on purpose to hide something and leave with the impression that we don't know how to solve her problem. So we better don't talk like that at all.

After we are done talking to the photographer we take the requirements captured in feature files back to the office and sit down with one of our testers to write Cucumber step definitions. Here is what we come up with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Given /^I am in the forest$/ do
  @current_page = ForestPage::open @browser
end

When /^I call the following elves to line up:$/ do |elves|
  @current_page.elves_line_up elves.hashes
end

Then /^they line up in the following order:$/ do |order_of_elves|
  order_of_elves.diff!(@current_page.order_of_elves)
end

When /^the elves are not lined up$/ do
  @current_page.elves_are_not_there
end

Remember that our goal is to write the tests before we actually start writing production code or create web pages. So we have to make up a few things just to get us going. Just as in classic TDD we can run our code now and - obviously - we will be reminded that we don't have a ForestPage object neither the other methods we are calling. Let's create as much as needed to get a bit further:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ForestPage

  def initialize(browser_to_be_used)
    @browser = browser_to_be_used
  end

  def self.open(browser)
    page = ForestPage.new(browser)
#    browser.goto FOREST_PAGE

    return page
  end

  def method_missing(m, *args)
    puts "Sorry, I (#{self}) don't know about any #{m} method."
  end
end

With that in place we can run again and will now see:

Testing started at 8:07 PM ...
Sorry, I (#) don't know about any elves_are_not_there method.
Sorry, I (#) don't know about any elves_line_up method.
Sorry, I (#) don't know about any order_of_elves method.

NoMethodError: undefined method `[]' for nil:NilClass
./features/step_definitions/elf_steps.rb:12:in `/^they line up in the following order:$/'

The Cucumber test runner in RubyMine shows up that we were able to execute successfully 3 out of 4 steps:

Screen shot 2010-10-17 at 8.10.08 PM.png

Great! But not so fast ... Our steps pass because we are accepting all messages being sent to the ForestPage class. We need to fix that later but for now this allows us to focus on last step which is:

1
2
3
Then /^they line up in the following order:$/ do |order_of_elves|
  order_of_elves.diff!(@current_page.order_of_elves)
end

This is the assertion in our test where we figure out whether the application is behaving correctly or not. We should get this step right before we start writing the production code for the application.

We have a table with all the elves ordered by height as input and we compare that table to what we read from the page using @current_page.order_of_elves. Let's create this method:

1
2
3
4
  def order_of_elves
    elves = []
    elves.push ['Sara']
  end

and run our feature again:

Screen shot 2010-10-17 at 8.25.00 PM.png

We still fail at the same place but this time we fail because the two tables are not identical. So our step is working in general and we know what to return from the method @current_page.order_of_elves. Now write the rest of the ForestPage class:

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
47
class ForestPage

  def initialize(browser_to_be_used)
    @browser = browser_to_be_used
  end

  def self.open(browser)
    page = ForestPage.new(browser)
    browser.goto "file:///Users/sns/elf/src/main/resources/org/example/elf/pages/Forest.html"
    return page
  end

  def method_missing(m, *args)
    puts "Sorry, I (#{self}) don't know about any #{m} method."
  end

  def elves_are_not_there
    @browser.link(:id, "startOver").click
  end

  def elves_line_up(elves)
    elves.each do |elf|
      @browser.text_field(:id, "elfName").set(elf['elf'])
      @browser.text_field(:id, "elfHeight").set(elf['height'])
      @browser.button(:id, "submit").click
    end
  end

  def order_of_elves
    elves = []

    begin
      elf_number = 0
      elf = @browser.div(:id => "elf", :index => elf_number)
      while !elf.nil?
        elves.push [elf.text]

        elf_number = elf_number + 1
        elf = @browser.div(:id => "elf", :index => elf_number)
      end

    rescue
      return elves
    end
  end

end

We also create the Forest.html as our first step to produce something visual:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <title>Forest Page</title>
</head>
<body>

<form>
	<input type="text" id="elfName"/>
	<input type="text" id="elfHeight"/>
	<input type="submit" id="submit"/>
</form>

<div id="elfLineup">
	<div id="elf">
		Elf's name
	</div>
</div>

<a href="#" id="startOver">Start over</a>

</body>
</html>

With that in place we can run our feature again and as before we get

Screen shot 2010-10-17 at 8.10.08 PM.png

At this point we have a failing test and can start writing the production code to make it pass. Of course now is the opportunity to show the HTML page and the feature again to our customer to make sure that we are still on the right track.

Update: The code for this is available on my github