Home =>
Articles =>
Poetry Magnets: First Verse
Poetry Magnets: First Verse
Charlie Poole
November, 2005
In a recent thread on the TestFirstDevelopment list, Bill Wake asked for some help
defining an algorithm for reading cards, which are laid out on a table. Even though the cards
are not perfectly lined up, the human eye and mind can perceive alignments. After
a bit of discussion, Bill offered a metaphor to help us understand the problem...
Think of this as a simulation of magnetic poetry on a refrigerator. (If you haven't
seen it, there are a bunch of magnets with words on them, and people arrange them
into messages.) The user drags these rectangles into an arrangement.
Since we aren't too precise, the arrangement looks a little like a ransom note
- you know, where someone has glued phrases from a newspaper. They aren't in a
perfect line, but the human reader has no trouble.
That got me thinking. Even if it's only a metaphor for the real problem, it's a pretty
interesting one, and sounds like fun. Let's see I can solve it using a test-driven
approach, rather than discussing various algorithms in advance...
A Quick Design Session
Right at the start, we should make sure we understand the problem we've decided to solve.
Here's a rewording of the problem, with some additonal specifications
the "Customer" - that's me - has provided...
- Magnets with words on them are placed on a surface. The
magnets are rectangular in shape and are all the same height. Their length
varies depending on their content. They may be placed in any location and
oriented horizontally, vertically or at some other angle. Due to physical
limitations, they may not overlap.
- The program should take a list of magnets, with the location, orientation
and content of each one, and produce a text string that concatenates them
in the order a human would read them. The text should be broken into lines
if the layout "looks like" a set of lines.
My approach will be to do the simplest thing first. In particular, I plan to
define some additional constraints at the start and remove them as I go along.
So initially, I'll limit my tests as follows...
- Magnets are always perfectly horizontal. This allows great simplification
initially. I wonder if it might be a problem later... well, we'll find
out as we go along.
- Magnets on the same line are always perfectly aligned. As a result,
magnets with different Y coordinates will always be in different lines.
I expect this to give us erroneous results eventually, but we'll deal with that
when it happens.
I can forsee needing two objects. A MagneticSurface on which the magnets
are placed and the Magnets themselves. I don't think it would necessarily
be a BadThing to create both of them right away, but - just for fun - let's
start out with only the surface.
That's the end of our design session. It really was quick, wasn't it?
First Test
When I was younger and less experienced, I thought that speed of programming
was simply a matter of style. Some people are faster, others slower. Some take
very small steps, others larger. These days, I've come to understand that speed
of programming and size of steps are a matter of technique. Depending on the
problem at hand, my confidence in understanding both it and the technology
and too many other factors to list, I may need to choose a different
speed at which to work and a different step size for my tests.
In this case, I'm going to program in fairly small steps at first. I could
easily go faster, and when I do the problem again I may. But it's good to warm
up before exercise and I'd like this article to be understandable to readers
who are new to TDD as well as to the veterans. For those who find the pace
too slow, meet me in the next installment, where we'll speed things up a bit.
It's often useful to start with the do-nothing test case and that's what I've done here...
[TestFixture]
public class MagnetTests
{
[Test]
public void NoMagnets()
{
MagneticSurface magnets = new MagneticSurface();
Assert.AreEqual( "", magnets.Text );
}
}
To make this compile, I need to create the MagneticSurface class...
public class MagneticSurface
{
public string Text
{
return null;
}
}
I had to have a Text property to satisfy the compiler. I deliberately
made it return a value - null - such that the test would fail. Here's
the error message...
expected: <"">
but was: <null>
This is easy enough to fix...
public class MagneticSurface
{
public string Text
{
return "";
}
}
The first test passes now. I could have made it pass right away, but it's
reassuring to see the failure - the red bar - followed by passing - the
green bar.
Getting the Word Out
Having handled the case with no magnets on the surface, the next logical
step is to put one there. So let's do that...
[TestFixture]
public class MagnetTests
{
[Test]
public void NoMagnets()
{
MagneticSurface magnets = new MagneticSurface();
Assert.AreEqual( "", magnets.Text );
}
[Test]
public void OneMagnet()
{
MagneticSurface magnets = new MagneticSurface();
magnets.Add( "Hello" );
Assert.AreEqual( "Hello", magnets.Text );
}
}
To make this compile, we have to modify our production class...
public class MagneticSurface
{
public string Text
{
return "";
}
public void Add( string magnet )
{
}
}
Of course, the test fails, since we still are returning the empty string. Failure
is just what we are looking for here. Now we can fix the code and see the bar turn green...
public class MagneticSurface
{
private string text = "";
public string Text
{
return text;
}
public void Add( string magnet )
{
this.text = magnet;
}
}
The tests are passing, but we see some duplication: both tests
create a new MagneticSurface object. We can use a SetUp
method to remove the duplication, while still getting a fresh
object to work with in each test. Let's do that before moving on...
[TestFixture]
public class MagnetTests
{
private MagneticSurface magnets;
[SetUp]
public void BeforeEachTest()
{
magnets = new MagneticSurface();
}
[Test]
public void NoMagnets()
{
MagneticSurface magnets = new MagneticSurface();
Assert.AreEqual( "", magnets.Text );
}
[Test]
public void OneMagnet()
{
MagneticSurface magnets = new MagneticSurface();
magnets.Add( "Hello" );
Assert.AreEqual( "Hello", magnets.Text );
}
}
Two in a Row
The next step that will drive us forward seems to be adding multiple words.
So as not to tempt fate, let's try two...
[TestFixture]
public class MagnetTests
{
...
[Test]
public void TwoMagnets()
{
magnets.Add( "Hello" );
magnets.Add( "World" );
Assert.AreEqual( "Hello World", magnets.Text );
}
}
Of course, we're only saving the last magnet added, so the new test fails
with the message...
expected: <"Hello World">
but was: <"World">
At this point, we have a choice. We can either build up the text as magnets
are added or we can save all the magnets, creating the text only when we
are asked for it. Either way will work for this test and we can always
refactor later. I'm going to use the first approach for now, since
it's the simplest thing to do...
public class MagneticSurface
{
private string text = "";
public string Text
{
return text;
}
public void Add( string magnet )
{
if ( this.text != string.Empty )
this.text += " ";
this.text += magnet;
}
}
All the tests are passing now. We could do tests with three, four or more
magnets, but it doesn't look as if that would teach us anything more about
the code. It would be OK to write those tests, but we'd be doing testing,
not Test-Driven Development.
You're Out of Order!
While we've made some tests pass, we clearly have not yet addressed one
of the major issues of the problem we are trying to solve: figuring out
the best order in which to put words together. So far, we've merely
concatenated the test on the magnets in the order they were added to
the surface. That's far from useless: these basic tests must continue
to pass no matter how we modify our approach. To demonstrate the
problem, we add another test...
[TestFixture]
public class MagnetTests
{
...
[Test]
public void TwoMagnetsInWrongOrder()
{
magnets.Add( "World" );
magnets.Add( "Hello" );
Assert.AreEqual( "Hello World", magnets.Text );
}
}
This one fails, as expected. What's more, we can't make it pass in
any useful way without breaking the earler TwoMagnets test.
Oh sure, we could do them in alphabetic order, but that doesn't
seem like a fruitful path to follow.
The problem is that we aren't providing enough information. Magnets
are supposed to have a location as well as text content. Using a
string to represent the magnet won't take us much further. In
addition, our decision to build up the text as magnets are added
appears to limit our options at this point.
It's good to take a break on a red bar. That way, when you return
to the program, you know exactly what needs to be fixed next. So
lets end this episode right here. But first, let's have a...
Retrospective
I like to end each verse... uhh, I mean iteration... with a
retrospective. What did we do? What went well? What problems
arose? What would we do differently next time?
In this case, we started very simply. We postponed creating
an object to represent the magnets and used a string instead.
We took this approach as far as we could and we'll clearly
need to refactor before we can move ahead with our tests.
Is this a problem? Should we have thought ahead? Time will tell,
but I don't think so. None of this took long to do and even if
we erased it all and started over, what we learned in the process
would stay with us and make the rewrite easier.
That said, I don't think we'll need to do anything quite so drastic. Next
time, we'll take the code as it is and try to take the
location of the magnets into account.
|