Test First, by Intention

A code and culture translation from the original Smalltalk to Ruby

Original by Ronald Jeffries, translation by Aleksi Niemela and Dave Thomas

In this document we show you the Ruby version of the Smalltalk code published in the Pink book. There's also an online version of the original (PDF 0.5MB, and zipped 1.0MB).

Table of contents

1. Introduction

2. In the Beginning a Test Should Fail

3. Then it should pass

4. The second test

5. Coming up with an Algorithm

6. Refactoring

7. Refactoring II

1. Introduction

Chapter 14 of XP Installed is a blow-by-blow account of Chet and Ron developing a simple Smalltalk program using test-first design.

There has been some discussion that the use of Smalltalk is confusing for some readers, and people have started offering their translations in various languages. Here's a possible Ruby version.

We had a couple of options when performing this translation. We could have written the code using more of Ruby's features, which would have resulted in a more compact program. However, it would then have been difficult to relate this Ruby code back to the Smalltalk original. We therefore took the path of translating the Smalltalk directly into Ruby, a fairly straightforward job as Ruby inherits many of Smalltalk's strengths.

In the descriptions that follow, page numbers reference the corresponding pages in XP Installed, and the numbers in parenthesis are for the online version, which has different numbering.

The code examples for each step in this proces are in two columns. On the left we have the code we start to develop from, and on the right is the code after modification. The part of code which changes or is being added is marked with darker color. The application code is colored in shades of Ruby, and the test code is in green (symbolizing the go color of traffic lights).

2. In the Beginning a Test Should Fail

Page 109 (130)

We `know' we need a class to represent a sum. Ron says ``In Smalltalk, you don't have to define the types of the variables.'' In Ruby, you don't even have to define the variables--they spring into existence when you use them. So, the Ruby code corresponding to the original Smalltalk is pretty simple--a plain class definition.


->
class Sum
end

Page 110 (131)

We'll add initialization code, so that the constructor will set the object's attributes. From within the class we can access the attributes in two ways, directly as instance variables (these start with an 'at' sign) or via the accessor functions: ``self.name = name'' or ``@name = name''.

The former is more like the Smalltalk code, but the latter is more idiomatic in Ruby, so we'll use it. We'll also use parallel assignment, as it shortens the psychological distance between the variables and the parameter list.

class Sum


end
->
class Sum
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

(132)

Then, following the order of the original text, we introduce accessors. Instead of writing the code ourselves (although it's very simple) we reduce the workload and the opportunity for errors by letting Ruby write them for us.

class Sum
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

(133/1)

Now it's time to write the first test. Ron & Chet actually came up with single line creating an emptySummarizer first, but we add the assert code right away.

  def testEmpty
    summarizer = emptySummarizer
  end
->
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end

(133/9)

In RubyUnit it's a common idiom to store the test suite in separate file than the class under test. It's also common to add a couple of lines of driver code to make the test directly executable from the command line. With this code at the bottom of a test file, you can run the tests in the file by executing the file. You can also include the file in a larger body of tests, in which case the driver code would be ignored.

In any event the test case needs a TestCase wrapper.




  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end





->
require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end

First test run

Then we run the test. And it fails.

 TestSummarizer#testEmpty test_summarizer.rb:6:in `testEmpty': undefined local 
variable or method `emptySummarizer' for #<TestSummarizer:0x4017c448> 
(NameError)
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `send'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `run_bare'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testresult.rb:75:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:29:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:15:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `each'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:27:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:20:in `run'
	from test_summarizer.rb:13

3. Then it should pass

(134/1)

Create the basic Summarizer class with two collections:

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end



->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end
class Summarizer
end

(134/2)

So now we can create the emptySummarizer method in TestSummarizer.

require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end


end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end
->
require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end

Second test run (134/1)

And run the tests again:

TestSummarizer#testEmpty E.
Time: 0.0012
FAILURES!!!
Test Results:
 Run: 1/1(0 asserts) Failures: 0 Errors: 1
Errors: 1
 test_summarizer.rb:10:in `initialize': wrong # of arguments(2 for 0) 
(ArgumentError)
	from test_summarizer.rb:10:in `new'
	from test_summarizer.rb:10:in `emptySummarizer'
	from test_summarizer.rb:6:in `testEmpty'
	from test_summarizer.rb:16

(135/1)

The test output indicates we're calling Summarizer's constructor with too many parameters. Well, that could well be the case as we haven't written the constructor yet. Let's create the constructor, and at the same time add the accessors.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer



end

->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
  end
end

New test run

 TestSummarizer#testEmpty test_summarizer.rb:7:in `testEmpty': undefined method 
`summary' for #<Summarizer:0x4017bcb4> (NameError)
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `send'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `run_bare'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testresult.rb:75:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:29:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:15:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `each'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:27:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:20:in `run'
	from test_summarizer.rb:16

(135/2)

It seems that emptySummarizer managed to return the right object, but the last run complained about a method that didn't exist. It's time to introduce it.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
  end


end

->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
  end
  def summary
    exit
  end
end

(135/3)

Let's go with Chet and return the concatenation of the two collections.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
  end
  def summary
    exit
  end
end

->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
  end
  def summary
    @first.concat @second
  end
end

What do our test say?

TestSummarizer#testEmpty .
Time: 0.000496
OK (1/1 tests  1 asserts)

Heh, the test passes. Ship it. ;-)

4. The second test

(136/1)

Let's add another test, based on Ron's table.

require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end



end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end
->
require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    assert_equals(3, summarizer.summary.size)
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end

(137/1)

We need to add the factory method abcSummarizer. We'll do the simplest thing for now, and define it better later.

require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    assert_equals(3, summarizer.summary.size)
  end


end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end
->
require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    assert_equals(3, summarizer.summary.size)
  end
  def abcSummarizer
    Summarizer.new([],[])
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end

(137/2)

We're on a roll! Let's keep the momentum going by adding all the rest of the tests. We know what to expect from the summary so it's all about stating it in code.

Ron & Chet have a slightly strange way of accessing the container from the head, the end, and by indexing it in the middle. Let's follow it. We also create a pronoun variable ``summary'' because we're referring to it a lot.

require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    assert_equals(3, summarizer.summary.size)






  end
  def abcSummarizer
    Summarizer.new([],[])
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end
->
require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    summary = summarizer.summary
    assert(summary.size == 3)
    assert(summary.first.name == 'a')
    assert(summary.first.amount == 11)
    assert(summary[1].name == 'c')
    assert(summary[1].amount == 2)
    assert(summary.last.name == 'b')
    assert(summary.last.amount == 3)
  end
  def abcSummarizer
    Summarizer.new([],[])
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end

A Bonus Test Run (not available in stores)

Actually there was no test run at this point in the book, but let's add one, so we know where we are before continuing. Note that here we follow the original way of asserting by saying ``real_value == expected'' We're going to change that later.

TestSummarizer#testABC F.
TestSummarizer#testEmpty .
Time: 0.001526
FAILURES!!!
Test Results:
 Run: 2/2(2 asserts) Failures: 1 Errors: 0
Failures: 1
 test_summarizer.rb:15:in `testABC': The condition is <false:FalseClass> 
(RUNIT::AssertionFailedError)
	from test_summarizer.rb:30

(137/3)

The test fails as the size of summary (empty container) can't possibly be 3 as expected. Now it's time to redefine abcSummarizer. The collections get built in their own methods.

require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    summary = summarizer.summary
    assert(summary.size == 3)
    assert(summary.first.name == 'a')
    assert(summary.first.amount == 11)
    assert(summary[1].name == 'c')
    assert(summary[1].amount == 2)
    assert(summary.last.name == 'b')
    assert(summary.last.amount == 3)
  end
  def abcSummarizer
    Summarizer.new([],[])
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end
->
require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    summary = summarizer.summary
    assert(summary.size == 3)
    assert(summary.first.name == 'a')
    assert(summary.first.amount == 11)
    assert(summary[1].name == 'c')
    assert(summary[1].amount == 2)
    assert(summary.last.name == 'b')
    assert(summary.last.amount == 3)
  end
  def abcSummarizer
    Summarizer.new(acCollection,abCollection)
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end

(138/1)

Add the collection builders. Here we just return an array (the simplest container) populated with proper Sum objects.

require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    summary = summarizer.summary
    assert(summary.size == 3)
    assert(summary.first.name == 'a')
    assert(summary.first.amount == 11)
    assert(summary[1].name == 'c')
    assert(summary[1].amount == 2)
    assert(summary.last.name == 'b')
    assert(summary.last.amount == 3)
  end
  def abcSummarizer
    Summarizer.new(acCollection,abCollection)
  end





end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end
->
require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    summary = summarizer.summary
    assert(summary.size == 3)
    assert(summary.first.name == 'a')
    assert(summary.first.amount == 11)
    assert(summary[1].name == 'c')
    assert(summary[1].amount == 2)
    assert(summary.last.name == 'b')
    assert(summary.last.amount == 3)
  end
  def abcSummarizer
    Summarizer.new(acCollection,abCollection)
  end
  def acCollection
    [Sum.new('a', 1), Sum.new('c', 2)]
  end
  def abCollection
    [Sum.new('a', 10), Sum.new('b', 3)]
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end

Finally it's time to start testing with our enhanced testing suite.

TestSummarizer#testABC F.
TestSummarizer#testEmpty .
Time: 0.001606
FAILURES!!!
Test Results:
 Run: 2/2(2 asserts) Failures: 1 Errors: 0
Failures: 1
 test_summarizer.rb:15:in `testABC': The condition is <false:FalseClass> 
(RUNIT::AssertionFailedError)
	from test_summarizer.rb:36

The failure is coming from the assertion that summary.size == 3. If we had written the assertions in more Rubyistic way, we would have said assert_equals(3, summary.size) and the error message would have been clearer saying something like "expected: <3> but was: <4>".

But back to the plot. Clearly our algorithm for summarizing is not working right. That's no suprise as we don't yet have any real algorithm...

5. Coming Up With an Algorithm

(139/1)

Time to write the start of a summary method. Here we make our first change from Chet and Ron's code.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
  end
  def summary
    @first.concat @second
  end
end

->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
  end
  def summary
    processFirst
    processSecond
  end
end

(139/2)

Add an instance variable to contain the summary we're producing.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
  end
  def summary
    processFirst
    processSecond
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    processFirst
    processSecond
  end
end

(139/3)

Next they noticed the two different processes were actually same thing. The code takes shape nicely as the name morphs to a passed parameter.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    processFirst
    processSecond
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
end

(139/4)

Describing the process in terms of the collection is easy: process each item of the collection. This uses a Ruby iterator. The code in the braces will be executed once for each member of the collection, passing in successive members.

The next deviation from the original is the naming of the iteration variable. In Ruby, ``each'' is the common name for an iterator method, and we'll pass in each element to a variable called ``item.''

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end


end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
end

Let's see how far we can get with this code.

 TestSummarizer#testABC ./summarizer.rb:19:in `process': undefined method 
`processItem' for #<Summarizer:0x4017ab5c> (NameError)
	from ./summarizer.rb:19:in `each'
	from ./summarizer.rb:19:in `process'
	from ./summarizer.rb:15:in `summary'
	from test_summarizer.rb:14:in `testABC'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `send'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `run_bare'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testresult.rb:75:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:29:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:15:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `each'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:27:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:20:in `run'
	from test_summarizer.rb:36

(140/1)

The test whines on about the missing method processItem. Time to define it: no one likes a whining test.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end

end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
  end
end

(140/2)

Still Chet and Ron don't exactly know how to do it. So they decide to find a matching sum for current sum, and add to it.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
end

Time to test and see what's needed.

 TestSummarizer#testABC ./summarizer.rb:22:in `processItem': undefined method 
`matchingSum' for #<Summarizer:0x4017a9f4> (NameError)
	from ./summarizer.rb:19:in `process'
	from ./summarizer.rb:19:in `each'
	from ./summarizer.rb:19:in `process'
	from ./summarizer.rb:15:in `summary'
	from test_summarizer.rb:14:in `testABC'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `send'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `run_bare'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testresult.rb:75:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:29:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:15:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `each'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:27:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:20:in `run'
	from test_summarizer.rb:36

(141/1)

The test forces us to write matchingSum. We'll write it using Chet's Smalltalk style. ``detect'' is another iterator method: it passes successive values in a collection to the block, stopping when the block returns true. If the block never matches, then detect returns nil. In non-Smalltalk terms, detect tries to find an item from the collection which satisfies the test passed as a block. It returns the element it finds or nil if none was found.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end









end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end

Now we're pretty hopeful, as it looks like we've done all the work. So let's test it.

 TestSummarizer#testABC ./summarizer.rb:22:in `processItem': undefined method 
`add' for #<Sum:0x4017a328> (NameError)
	from ./summarizer.rb:19:in `process'
	from ./summarizer.rb:19:in `each'
	from ./summarizer.rb:19:in `process'
	from ./summarizer.rb:15:in `summary'
	from test_summarizer.rb:14:in `testABC'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `send'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `run_bare'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testresult.rb:75:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:29:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:15:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `each'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:27:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:20:in `run'
	from test_summarizer.rb:36

But, no, it's not working. We're missing the add method used in processItem.

(141/2)

Ruby allows more concise expression for @amount = @amount + sum.amount, so we'll use it.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end


end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end

Now, we're finally ready. Aren't we? Well, the test will tell.

TestSummarizer#testABC F.
TestSummarizer#testEmpty .
Time: 0.00183
FAILURES!!!
Test Results:
 Run: 2/2(2 asserts) Failures: 1 Errors: 0
Failures: 1
 test_summarizer.rb:15:in `testABC': The condition is <false:FalseClass> 
(RUNIT::AssertionFailedError)
	from test_summarizer.rb:36

(141/3)

By debugging the code we saw that we're not returning the right collection from Summarizer#summary. So let's fix it by returning the collection instance variable that summary has been referring all the time.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
    @summary
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end

New try:

TestSummarizer#testABC .
TestSummarizer#testEmpty .
Time: 0.001328
OK (2/2 tests  8 asserts)

And the code passes our test. Everything's perfect. Let's go home!

Except that we might like to continue to refactor our code into better shape.

6. Refactoring

(142/1)

Chet and Ron decide to change non-communicative names into better ones. (We think that ``Chet'' and ``Ron'' are OK, but it's their call...)

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    process(@first)
    process(@second)
    @summary
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    summarize(@first)
    summarize(@second)
    @summary
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end

Then testing tells us if we've forgotten something.

 TestSummarizer#testABC ./summarizer.rb:18:in `summary': undefined method 
`summarize' for #<Summarizer:0x4017a3c8> (NameError)
	from test_summarizer.rb:14:in `testABC'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `send'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `run_bare'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testresult.rb:75:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:29:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:15:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `each'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:27:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:20:in `run'
	from test_summarizer.rb:36

(142/2)

And we have. So let's change the method definition too.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    summarize(@first)
    summarize(@second)
    @summary
  end
  def process(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    summarize(@first)
    summarize(@second)
    @summary
  end
  def summarize(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end

Tests never lie....

TestSummarizer#testABC .
TestSummarizer#testEmpty .
Time: 0.001253
OK (2/2 tests  8 asserts)

Another happy moment.

(142/3)

But let's not stop. There's still that vague word `process' in our code.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    summarize(@first)
    summarize(@second)
    @summary
  end
  def summarize(collection)
    collection.each {|item| processItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    summarize(@first)
    summarize(@second)
    @summary
  end
  def summarize(collection)
    collection.each {|item| summarizeItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end

 TestSummarizer#testABC ./summarizer.rb:23:in `summarize': undefined method 
`summarizeItem' for #<Summarizer:0x4017a3c8> (NameError)
	from ./summarizer.rb:23:in `each'
	from ./summarizer.rb:23:in `summarize'
	from ./summarizer.rb:18:in `summary'
	from test_summarizer.rb:14:in `testABC'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `send'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:35:in `run_bare'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testresult.rb:75:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testcase.rb:29:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:15:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `each'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/testsuite.rb:14:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:27:in `run'
	from /tc/usr/lib/ruby/site_ruby/1.6/runit/cui/testrunner.rb:20:in `run'
	from test_summarizer.rb:36

(142/4)

And the fix.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    summarize(@first)
    summarize(@second)
    @summary
  end
  def summarize(collection)
    collection.each {|item| summarizeItem(item) }
  end
  def processItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    summarize(@first)
    summarize(@second)
    @summary
  end
  def summarize(collection)
    collection.each {|item| summarizeItem(item) }
  end
  def summarizeItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end

Re-test.

TestSummarizer#testABC .
TestSummarizer#testEmpty .
Time: 0.001269
OK (2/2 tests  8 asserts)

Now everything works, and this is where the book stops. However, there's still a tad more refactoring to do.

7. Refactoring II

First, tidy the application code:

  1. Rename @summary to @summaryResult so that it won't get confused with the method Summarizer#summary.
  2. Rename method summarize's parameter sums, as the collection is really collection of sums. Also the iterator variable item gets renamed to sum, as each of collection sums elements is really a sum :).
  3. The ugly ``if !match'' get transformed to ``if not match'', and then quickly to ``unless match''.

class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summary = []
  end
  def summary
    summarize(@first)
    summarize(@second)
    @summary
  end
  def summarize(collection)
    collection.each {|item| summarizeItem(item) }
  end
  def summarizeItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summary.detect do
      |item| item.name == sum.name
    end
    if !match
      match = Sum.new(sum.name, 0)
      @summary << match
    end
    match
  end
end
->
class Sum
  attr_accessor :name, :amount
  def initialize(name, amount)
    @name, @amount = name, amount
  end
  def add(sum)
    @amount += sum.amount
  end
end

class Summarizer
  attr_accessor :first, :second
  def initialize(first, second)
    @first, @second = first, second
    @summaryResult = []
  end
  def summary
    summarize(@first)
    summarize(@second)
    @summaryResult
  end
  def summarize(sums)
    sums.each {|sum| summarizeItem(sum) }
  end
  def summarizeItem(sum)
    matchingSum(sum).add(sum)
  end
  def matchingSum(sum)
    match = @summaryResult.detect do
      |item| item.name == sum.name
    end
    unless match
      match = Sum.new(sum.name, 0)
      @summaryResult << match
    end
    match
  end
end

Tidy the tests, too:

  1. We can use ``assert_equal(1, foo)'' instead of ``assert(foo == 1)'', as it provides much better error messages.
  2. We can index the collections better. In Ruby, a negative index counts from the end of a collection, so we can use -1. At this point the code should be in good-enough shape for the assertions to be stuffed inside a loop should we add more cases. It's a good idea to make larger blocks of repeating code table driven, but here we're just too lazy.

require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    summary = summarizer.summary
    assert(summary.size == 3)
    assert(summary.first.name == 'a')
    assert(summary.first.amount == 11)
    assert(summary[1].name == 'c')
    assert(summary[1].amount == 2)
    assert(summary.last.name == 'b')
    assert(summary.last.amount == 3)
  end
  def abcSummarizer
    Summarizer.new(acCollection,abCollection)
  end
  def acCollection
    [Sum.new('a', 1), Sum.new('c', 2)]
  end
  def abCollection
    [Sum.new('a', 10), Sum.new('b', 3)]
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end
->
require 'runit/testcase'
require 'summarizer'

class TestSummarizer < RUNIT::TestCase
  def testEmpty
    summarizer = emptySummarizer
    assert( summarizer.summary.empty? )
  end
  def emptySummarizer
    Summarizer.new([],[])
  end
  def testABC
    summarizer = abcSummarizer
    summary = summarizer.summary
    assert_equals(3, summary.size)
    assert_equals('a', summary[0].name)
    assert_equals( 11, summary[0].amount)
    assert_equals('c', summary[1].name)
    assert_equals(  2, summary[1].amount)
    assert_equals('b', summary[-1].name)
    assert_equals(  3, summary[-1].amount)
  end
  def abcSummarizer
    Summarizer.new(acCollection,abCollection)
  end
  def acCollection
    [Sum.new('a', 1), Sum.new('c', 2)]
  end
  def abCollection
    [Sum.new('a', 10), Sum.new('b', 3)]
  end
end

if $0 == __FILE__
  require 'runit/cui/testrunner'
  RUNIT::CUI::TestRunner.run(TestSummarizer.suite)
end

The last check

TestSummarizer#testABC .
TestSummarizer#testEmpty .
Time: 0.001288
OK (2/2 tests  8 asserts)

Ok, that's that. Thank you for wandering through this quite long presentation. There wasn't so much to read, but the repeating code made it surprisingly long. The moral of the story? Well, at least you shouldn't repeat code like we did... :)

Dave & Aleksi