Disclaimer: This article does not show all code and possible refactorings. It gives just enough to show how a design for testable printing evolved.Credit for the ideas here go to all members of my team who paired and collaborated on the reporting story.
Let's get started. Of course, that's the problem, isn't it? Where do you start when you don't know where to start? Well, me, I cheat. The XP community over at Yahoo! helped kick the brain into gear with some helpful starters.
So here's what I decided: For now, I'm going to focus on what I know how to test, and maybe that will help me figure out how to test the stuff I don't know how to test. Initial thinking is that I can probably figure out how to test-drive content and presentation, and maybe that will lead me to a testable design for the printing part.
Ok, enough talking, lets see some tests. I'm lazy, I want to test the easiest thing - for me, that's probably the content.
[TestFixture]
public class ContentTest
{
[Test]
public void SeldomReferredToStats(){
Assert.IsTrue(false);
}
}
Red! At least the NUnit framework works. Ok, so I'm going to try to get the content for the very first section of the report.
[TestFixture]
public class ContentTest
{
[Test]
public void SeldomReferredToStats()
{
Content content = new Content();
Assert.AreEqual(670, content.YawnFactor);
}
}
And now the Content class:
public class Content
{
public int YawnFactor { get {return 670;} }
}
Green!
That wasn't too interesting. But to tell you the truth, the Content class was not that exciting. In the real application, it served as a place to gather a whole bunch of different objects that were alive in the system and turn them into content. For now, let's pretend that we wrote a bunch of tests that let us develop a Content class that provides us with all the text for the Seldom Referred-to Stats section. We'll revisit this class in a bit, when it gets more interesting.
Now that I've got this content, I have to do something with it. Looking at the report, it makes sense to divide the content from the way it is presented.
[TestFixture]
public class PresentationTest
{
[Test]
public void Title()
{
Presentation presentation = new Presentation();
Assert.AreEqual("TPS Report", presentation.Title);
}
}
public class Presentation
{
public string Title { get {return "TPS Report";} }
}
Great. I've got a title. Now I need that title underlined and centered. Time to stop coding for a second and think about this. Looking at the report, I can see a title, a bunch of sections, and a footer. And to me, the title and footer are just sections, so maybe the report presentation is just a bunch of sections. Enough talking, let's do some walking.
[TestFixture]
public class SectionTest
{
[Test]
public void TitleSection()
{
Assert.AreEqual("TPS Report", section.Title);
}
}
public class Section
{
public string Title { get {return "TPS Report";} }
}
That passes. And there is duplication, which reminds me that I have to actually use the new class.
[TestFixture]
public class PresentationTest
{
[Test]
public void Title()
{
Presentation presentation = new Presentation();
Assert.AreEqual("TPS Report", presentation.TitleSection.Title);
}
}
public class Presentation
{
private Section titleSection = new Section();
public Section Title { get {return titleSection;} }
}
Looking good. I guess I could do one of two things. I could try to get the underline in there, or I could try to do the next section, because right now the Section class is pretty useless for anything but a title. Underlining interests me more, so I'm going to try that. I don't know how.
[TestFixture]
public class SectionTest
{
[Test]
public void TitleSection()
{
Section section = new Section();
Assert.AreEqual("TPS Report", section.Title);
Assert.AreEqual("-----", section.Line);
}
}
public class Section
{
public string Title { get {return "TPS Report";} }
public string Line { get {return "-----";} }
}
Another pass, update the Presentation test:
[TestFixture]
public class PresentationTest
{
[Test]
public void Title()
{
Presentation presentation = new Presentation();
Assert.AreEqual("TPS Report", presentation.TitleSection.Title);
Assert.AreEqual("-----", presentation.TitleSection.Line);
}
}
And green. If you are looking at this about now, and saying that the presentation test doesn't add much value, think again. It adds a very important piece of value. It says to me that the design is kind of wierd. I'm exposing data that maybe shouldn't be exposed. I'm not happy about that. Also, it tells me that if I keep going down this path without being careful, I may not get what I want. If that sounds kind of non-committal, it is. I don't know what is wrong, but I get the sense, even this early, that something doesn't jive. Let's keep going to see if we can find out what doesnt' work here.
So, maybe now would be a good time to figure out how to print. Not quite yet. One thing that is wrong is that I have some dead code. The Section class isn't employed at the moment, and in my books, that means it either gets used, or it gets tossed. Maybe I should have waited till I needed that class. In this case it did some good to start with it because it got the juices flowing and the confidence up.
Back to the code.
[TestFixture]
public class SectionTest
{
[Test]
public void TitleSection()
{
Section section = new Section();
Assert.AreEqual("TPS Report", section.Title);
Assert.AreEqual("-----", section.Line);
}
[Test]
public void ContentSection()
{
Section section = new Section();
Assert.AreEqual("Section Title", section.Title);
Assert.AreEqual("A bit of content" section.Line);
}
}
Red, of course. We need a way to set the title and content.
[Test]
public void ContentSection()
{
Section section = new Section();
section.Title = "Section Title";
section.Line = "A bit of content";
Assert.AreEqual("Section Title", section.Title);
Assert.AreEqual("A bit of content" section.Line);
}
public class Section
{
private string title;
private string line;
public string Title {
get {return title;}
set {title = value;}
}
public string Line {
get {return line;}
set {line = value;}
}
}
That test passes now, but the TitleSection and Presentation tests fail. One at a time:
public class Presentation
{
private Section titleSection = new Section();
public Presentation()
{
titleSection.Title = "TPS Report";
titleSection.Line = "-----";
}
public Section Title { get {return titleSection;}}
}
Presentation tests are now passing. What about that section test?
[Test]
public void TitleSection()
{
Section section = new Section();
section.Title = "TPS Report";
section.Line = "-----";
Assert.AreEqual("TPS Report", section.Title);
Assert.AreEqual("-----", section.Line);
}
Hmmmm....
[Test]
public void TitleSection()
{
Section section = new Section();
section.Title = "TPS Report";
section.Line = HorizontalLine;
Assert.AreEqual("TPS Report", section.Title);
Assert.AreEqual(HorizontalLine, section.Line);
}
private string HorizontalLine { get {return "-----";} }
Green now, and I have a new idea. There is a difference between the types of lines that are in a section. It seems as though a section can have a horizontal line or a text line. I'm going to put that on the back burner for a second or two. I think that what I really need to do is let a section have more lines.
public class SectionTest
{
...
[Test]
public void ContentSection()
{
Section section = new Section();
section.Title = "Section Title";
section.AddLine("A bit of content");
section.AddLine("A bit more content");
Assert.AreEqual("Section Title", section.Title);
Assert.AreEqual("A bit of content" section.Lines[0]);
Assert.AreEqual("A bit more content" section.Lines[1]);
}
}
public class Section
{
private string title;
private ArrayList lines;
public string Title {
get {return title;}
set {title = value;}
}
public string[] Lines { get {return (string[])lines.ToArray(typeof(string));}
public void AddLine(string line) { lines.Add(line); }
}
Of course, this passes, the other tests fail, we refactor those, and they all pass. Now I feel a little better. But I don't like this code, in the Section test:
section.AddLine(HorizontalLine);
Lets make it this:
section.AddHorizontalLine();
And implement it this way
public class Section
{
...
public void AddHorizontalLine() { AddLine("-----"); }
}
Time for a recap of all the classes:
public class Section
{
private string title;
private ArrayList lines;
public string Title {
get {return title;}
set {title = value;}
}
public string[] Lines { get {return (string[])lines.ToArray(typeof(string));}
public void AddLine(string line) { lines.Add(line); }
public void AddHorizontalLine() {lines.Add("-----") }
}
public class Presentation
{
private Section titleSection = new Section();
public Presentation()
{
titleSection.Title = "TPS Report";
titleSection.AddHorizontalLine();
}
public Section Title { get {return titleSection;} }
}
public class Content
{
public int YawnFactor { get {return 670;} }
...
}
Time to make that Content class a full contributor. Some of my team mates would have axed it already, since it's not being used anywhere but inside of a test.
[TestFixture]
public class PresentationTest
{
[Test]
public void TPSReport()
{
Presentation presentation = new Presentation(new Content());
Assert.AreEqual("TPS Report", presentation.TitleSection.Title);
Assert.AreEqual("-----", presentation.TitleSection.Lines[0]);
Assert.AreEqual("Seldom Referred-to Stats", presentation.SeldomReferredToStatsSection.Title);
Assert.AreEqual("Yawn Factor:\t\t\t\t\t670", presentation.SeldomReferredToStatsSection.Lines[0]);
Assert.AreEqual("Average of a Bunch of Averages:\t\t\t\t\t1", presentation.SeldomReferredToStatsSection.Lines[1]);
Assert.AreEqual("Sleep Quotient\t\t\t\t\tPurple", presentation.SeldomReferredToStatsSection.Lines[2]);
}
}
public class Presentation
{
private Section titleSection = new Section();
private Content content;
public Presentation(Content content)
{
titleSection.Title = "TPS Report";
titleSection.AddHorizontalLine();
this.content = content;
}
public Section Title { get {return titleSection;} }
public Section SeldomReferredToStatsSection
{
get
{
Section section = new Section();
section.Title = "Seldom Referred-to Stats";
section.AddLine("Yawn Factor:\t\t\t\t\t" + content.YawnFactor);
section.AddLine("Average of a Bunch of Averages:\t\t" + content.AverageOfAverages);
section.AddLine("Sleep Quotient\t\t\t\t\t" + content.SleepQuotient);
return section;
}
}
}
That passes. Notice I took out the title test in the Presentation test. It wasn't needed anymore. While doing that last test, I noticed some more refactoring that could be done. That silly titleSection needs to go:
public class Presentation
{
private Content content;
public Presentation(Content content)
{
this.content = content;
}
public Section Title {
{
get
{
Section section = new Section();
section.Title = "TPS Report";
section.AddHorizontalLine();
return section;
}
}
public Section SeldomReferredToStatsSection
{
get
{
Section section = new Section();
section.Title = "Seldom Referred-to Stats";
section.AddLine(content.YawnFactor);
section.AddLine(content.AverageOfAverages);
section.AddLine(content.SleepQuotient);
return section;
}
}
}
That's a little better. But why am I distiguishing between the Title section and the SeldomReferredToStatsSection? It's an iffy call. I could leave it because it is readable. But I have an idea about how to refactor my tests that I can't give up:
public class Presentation
{
private Content content;
public Presentation(Content content)
{
this.content = content;
}
public Section[] Sections {
get{return new Section[] {Title, SeldomReferredToStatsSection};}
}
private Section Title {
get
{
Section section = new Section();
section.Title = "TPS Report";
section.AddHorizontalLine();
return section;
}
}
private Section SeldomReferredToStatsSection{
get
{
Section section = new Section();
section.Title = "Seldom Referred-to Stats";
section.AddLine("Yawn Factor:\t\t\t\t\t" + content.YawnFactor);
section.AddLine("Average of a Bunch of Averages:\t\t" + content.AverageOfAverages);
section.AddLine("Sleep Quotient\t\t\t\t\t" + content.SleepQuotient);
return section;
}
}
}
Now I can do this:
[TestFixture]
public class PresentationTest
{
Section sectionUnderTest;
[Test]
public void TPSReport()
{
Presentation presentation = new Presentation(new Content());
sectionUnderTest = presentation.Sections[0];
CheckTitle("TPS Report");
CheckLine(0, "-----");
sectionUnderTest = presentation.Sections[1];
CheckTitle("Seldom Referred-to Stats");
CheckLine(0, "Yawn Factor:ttttt670");
CheckLine(1, "Average of a Bunch of Averages:tt1");
CheckLine(2, "Sleep QuotienttttttPurple");
}
private void CheckTitle(string expected)
{
Assert.AreEqual(expected, sectionUnderTest.Title);
}
private void CheckLine(int line, string expected)
{
Assert.AreEqual(expected, sectionUnderTest.Lines[line]);
}
}
I like that! Now that I have a bit of a report, it's time, I think, to start printing. This is where I step back and say, what do we have and where can we go?
Well, printers print reports, don't they? I guess we could start there. First, I need a printer. I still don't know how to test printing, so I'm going to start by creating my own printer that's not really a printer.
[TestFixture]
public class MyOwnPrinterTest
{
private string expectedReport = "TPS Report\r\n" +
"-----\r\n" +
"Yawn Factor\t\t\t\t\t670\r\n" +
"Average of a Bunch of Averages:\t\t1\r\n" +
"Sleep Quotient\t\t\t\t\tPurple";
[Test]
public void PrintReport()
{
Presentation presentation = new Presentation(new Content());
MyOwnPrinter printer = new MyOwnPrinter();
printer.Print(presentation);
Assert.AreEqual(expectedReport, printer.PrintedPage);
}
}
public class MyOwnPrinter
{
public StringWriter Page;
public void Print(Presentation presentation)
{
foreach (Section section in presentation.Sections)
{
Page.WriteLine(section.Title);
foreach (string line in section.Lines)
Page.WriteLine(line);
}
}
}
I knew that putting all the sections into an array would make sense. So there we have it. We have our report printing to a fake printer. But we aren't done yet. We need this to go to a real printer. The reason I made the fake printer first was simple - make sure the report can be printed. If a fake printer can define an interface to print the report to a stream, then the real printer can implement the same interface and change the underlying mechanics.
But, once again, I'm not happy. We are so close, but the interface doesn't seem right. For instance, how can I draw '-----' as a nice horizontal line on a real printer? I probably have the interface wrong. Let's think about this for a minute.
What does a printer do? It prints reports, right? Well, that was my first thought. But really, a printer is just like a monitor or even a web browser. And all those things provide services for rendering images. If that's the case, then maybe I have the relationship between Presentation and MyOwnPrinter backwards. Maybe Presentation draws itself using the services that MyOwnPrinter offers.
Lets try that. I feel a big refactoring coming on.
[TestFixture]
public class MyOwnPrinterTest
{
private string expectedReport = "TPS Report\r\n" +
"-----\r\n" +
"Yawn Factor\t\t\t\t\t670\r\n" +
"Average of a Bunch of Averages:\t\t1\r\n" +
"Sleep Quotient\t\t\t\t\tPurple";
[Test]
public void PrintReport()
{
Presentation presentation = new Presentation(new Content());
MyOwnPrinter printer = new MyOwnPrinter();
presentation.To(printer);
Assert.AreEqual(expectedReport, printer.PrintedPage);
}
}
public class Presentation
{
private Content content;
public Presentation(Content content) {
this.content = content;
}
public Section[] Sections {
get{return new Section[] {Title, SeldomReferredToStatsSection};}}
public void To(MyOwnPrinter printer)
{
foreach (Section section in Sections){
printer.Render(section.Title);
foreach (string line in section.Lines)
printer.Render(line);
}
}
private Section Title {
get{
Section section = new Section();
section.Title = "TPS Report";
section.AddHorizontalLine();
return section;
}
}
private Section SeldomReferredToStatsSection {
get{
Section section = new Section();
section.Title = "Seldom Referred-to Stats";
section.AddLine("Yawn Factor:ttttt" + content.YawnFactor);
section.AddLine("Average of a Bunch of Averages:tt" + content.AverageOfAverages);
section.AddLine("Sleep Quotientttttt" + content.SleepQuotient);
return section;
}
}
}
public class MyOwnPrinter
{
public StringWriter Page;
public void Render(string line)
{
Page.WriteLine(line);
}
}
I like that better. MyOwnPrinter now doesn't know about Presenation or it's structure. It just knows how to print stuff. Cool. But that still doesn't fix the problem of printing horizontal lines on a real printer. So maybe a service that the printer offers is to print horizontal lines. Before I even write a test for that, I don't want to have a bunch of 'if string == '-----' ' code everywhere. Maybe it's time to go back to that idea of lines.
[TestFixture]
public class HorizontalLineTest
{
[Test]
public void HorizontalLine()
{
MyOwnPrinter printer = new MyOwnPrinter();
printer.Render(new HorizontalLine());
Assert.AreEqual("-----", printer.Page);
}
}
This will seem a little tricky, but here is what is in my head - the printer knows how to draw a horizontal line, and the HorizontalLine class uses the printer. Watch:
public class HorizontalLine
{
public void Draw(MyOwnPrinter printer)
{
printer.RenderHorizontalLine();
}
}
and then add the service to the printer:
public class MyOwnPrinter
{
public StringWriter Page;
public void Render(string line)
{
Page.WriteLine(line);
}
public void RenderHorizontalLine()
{
Render("-----");
}
}
And we have a green bar. And I love that design, because now I can see how to extend it a little and make any line draw itself by extracting the Draw method into an abstract class.
Let's refactor a bit to use that concept:
public class Section
{
private string title;
private ArrayList lines;
public string Title {
get {return title;}
set {title = value;}
}
public Line[] Lines { get {return (Line[])lines.ToArray(typeof(Line));}
public void AddLine(string line) { lines.Add(new TextLine(line)); }
public void AddHorizontalLine() {lines.Add(new HorizontalLine()) }
}
The test can no longer access the data directly because it is nice and encapsulated. Remember that test refactoring earlier? Maybe that can help:
public void TestHelper
{
Section sectionUnderTest;
MyOwnPrinter printer;
[SetUp]
public void SetUp()
{
printer = new MyOwnPrinter();
}
protected void CheckTitle(string expected)
{
sectionUnderTest.Title.Draw(printer)
Assert.AreEqual(expected, printer.Page );
}
protected void CheckLine(int line, string expected)
{
sectionUnderTest.Lines[line].Draw(printer)
Assert.AreEqual(expected, printer.Page);
}
}
Use it in the Section Test:
public class SectionTest : TestHelper
{
[Test]
public void TitleSection()
{
Section section = new Section();
section.AddTitle("TPS Report");
section.AddHorizontalLine();
sectionUnderTest = section;
CheckTitle("TPS Report");
CheckLine(0, HorizontalLine);
}
[Test]
public void ContentSection()
{
Section section = new Section();
section.AddTitle("Section Title");
section.AddLine("A bit of content");
section.AddLine("A bit more content");
sectionUnderTest = section;
CheckTitle("Section Title");
CheckLine(0, "A bit of content");
CheckLine(1, "A bit more content");
}
private string HorizontalLine { get {return "-----";} }
}
And the Presentation test:
[TestFixture]
public class PresentationTest:TestHelper
{
[Test]
public void TPSReport()
{
Presentation presentation = new Presentation(new Content());
sectionUnderTest = presentation.Sections[0];
CheckTitle("TPS Report");
CheckLine(0, "-----");
sectionUnderTest = presentation.Sections[1];
CheckTitle("Seldom Referred-to Stats");
CheckLine(0, "Yawn Factor:ttttt670");
CheckLine(1, "Average of a Bunch of Averages:tt1");
CheckLine(2, "Sleep QuotienttttttPurple");
}
}
and refactor the classes:
public class Presentation
{
private Content content;
public Presentation(Content content)
{
this.content = content;
}
public Section[] Sections {
get{return new Section[] {Title, SeldomReferredToStatsSection};}}
public void To(MyOwnPrinter printer)
{
foreach (Section section in Sections) {
printer.Render(section.Title);
foreach (Line line in section.Lines)
printer.Render(line);
}
}
private Section Title {
get {
Section section = new Section();
section.AddTitle("TPS Report");
section.AddHorizontalLine();
return section;
}
}
private Section SeldomReferredToStatsSection{
get {
Section section = new Section();
section.AddTitle("Seldom Referred-to Stats");
section.AddLine("Yawn Factor:ttttt" + content.YawnFactor);
section.AddLine("Average of a Bunch of Averages:tt" + content.AverageOfAverages);
section.AddLine("Sleep Quotientttttt" + content.SleepQuotient);
return section;
}
}
}
public class Section
{
private ArrayList lines;
public Line[] Lines { get {return (Line[])lines.ToArray(typeof(Line));}
public void AddTitle(string title){ AddLine(title)};
public void AddLine(string line) { lines.Add(new TextLine(line)); }
public void AddHorizontalLine() {lines.Add(new HorizontalLine()) }
}
And done. Well almost, I need to add a real printer. I think it's safe to extract an interface from my own printer:
public abstract class Printer
{
public virtual void Render(string text){}
public virtual void RenderHorizontalLine(){}
}
Any printer that implements that interface can now be used to print presentations. You will see the final refactoring in the class recap at the end. We've finally come down to the very bottom - using a real printer to print. And really, it shouldn't be too hard to create a class that represents the real printer. In this case, I'm going to show you the class first, pseudo-coded:
public class RealPrinter : Printer
{
Graphics printerSurface;
public override Render(string text)
{
printerSurface.DrawString(text);
}
public override RenderHorizontalLine()
{
printerSurface.DrawLine(...)
}
}
Pass that to the Presentation class, and everything should draw. But where is the test? Ok, this may seem a little anti-climactic, but to test this, we write a very simple test:
[TestFixture]
public class RealPrinterTest
{
[Test]
public void LookAtTPSReport()
{
Presentation presentation = new Presentation(new Content());
LookAt(presentation);
}
private void LookAt(Presentation presentation)
{
presentation.To(printer);
System.PrintPreview();
}
}
(GRIN) I hope you haven't gotten this far, expecting an out of this world way of testing the actual printed page, only to feel like strangling me because I am using the print preview capabilities of the .Net framework (which I have pseudo-coded here because it is really not important to understanding the design).
However, the reason we feel confident doing this is because we have used TDD to drive every single part of the design right down to the interface to the printer. So we know that the lines, sections, and layout work properly before putting ink on the paper. By the time we get to the printer, we are dealing with a thin wrapper around graphics calls. And we don't test low level graphics calls to the OS because we won't be changing that stuff. What we have done is isolated the printer as much as possible so that we could have confidence in every other piece of the design.
So that's it for now. I'm not going to publish how we put the graphics or tabular elements on the page, or how we did the footers, or pagination, or any other detail. I have to maintain some element of mystery. And I need to keep my job, so I can't give away all our secrets to the public :) Look me up on the XP mailing list if you have questions about those things (hint - it's the same as all the other lines).
Here is a recap of all classes, sans tests:
public abstract class Printer
{
public virtual void Render(string text){}
public virtual void RenderHorizontalLine(){}
}
public class RealPrinter : Printer
{
Graphics printerSurface;
public override Render(string text)
{
printerSurface.DrawString(text);
}
public override RenderHorizontalLine()
{
printerSurface.DrawLine(...)
}
}
public class MyOwnPrinter : Printer
{
public StringWriter Page;
public override void Render(string line)
{
Page.WriteLine(line);
}
public override void RenderHorizontalLine()
{
Render("-----");
}
}
public class Presentation
{
private Content content;
public Presentation(Content content)
{
this.content = content;
}
public Section[] Sections {
get{return new Section[] {Title, SeldomReferredToStatsSection};}}
public void To(Printer printer)
{
foreach (Section section in Sections) {
printer.Render(section.Title);
foreach (Line line in section.Lines){
printer.Render(line);
}
}
private Section Title {
get {
Section section = new Section();
section.AddTitle("TPS Report");
section.AddHorizontalLine();
return section;
}
}
private Section SeldomReferredToStatsSection {
get {
Section section = new Section();
section.AddTitle("Seldom Referred-to Stats");
section.AddLine("Yawn Factor:ttttt" + content.YawnFactor);
section.AddLine("Average of a Bunch of Averages:tt" + content.AverageOfAverages);
section.AddLine("Sleep Quotientttttt" + content.SleepQuotient);
return section;
}
}
}
public class Section
{
private ArrayList lines;
public Line[] Lines { get {return (Line[])lines.ToArray(typeof(Line));}
public void AddTitle(string title){ AddLine(title)};
public void AddLine(string line) { lines.Add(new TextLine(line)); }
public void AddHorizontalLine() {lines.Add(new HorizontalLine()) }
}
public class HorizontalLine
{
public void Draw(Printer printer)
{
printer.RenderHorizontalLine();
}
}
public class Content
{
...
public int YawnFactor { get {return 670;} }
}