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.
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
|
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:
- Rename
@summary
to @summaryResult
so that it
won't get confused with the method Summarizer#summary
.
- 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
:).
- 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:
- We can use ``
assert_equal(1, foo)
'' instead of
``assert(foo == 1)
'', as it provides much better error messages.
- 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