Click here to Skip to main content
15,885,028 members
Articles / Programming Languages / C#
Article

MikiWiki

Rate me:
Please Sign up or sign in to vote.
4.44/5 (11 votes)
30 Jun 200510 min read 51.7K   151   52   12
An article about Test Driven Development, Refactoring, using NUnit, FitNesse, and a wiki engine too!

Miki Wiki Default Home page

Introduction

I've been bombarded by a few concepts (some would call them hypewords) lately. Agile methodologies, Test-Driven Development, NUnit, FitNesse, Refactoring... I'm not sure we can call these new concepts anymore: they have been around for a while. I figured I'd take them out for a test-drive and see how they handled the tight turns of a week-end project. In this article, you'll find a few information about these concepts, my thoughts in regard.. oh, and the current version of my week-end project: a wiki engine!

The Miki Application

Before I start ranting about the general concepts I want to expose, let me talk for a minute about the application that you can get from this page. It's called Miki (but MikiWiki will do as well), and it's a wiki engine. If you've never heard of wikis, I suggest you look it up online. Here're a few links to get you started, if you need them:

My personal definition is that a wiki is a web site that allows users to easily create and edit web pages.

If you install Miki you can quickly access the FormattingRules page (linked from the default root page) to see the various formatting rules supported.

The application is available in two flavours:

  • The pre-compiled version which is built for you, but you will still have to manually install on your machine: please see the detailed installation instructions in the Readme.txt file that is shipped with it;
  • The source code version which is not built for you, but lets you peek at the code; you get the same Readme.txt file with it, and an additional Miki Developer Manual.txt file to help you get oriented in the code.

I will not go in to more details on how you can build and/or install and run the application, since it is all described in the two mentioned files. For the rest of the article, I'll simply use the Miki application as a practical example.

Agile Methodologies

I guess the best page to start learning about Agile Methodologies is this and, specifically, the declared principles are available here. We've been talking about Agile methodologies for a while in my work environment. We also had the pleasure to have Robert Martin visit us for a week not long ago.

Since Miki has been developed, so far, just as a weekend project of mine, and I had no customer requesting specific functionalities and features, I can't really tell you how the Agile concepts worked for me. However, I used FitNesse during this project, which is a tool often mentioned by the Agile gurus (more on FitNesse below), and I tried to divide my work into (fairly arbitrary) chunks, or iterations (which, again, is reflected in the FitNesse tests). Dividing up the work in small iterations seemed to help a lot, especially since it lets me focus on the immediate needs of the project (i.e., 'What stories do I have to implement in this iteration?'), which, in turn, fits nicely in the Test-Driven Development theory that you should implement the minimum required, and refactor your code as you introduce new functionality and notice opportunities for refactoring.

Test-Driven Development (TDD)

TDD is well summarized here. Write a test, get a red light, make it green, refactor, and then move on. Seems fairly simple, right? Well, it is. Not only that, but it comes with huge benefits to your development quality and overall productivity. I must admit that the learning curve for this practice is fairly steep though, so, as you will see, there're bumps along this path that you need to overcome before experiencing the full advantages of the TDD way.

First and foremost, I found it difficult to stick to the practice of writing the tests first. I think this is due to years of bad practice (writing the code first, then the tests), because, by the end of the project, it was much easier for me to write the tests first when introducing new functionalities or fixing a bug.

One thing that I never became more comfortable with was that of writing the first test for a class or method I was going to introduce. I don't think I'll ever get over the feeling of silliness I get when I write something like this, and then try to compile it:

C#
using System;
using NUnit.Framework;
using Miki.Web.Lib.TopicLib;

namespace Miki.Web.Lib.UnitTests
{
   [TestFixture]
   public class Test_Topic
   {
      public Test_Topic()
      { }

      [Test]
      public void TestName()
      {
         Uri testUri = new Uri("http://www.somesite.com/alpha/beta/gamma");
         topic = new Topic(testUri);
         Assert.AreEqual("gamma", topic.Name,
            "Unexpected Name for Topic created using Uri.");
      }
   }
}

Of course it doesn't compile! By Thrall, I don't even have a Topic class defined in my project yet... nor, actually, a Miki.Web.Lib.TopicLib project!

Okay, so, maybe the first test for a new class feels very silly. Once you have a class, and you are about to introduce a new method, though, the silly feeling is a lot less awkward.

Any other bump? Well, to do TDD you need tools that allow you to write and perform automated tests. Specifically, I used NUnit and FitNesse. The tools are surprisingly good, if you ask me, especially since they come at no licensing cost. I don't really see a reason why anyone would not want to use them. Some tool-specific details follow.

NUnit

Let's be honest.. writing all these unit tests means that your initial development time will be quite longer than it would be otherwise. Yet, over the long run, your productivity goes up, because you are able to introduce new features and apply refactoring with the safety of all these unit tests in place.

The only problem I have with unit tests is that there's no clear way to know whether you have all the tests you need (yes, there are tools to look at code coverage, but, aside from that, there's always a qualitative question). And, finally, 'Should you keep your tests in the same assembly as your production code, or should you keep them separate?'. Everyone, sooner or later, asks that question. I keep them in separate projects within the same solution, but it's one of those debates that will never end.

FitNesse

This was the first time I used this tool. I must say I was positively impressed. It took a little extra effort (to write the acceptance tests in the wiki pages, and to write the fixture classes that allow those wiki pages to be run as tests), but, overall, it was worth it.

You may wonder why you need FitNesse tests if you already have NUnit tests (or, in other words, why would you need acceptance tests if you have unit tests). The answer is that the two test the application at different layers. Just like in the 'old' days, we used to talk about unit tests and system tests, these two type of tests let you check different things. I believe you get even more value back from using both if you have customers (which, for the Miki project I didn't have) that drive the project. It is a classical problem: developers and customers speak two different languages, or, more precisely, observe the product from two distinct point of views. The acceptance tests tend to bridge that gap, by letting your customers specify requirements (and/or bugs that they find) through a specific language (i.e. the FitNesse tables). This language is encoded in such a way that you can use it to run tests; that's the real secret.

For the records, my acceptance tests are really tailored to test the Miki.MikiTextConverter project (one of the projects in the Miki solution). I did not (yet) write acceptance tests for the surrounding application (i.e. the Miki project itself) because that application is really concerned with displaying the results produced by the MikiTextConverter project in the form of a web page. In other words, the surrounding application is a presentation layer for the web. There is a set of fixture classes available from the FitNesse site intended to test web pages, but I have not had a chance (yet) to experiment with them.

Refactoring

The home page for this technique is here. I am a total n00b when it comes down to refactoring. Technically speaking, I might have been doing refactoring for years and never realized it (and that's probably true, to an extent), but I just started reading the 'Refactoring' book by Fowler and this is really the first project where I tried to apply this practice.

The results were interesting. I guess, the best way to share what happened is to copy a story from the Miki Developer Manual (which you get if you download the source code package of Miki):

4.2.2 The Parser class (and a story about refactoring)

The Parser class is all about parsing the given miki text. Its constructor requires an incoming string parameter (the miki text), and it exposes a few public methods: MoreTokens, NextToken, RestOfLine, PeekLine, RestOfLineStartsWithWhitespace. Some of these might surprise you, but they are needed by the MikiRender class when it encounters a token that affects the rendering of the rest of the miki text line. Future refactoring may, of course, improve on this interface.

You may notice that the Parser class defines a private delegate. This is the result of applying some refactoring to the NextToken method. It started out as something like this (...if I remember it all...):

C#
public string NextToken()
{
   if(!this.MoreTokens())
      return "";

   StringBuilder sb = new StringBuilder();
   if(this.srcText[0]=='\\')
   {
      for(int i=0; i<this.srcText.Length && this.srcText[i]=='\\'; i++)
      {
         sb.Append(this.srcText[i]);
      }
   }
   else if(Char.IsWhitespace(this.srcText[0]=='\\'))
   {
      for(int i=0; i<this.srcText.Length && 
          Char.IsWhitespace(this.srcText[i]); i++)
      {
         sb.Append(this.srcText[i]);
      }
   }
   else
   {
      for(int i=0; i<this.srcText.Length && 
         (!Char.IsWhitespace(this.srcText[i])); i++)
      {
         sb.Append(this.srcText[i]);
      }
   }

   this.srcText.Remove(0, sb.Length);
   return sb.ToString();
}

Of course, as I was adding more user stories, this method was growing at a steady rhythm. Each new type of token to be considered meant a new 'else if' block. So, looking at the code above, what should we spot? Some (and this is a technical term) 'bad smell'. So, to resolve the stinking problem, here's what I did (with the version of the code after the refactoring was applied):

  1. Moved the conditionals into separate methods:
    C#
    public string NextToken()
    {
       if(!this.MoreTokens())
          return "";
    
       StringBuilder sb = new StringBuilder();
       if(this.SourceStartsWithSlash())
       {
          for(int i=0; i<this.srcText.Length && this.srcText[i]=='\\'; i++)
          {
             sb.Append(this.srcText[i]);
          }
       }
       else if(this.SourceStartsWithWhitespace())
       {
          for(int i=0; i&t;this.srcText.Length && 
              Char.IsWhitespace(this.srcText[i]); i++)
          {
             sb.Append(this.srcText[i]);
          }
       }
       else
       {
          for(int i=0; i<this.srcText.Length && 
             (!Char.IsWhitespace(this.srcText[i])); i++)
          {
             sb.Append(this.srcText[i]);
          }
       }
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private bool SourceStartsWithSlash()
    { return this.srcText[0]=='\\'; }
    
    private bool SourceStartsWithWhitespace()
    { return Char.IsWhitespace(this.srcText[0]=='\\'); }
  2. Moved the body of each if statement in its own method (the chomping of the token also had to be moved):
    C#
    public string NextToken()
    {
       if(!this.MoreTokens())
          return "";
    
       if(this.SourceStartsWithSlash())
          return SlashToken();
       else if(this.SourceStartsWithWhitespace())
          return WhitespaceToken();
       return WordToken();
    }
    
    private bool SourceStartsWithSlash()
    { return this.srcText[0]=='\\'; }
    
    private bool SourceStartsWithWhitespace()
    { return Char.IsWhitespace(this.srcText[0]=='\\'); }
    
    private string SlashToken()
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && this.srcText[i]=='\\'; i++)
          sb.Append(this.srcText[i]);
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private string WhitespaceToken()
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && 
           Char.IsWhitespace(this.srcText[i]); i++)
          sb.Append(this.srcText[i]);
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private string WordToken()
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && 
          (!Char.IsWhitespace(this.srcText[i])); i++)
           sb.Append(this.srcText[i]);
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
  3. Realize that the SlashToken, WhitespaceToken and WordToken are *very* similar to each other;
  4. Define a delegate, and introduce a method like those three, that uses a delegate function:
    C#
    public string NextToken()
    {
       if(!this.MoreTokens())
          return "";
    
       if(this.SourceStartsWithSlash())
          return SlashToken();
       else if(this.SourceStartsWithWhitespace())
          return WhitespaceToken();
       return WordToken();
    }
    
    private bool SourceStartsWithSlash()
    { return this.srcText[0]=='\\'; }
    
    private bool SourceStartsWithWhitespace()
    { return Char.IsWhitespace(this.srcText[0]=='\\'); }
    
    private string SlashToken()
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && this.srcText[i]=='\\'; i++)
          sb.Append(this.srcText[i]);
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private string WhitespaceToken()
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && 
           Char.IsWhitespace(this.srcText[i]); i++)
          sb.Append(this.srcText[i]);
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    private string WordToken()
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && 
          (!Char.IsWhitespace(this.srcText[i])); i++)
           sb.Append(this.srcText[i]);
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private delegate bool ChompTokenCondition(char c);
    private string ChompToken(ChompTokenCondition chompCondition)
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && 
           chompCondition(this.srcText[i]); i++)
          sb.Append(this.srcText[i]);
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
  5. Implement three ChompTokenCondition delegates as implementing the logic currently used in SlashToken, WhitespaceToken, and WordToken:
    C#
    public string NextToken()
    {
       if(!this.MoreTokens())
          return "";
    
       if(this.SourceStartsWithSlash())
          return SlashToken();
       else if(this.SourceStartsWithWhitespace())
          return WhitespaceToken();
       return WordToken();
    }
    
    private bool SourceStartsWithSlash()
    { return this.srcText[0]=='\\'; }
    
    private bool SourceStartsWithWhitespace()
    { return Char.IsWhitespace(this.srcText[0]=='\\'); }
    
    private string SlashToken()
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && this.srcText[i]=='\\'; i++)
          sb.Append(this.srcText[i]);
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private string WhitespaceToken()
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && 
           Char.IsWhitespace(this.srcText[i]); i++)
          sb.Append(this.srcText[i]);
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private string WordToken()
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && 
          (!Char.IsWhitespace(this.srcText[i])); i++)
           sb.Append(this.srcText[i]);
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private delegate bool ChompTokenCondition(char c);
    
    private string ChompToken(ChompTokenCondition chompCondition)
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && 
           chompCondition(this.srcText[i]); i++)
          sb.Append(this.srcText[i]);
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private bool StopOnNonSlash(char c)
    { return c=='\\'; }
    
    private bool StopOnWhitespace(char c)
    { return Char.IsWhitespace(c); }
    
    private bool StopOnNonWhitespace(char c)
    { return !Char.IsWhitespace(c); }
  6. Remove the calls to the three stinking methods (and their bodies):
    C#
    public string NextToken()
    {
       if(!this.MoreTokens())
          return "";
    
       if(this.SourceStartsWithSlash())
          return this.ChompToken(new CompTokenCondition(this.StopOnNonSlash));
       else if(this.SourceStartsWithWhitespace())
          return this.ChompToken(new ChompTokenCondition(this.StopOnNonWhitespace));
       return this.ChompToken(new ChompTokenCondition(this.StopOnWhitespace));
    }
    
    private bool SourceStartsWithSlash()
    { return this.srcText[0]=='\\'; }
    
    private bool SourceStartsWithWhitespace()
    { return Char.IsWhitespace(this.srcText[0]=='\\'); }
    
    private delegate bool ChompTokenCondition(char c);
    private string ChompToken(ChompTokenCondition chompCondition)
    {
       StringBuilder sb = new StringBuilder();
       for(int i=0; i<this.srcText.Length && 
           chompCondition(this.srcText[i]); i++)
          sb.Append(this.srcText[i]);
    
       this.srcText.Remove(0, sb.Length);
       return sb.ToString();
    }
    
    private bool StopOnNonSlash(char c)
    { return c=='\\'; }
    
    private bool StopOnWhitespace(char c)
    { return Char.IsWhitespace(c); }
    
    private bool StopOnNonWhitespace(char c)
    { return !Char.IsWhitespace(c); }

The refactoring continued as the project moved on, and the current result is what you can see in the source code. IMHO, it smells better than the original code. Of course, even the current code can and should be improved. It turns out that making your code smell as a bunch of fresh roses takes quite some effort!

Actually, the current version of the NextToken method smells even better (at least to my nostrils):

C#
public string NextToken()
{
   if(!this.MoreTokens())
      return "";

   ChompTokenCondition chompCondition = 
             this.ChompConditionForNextToken();
   return this.ChompToken(chompCondition);
}

So, how did refactoring work out for me? Pretty good, I think. It didn't add too much overhead to my work, and actually allowed me to keep the code in a decent shape, which made it easier to introduce new features (that's the whole point of refactoring I think). Recommended practice? Definitively.

Conclusions

The bottom line is that, after a test-drive, these theories, techniques and tools seem to be very useful and work wonders for me. I am planning to keep on using them, and to learn more about each of them, and I believe any software project can gain by applying them (translation: 'To live a long and prosperous life, thou shall use TDD, with unit and acceptance tests, with a healthy serving of refactoring, in an agile project'). Of course, your mileage may vary, but I think you will find these tools and practices a Good Thing if you'll give them a spin as I did.

Oh, and I'd like to keep the Miki project alive.. so, if you want to become a Miki developer, grab the source and read the Miki Developer Manual.

History

  • June, 24th 2005: first release of Miki.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralI like the approach! Pin
martinplatt6-May-07 18:33
martinplatt6-May-07 18:33 
GeneralTestDriven.net with NUnit Pin
dotScott20-Jul-05 7:20
dotScott20-Jul-05 7:20 
GeneralNamespaces Pin
TurnSomePages8-Jul-05 4:55
TurnSomePages8-Jul-05 4:55 
GeneralRe: Namespaces Pin
Frank Olorin Rizzi8-Jul-05 5:40
Frank Olorin Rizzi8-Jul-05 5:40 
QuestionWhat about continuous integration? Pin
Brickski7-Jul-05 15:06
Brickski7-Jul-05 15:06 
AnswerRe: What about continuous integration? Pin
Frank Olorin Rizzi8-Jul-05 1:48
Frank Olorin Rizzi8-Jul-05 1:48 
QuestionWhat about NDoc Pin
Richard Schneider6-Jul-05 2:40
Richard Schneider6-Jul-05 2:40 
AnswerRe: What about NDoc Pin
Frank Olorin Rizzi6-Jul-05 4:42
Frank Olorin Rizzi6-Jul-05 4:42 
GeneralRe: What about NDoc Pin
patnsnaudy_6-Jul-05 14:22
susspatnsnaudy_6-Jul-05 14:22 
GeneralRe: What about NDoc Pin
Richard Schneider6-Jul-05 21:00
Richard Schneider6-Jul-05 21:00 
GeneralRe: What about NDoc Pin
Frank Olorin Rizzi8-Jul-05 2:28
Frank Olorin Rizzi8-Jul-05 2:28 
GeneralRe: What about NDoc Pin
Richard Schneider8-Jul-05 9:39
Richard Schneider8-Jul-05 9:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.