Refactoring to TDD


Okay, so a few of you might think I am cracked right now. The rest of you are reading this, not really realizing the irony in the title. Think about it for a second. Can you really refactor code to be Test Driven? No, but TDD is a word that people recognize, and you can refactor your development paradigm to TDD. That is what we are talking about here.

What I mean when I say “refactor to TDD”, in this post is refactoring from informal integration tests or no tests to unit testing. In this particular post I am moving from an environment where there is no formal unit testing done by the developers in the organization to formal unit testing. In this environment, any testing done is integration testing, exercising all layers of the application. And, much of it is not automated. Instead, wrappers are written around the code to test output.

As an example, you will see a “test” coded in a separate windows forms application (or console application, etc):

    public Form1()
    {
        InitializeComponent();
    }

    private void TestButton1_Click(object sender, EventArgs e)
    {
        string name = "Greg";
        string message = MessageGenerator.GetMessage(name);

        if (message != "Hello Greg!")
        {
            MessageBox.Show("GetMessage() failed and did not return 'Hello Greg!'");
        }
        else
        {
            MessageBox.Show("GetMessage() test succeeded with 'Hello Greg!'");
        }
    }

Now, let’s assume the GetMessage() routine is in a middle tier class and looks like this:

public static string GetMessage(string name)
{
    StringBuilder builder = new StringBuilder();
    builder.Append("Hello ");
    builder.Append(name);
    builder.Append('!');

    return builder.ToString();
}

So far this is simple stuff. But let’s suppose we get some change orders.

New Requirements

In the new requirements, we pull a random quote from a database on another line. So, the GetMessage() routine starts to look more like this:

public static string GetMessage(string name)
{
    StringBuilder builder = new StringBuilder();
    builder.Append("Hello ");
    builder.Append(name);
    builder.Append('!');
    builder.Append("rn");
    builder.Append(RandomQuoteGenerator.GetRandomQuote());

    return builder.ToString();
}

GetRandomQuote() is the routine that calls the database. It looks something like this (noting that this is not really good form, it is to illustrate refactoring and moving to a TDD type paradigm):

public string GetRandomQuote()
{
    //Bad that this is hardcoded, but showing the proper way to 
    //get max from table == bloated blog entry
    int max = 1000;

    Random random = new Random((int)DateTime.Now.Ticks);
    int id = random.Next(1, tableMax);

    SqlConnection connection = new SqlConnection(ConnectionString);
    SqlCommand command = new SqlCommand("GetRandomQuote");
    command.CommandType = CommandType.StoredProcedure;

    string returnValue;

    try
    {
        connection.Open();
        returnValue = command.ExecuteScalar().ToString();
    }
    finally
    {
        connection.Dispose();
    }

    return returnValue;
}

The important part is our test becomes rather munged up, as we have to do something more like this:

private void TestButton1_Click(object sender, EventArgs e)
{
    string name = "Greg";
    string message = MessageGenerator.GetMessage(name);
    string[] messageSplit = message.Split("rn".ToCharArray());

    if (messageSplit[0] != "Hello Greg!")
    {
        MessageBox.Show("GetMessage() failed and did not return 'Hello Greg!'");
    }
    else
    {
        if (message[message.Length - 1].ToString().Length > 0)
        {
            MessageBox.Show("GetMessage() test succeeded with 'Hello Greg!' and a message.");
        }
        else
        {
            MessageBox.Show("GetMessage() has 'Hello Greg!', but does not have a database message!");
        }
    }
}

That is getting pretty nasty. Now, let’s try to move to automated testing.

Refactoring: The Dual Edged Sword

As we move to automated tests, we have a bit of chicken/egg problem. We need to refactor some of our code so it is testable, but we need tests to be refactor safely. So we need to refactor and test at the same time, but we can’t do both.

The solution is to use a unit test framework to set up integration tests. In this case, we already have windows forms testing (not great, but better than nothing), we cna use the tests and retool them to at least test the first line.

[TestClass()]
public class when_creating_a_hello_message_for_greg
{
    [TestMethod()]
    public void first_line_equals_hello_greg()
    {
        string name = "Greg"; 
        string expected = "Hello Greg!"; 
        string actual = MessageGenerator.GetMessage(name);
        string[] splitActual = actual.Split("rn".ToCharArray());

        Assert.AreEqual(expected, splitActual[0], "First line does not equal 'Hello Greg!'");
    }
}

That works. Note I use a BDD type of naming convention, although BDD purists are probably ready to throttle me. I do this, as I can easily see which methods failed and understand what is failing. The message tells me what went wrong, but I don’t have to see the message to know.

I now need to refactor the test a bit to test a second line.

[TestClass()]
public class when_creating_a_hello_message_for_greg
{
    private static string[] messageSplit;

    private string[] GetMessageSplit()
    {
        if (messageSplit == null)
        {
            string name = "Greg";
            string actual = MessageGenerator.GetMessage(name);

            messageSplit = actual.Split("rn".ToCharArray());
        }

        return messageSplit;
    }

    [TestMethod()]
    public void first_line_equals_hello_greg()
    {
        string expected = "Hello Greg!";
        string[] splitActual = GetMessageSplit();
        Assert.AreEqual(expected, splitActual[0], "First line does not equal 'Hello Greg!'");
    }

    [TestMethod()]
    public void second_line_equals_a_random_quote()
    {
        string[] splitActual = GetMessageSplit();

        Assert.IsTrue(splitActual[splitActual.Length - 1].ToString().Length > 0, "Does not contain a random quote!");
    }
}

So, we have both lines tested, but it is automated. Note that the code is not really unit tested, however, as we are still calling the database. But, since we have a test, even if it does exercise too much code, we can refactor and at least have some semblance of sanity, with tests to show our code does not break.

Inversion of Control

To move to true unit tests, we have to invert control. To do this, I have to do a few things at once. This is a good time to check in the code, including the tests, so we have a point to return to. In Team Foundation Server, we can simply shelf the changes.

  1. Create an interface for the random quote database bits
  2. Invert the GetMessage() routine to take the interface

We are not moving to a unit test yet, as refactoring is ALWAYS done in small steps. But we do have to accomplish this task to get to unit testing. First, here is the interface:

public interface IRandomQuoteRepository
{
    string GetRandomQuote();
}

Now, since our routine is embedded in the same class, we have to move it out and adhere to the interface. It looks like this (yes, it is ugly, but it is merely to illustrate our steps):

public class RandomQuoteRepository : IRandomQuoteRepository
{
    public string ConnectionString { get; private set; }

    public RandomQuoteRepository(string connectionString)
    {
        ConnectionString = connectionString;
    }

    #region IRandomQuoteRepository Members
    public string GetRandomQuote()
    {
        //Bad that this is hardcoded, but showing the proper way to 
        //get max from table == bloated blog entry
        int max = 1000;

        Random random = new Random((int)DateTime.Now.Ticks);
        int id = random.Next(1, tableMax);

        SqlConnection connection = new SqlConnection(ConnectionString);
        SqlCommand command = new SqlCommand("GetRandomQuote");
        command.CommandType = CommandType.StoredProcedure;

        string returnValue;

        try
        {
            connection.Open();
            returnValue = command.ExecuteScalar().ToString();
        }
        finally
        {
            connection.Dispose();
        }

        return returnValue;
    }
    #endregion
}

Our GetMessage then changes like this:

public static string GetMessage(string name)
{
    RandomQuoteRepository repository = new RandomQuoteRepository(ConnectionString);

    StringBuilder builder = new StringBuilder();
    builder.Append("Hello ");
    builder.Append(name);
    builder.Append('!');
    builder.Append("rn");
    builder.Append(repository.GetRandomQuote());

    return builder.ToString();
}

The next step is to test again, to make sure moving the code out works. You have done one refactor, so bozo check your work. Sure, you are still going to the database, but that is the cost. Refactor and test. Refactor and test. Get this rhythm down.

Once you are actually using TDD, you switch to Red … Green … Refactor. So we start with the test:

private string[] GetMessageSplit()
{
    if (messageSplit == null)
    {
        IRandomQuoteRepository repository = new RandomQuoteRepository("");

        string name = "Greg";
        string actual = MessageGenerator.GetMessage(name, repository);

        messageSplit = actual.Split("rn".ToCharArray());
    }

    return messageSplit;
}

No compile. That is close enough to a red. Now, just enough code to make it work. This is a simple change:

public static string GetMessage(string name, IRandomQuoteRepository repository)
{
    StringBuilder builder = new StringBuilder();
    builder.Append("Hello ");
    builder.Append(name);
    builder.Append('!');
    builder.Append("rn");
    builder.Append(repository.GetRandomQuote());

    return builder.ToString();
}

You are now inverted. The “UI” (test project in this case) feeds the repository as an interface. Note that you are still integration testing, as the database is still called.

Mocking so we can Unit Test

I am not getting into mock frameworks at this time, but we will make a very simple mock that always returns “A stitch in time saves nine!”. It has to adhere to the IRandomQuoteRepository interface, so it looks like this:

public class MockRandomQuoteRepository : IRandomQuoteRepository
{

    #region IRandomQuoteRepository Members

    public string GetRandomQuote()
    {
        return "A stitch in time saves nine!";
    }

    #endregion
}

Pretty simple, but watch what we can do. We no longer have to hit the database if we simply change our repository to the mock in our test class:

private string[] GetMessageSplit()
{
    if (messageSplit == null)
    {
        IRandomQuoteRepository repository = new MockRandomQuoteRepository();

        string name = "Greg";
        string actual = MessageGenerator.GetMessage(name, repository);

        messageSplit = actual.Split("rn".ToCharArray());
    }

    return messageSplit;
}

Even more important, since the output is no longer from a database, it need not be random, only act as if it is random. So we can test our conditions and make sure the last line is correct. This is a simple test refactor:

[TestMethod()]
public void second_line_equals_a_stitch_in_time_saves_nine()
{
    string expected = "A stitch in time saves nine!";
    string[] splitActual = GetMessageSplit();

    Assert.AreEqual(expected, splitActual[splitActual.Length - 1] , "Second line is not 'A stitch in time saves nine!'");
}

Now we are unit testing and life is grand. To move to TDD, we would take an additional step and start writing tests before every new feature. There is really no way to refactor to this, but you do have to refactor to unit testing before you can adopt TDD. I will have to leave TDD for another post.

Summary

Here is the sequence of events. They are in this order as you cannot effectively refactor WITHOUT tests. To recode blindly is both dangerous and stupid, just like driving drunk, except, in general, nobody dies. On critical medical systems, however, someone could actually die. You should treat all of your software as if it were a critical medical system. Kapish?

If anyone wants to comment about the basic idea of how to move from forms testing or no testing to unit tests, I would welcome comments. If you want to bitch about my naming convention or the fact the samples are not all best practices, please don’t waste your time. The whole purpose of this particular post is moving to unit testing.

Peace and Grace,
Greg

Twitter: @gbworld

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: