Calendar Day View: TDD and Refactoring at work
So, to start out, I thought a little bit about the initial setup for how I could do some testing on the server side of the control. You have to understand the inner workings of server controls to understand. At the end of the process, no matter whether the control is a simple control, a composite control, or a control employing data binding or templating, there is a rendering process that converts the contents of the control to it's HTML equivalent. For instance, a GridView is rendered as a table, a panel as a span, composite controls as a collection of HTML elements, etc.
The control has a Render method that takes an HtmlTextWriter object, which renders all of the content. With composite control, if no render method is defined, then during rendering phase, the base CompositeControl class calls the Render method on each control. The HtmlTextWriter object simply uses a stream to write HTML tags, DHTML attributes, and any other content to the underlying stream. What this means is that the constructor of HtmlTextWriter could take an instance of a StringWriter class, and all HTML is written to a local writer, and easily retrieved through StringWriter.ToString().
To start out with TDD, the following test was created:
[
Test]
public void TestRenderingCalendarHourly() {
this.FirstTimeVisible = new TimeSpan(8, 0, 0);
this.LastTimeVisible = new TimeSpan(16, 0, 0);
this.TimeInterval = CalendarDayViewIncrement.Hour;
StringWriter textWriter = new StringWriter();
HtmlTextWriter htmlWriter = new HtmlTextWriter(textWriter);
this.RenderCalendar(htmlWriter);
StringReader reader = new StringReader(textWriter.ToString());
XElement doc = XElement.Load(reader);
var tdTags = from tdTag in doc.Elements("tr").Elements("td")
select tdTag;
Assert.AreEqual(8, tdTags.Count());
}
Picturing an Outlook calendar, in the current view is a period of time between 8:00 and 4:00 (in this example above). The length of time between each interval of time is one hour, so every slot in the calendar is an hour. The StringWriter is passed into the HTMLTextWriter, and the contents are read from a StringReader; textReader.ToString() returns the HTML that is generated from that method.
Now, at this point, when the test is created, by some TDD practitioners, none of the classes, properties, or methods have been implemented. I personally create the class definition, so it appears in VS intellisense, but most times do not implement the properties and methods (like I said, I'm not always the most faithful TDD practitioner). What this means is that this unit test being developed drives what is being designed. The reason is that the test really drives what you need to do in an application. In a test, you know you need X, Y, and Z pieces of information, and you know you need to perform action A and B, so the unit test fleshes those out in advance. So from this unit test, it let me know that I needed a FirstTimeVisible and LastTimeVisible properties, a TimeInterval to note the unit of time between each line. I've also made the decision in the test to use a table tag, and when the first time visible is 8:00 AM and the last is 4:00 PM, and the interval is an hour long, there should be 8 TD tags. Creating the test first doesn't waste time creating objects, properties, or methods you end up not needing in the end, because you work through the solution in advance.
From this test, this led me to create the following:
protected override void Render(HtmlTextWriter writer)
{
base.AddAttributesToRender(writer);
writer.RenderBeginTag(HtmlTextWriterTag.Table);
writer.RenderBeginTag(HtmlTextWriterTag.Thead);
writer.RenderEndTag(); //thead
writer.RenderBeginTag(HtmlTextWriterTag.Tbody);
this.RenderCalendar(writer);
writer.RenderEndTag(); //tbody
writer.RenderEndTag(); //table
}
protected virtual void RenderCalendar(HtmlTextWriter writer)
{
TimeSpan currentTime = this.FirstTimeVisible;
int index = 0;
while (currentTime < this.LastTimeVisible)
{
CalendarDayViewItemStyle style = (index % 2 == 0) ? this.GetItemStyle() : this.GetAlternatingStyle();
this.RenderDayRow(writer, style);
currentTime = this.AddIncrement(currentTime);
index++;
}
}
protected virtual void RenderDayRow(HtmlTextWriter writer, CalendarDayViewItemStyle itemStyle)
{
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
writer.AddAttribute(HtmlTextWriterAttribute.Height, itemStyle.Height.ToString());
if (itemStyle.Width != Unit.Empty)
writer.AddAttribute(HtmlTextWriterAttribute.Width, itemStyle.Width.ToString());
ControlRenderingUtility.RenderBorderStyle(writer, itemStyle);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.Write("TEST");
writer.RenderEndTag(); //td
writer.RenderEndTag(); //tr
}
The Render method sets up the table, the RenderCalendar method actually performs the inside looping/calculation work, while RenderDayRow actually renders a row for a specified timeslot (I think I'll be refactoring that name soon as I think about it). However, my test fails. My test calls the RenderCalendar method, which doesn't write XHTML compliant content; instead, it leaves out the beginning table tag, and so the LINQ-to-XML load statement fails, because there isn't a valid root level element.
I also don't think this is the best structure of code also because of separation of concerns, which is defined by Wikipedia as: "separation of concerns (SoC) is the process of breaking a computer program into distinct features that overlap in functionality as little as possible". In my mind, these methods are too interdependent; RenderDayRow and RenderCalendar are dependent on the table structure setup by Render, while RenderCalendar performs most of the work and expects RenderDayRow to render the entire row.
This will lead me to move the RenderCalendar functionality into the Render method or vice versa; I haven't decided yet. By the way, in Martin Fowler's Refactoring book, this is one of his defined refactorings, but I don't remember what the name of the Refactoring is! Anyway, I hope that maybe this is helpful for you to understand that TDD starts out with creating unit tests, even failed ones, and defines the class structures from these tests. This creates more useful code. Also, other techniques associated with TDD, such as refactoring, help make code better.