Test Driven Development with Visual Studio 2005 Team System
By Doug Seven
Published: 3/10/2006
Reader Level: Beginner Intermediate
Rated: 4.38 by 8 member(s).
Tell a Friend
Rate this Article
Printable Version
Discuss in the Forums

Test Driven Development (TDD) is not a new concept. In fact, the idea of test-first, code-second has been around for many years. In the latest release of Microsoft's premier developer tool, Visual Studio 2005 Team System many new features have been added, including features for testing software. What does this mean to you, the serious developer? It means you now have integrated unit testing that can be leveraged for Test Driven Development.

What is Test Driven Development?

I like to think of TDD as a means to design software by first documenting the requirements in a language a programmer understands, and then writing the code to fulfill the requirements. In his book Test-Driven Development: By Example (Addison-Wesley), Kent Beck defined TDD with two simple rules:

  1. Never write a single line of code unless you have a failing automated test.
  2. Eliminate duplication.

Kent's rules and my idea of TDD go hand-in-hand. In most typical software development cycles, a product owner or project manager will provide a set of requirements; typically the requirements are in a Word document titled "Functional Specification." Word documents are great for conveying a story, but what we really need to do is translate the story told by the project manager into a programming language - think of this as a new requirements document written in C# (or Visual Basic). The requirements are written in code as tests; each test representing a single requirement. In addition, when we get to writing the functional code (a little later) we want to ensure we eliminate any duplicate code by centralizing code that is needed in more than one place.

Types of Tests

Every time I start talking to people about software tests their mind seems to go immediately to thinking about a test to ensure the application works as expected. This is certainly important, and it can be broken into two parts.

  • Programmer Tests
    Automated tests written by the programmer prior to writing the functional code. The functional code is written to do no more and no less than pass the test. These are also know as unit tests.
  • Customer Tests
    Tests written by a test engineer to validate the functionality of the application from the customer perspective. These are also known as acceptance tests.

For the remainder of this article I will be talking solely about Programmer Tests, which, even though they include the word "test," are the responsibility of the programmer.

No Less, No More

In the definition of Programmer Tests, I mentioned that the functional code should do no more and no less that what the programmer test indicates. This is a lot more difficult than it sounds. It is a natural tendency, especially for programmers, to attempt to foresee the future. How many times while writing code have to included a "feature" because you know that eventually it would be requested. Perhaps you wrote a method to get customer order summaries, even though that was not part of the current specification. Certainly this would be needed soon.

This is the trap we have to avoid. Albert Einstein once said, "Any fool can make things bigger, more complex, and more violent. It takes a touch of genius — and a lot of courage — to move in the opposite direction." In other words, truly brilliant code includes on the code necessary to execute the requirements, and no more. The functional code should pass all of its programmer tests, communicate its purpose clearly, and contain the smallest number of classes and methods possible. When the code does all of these things it is done. Step away from the keyboard. Resist the urge to add more.

That's Great, But How Does It All Work?

As a programmer committed to using TDD well, the first thing you need to do is document the functional requirements as programmer tests. Do this before any functional code is written. Start by creating a Test List. Describe all of the tests that define the requirements, and ensure this list is the most complete definition of the requirements criteria. Ensure the Test List is as simple as it can be while conversely ensuring all of the requirements are documented.

For example, let's say as part of our application we need to create an object to represent a Product. The Test List may look something like this:

  1. Create a Product and verify IsDirty is false.
  2. Create a Product, change the Product Name, and verify IsDirty is true.
  3. Create a Product, change the Product Name, verify the ProductName is changed.
  4. Create a Product, change the Product Name, save the Product, verify the Product was saved.

This is a simplified version of a Test List, and depending on the requirements there may not be much more to document.

Red/Green/Refactor

Red/Green/Refactor is the TDD mantra, and it describes the three states you go through when writing code using TDD methods.

  • Red
    Write a failing test.
  • Green
    Write the code to satisfy the test.
  • Refactor
    Improve the code without changing its functionality.

Alternately you could say, "Make it fail. Make it work. Make it better." Let's take a look at these steps in action.

Writing the Programmer Test

Let's satisfy the first requirement (test) in our Test List. In Visual Studio 2005 Team System, assuming you have the Developer Edition or Test Edition, you can create a new kind of project, a Test Project.

create a test project

The test project produces a few artifacts, and I am going to focus on the UnitTest1.cs file. This file is where you will document your requirements in C#. Delete the ManualTest1.mht file - it is not needed for this project.

using System;
using System.Text;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DotNetJunkiesTDD {

  [TestClass]
  public class ProductTest {

    public ProductTest() {
      // TODO: Add constructor logic here //
    }

    [TestMethod] 
    public void TestMethod1() {
      // TODO: Add test logic here
    }
  }
}

In the preceding code block you see the basic structure of a test class. This is a class that is specifically for containing programmer (unit) tests. The file includes a reference to the Microsoft.VisualStudio.TestTools.UnitTesting namespace. This brings all of the unit testing utilities into this class.

The class definition is modified with the [TestClass] attribute. This specifies this class as a collection of unit tests (if you've even used NUnit, this is the equivalent of TestFixture).

The method declaration is modified by the [TestMethod] attribute. This specifies this method as a test for a single unit of functionality. A single feature (such as a Product representation in code) may contain several programmer tests (in NUnit this is a Test).

Red

To write the first requirement as a programmer test, you modify the TestMethod1 method as shown here:

/// <summary>
/// Create a Product and verify IsDirty is false.
/// </summary>
[TestMethod]
public void CheckIsDirty() {
  Product prod = new Product();
  Assert.IsTrue(prod.IsDirty);
}

In the preceding code you define the method CheckIsDirty as a TestMethod. In the test you write only the code to define the test criteria. In this case you create a new instance of the Product class, and use the Assert.IsFalse() method to make the assertion that the IsDirty property has the value of false. Of course if you try to compile this code it will fail because you have not defined the Product class. At this stage you are Red - you have a failing test (failing to compile is failing).

Green

The next step is to write only the code required to fulfill the requirement (to make the test pass). To do this you can create a new ClassLibrary project where the Product class will reside. Create a Product class.

using System;
using System.Collections.Generic;
using System.Text;

namespace MyLibrary {
  public class Product {}
}

After creating the Product class (and adding a reference to the ClassLibrary project in our Test project), the Test project still wont compile because we still have not fulfilled the test. We need to satisfy the requirement for an IsDirty property to return false.

public class Product {
  public bool IsDirty = false;
}

Now, the preceding code meets the requirements as they are defined. It would be real easy for me to add some additional code (like the possibility to return true from this property), but I haven't reached the requirement for that yet.

One of the key principles to TDD is the small, incremental process for developing software. You document a requirement in a test; write only the code to fulfill the test; clean up the code if necessary; move on to the next requirement. It takes a great deal of discipline to write ONLY the code necessary to fulfill the requirements (i.e. make the test pass).

Now that we have this code, both projects should compile. Once the projects compile you can run your programmer test to see if they pass (to see if you fulfilled the requirements). Click the green right-facing arrow in the VSTS toolbar, as shown here:

run the tests

When the tests run, the Test Results window opens, the test has an "!" icon to its left indicating the test is about to run. Once the test runs you will see the icon change to a green circle with a check, or a red circle with an X. Green indicates the test passed, while red indicates it failed (are you starting to see where Red/Green/Refactor comes from?).

Since our functional code is exactly the code required to pass the test, we get a green icon and we can move on.

Refactor

The next step is to refactor our code if possible. Going back to Kent Beck's rules, this is the "eliminate duplication" rule. Recall, the definition of refactor is to improve the code without changing its functionality. Reviewing the code we have written we can see that there is no refactoring needed.

Repeat and Rise

At this point you can go back to your Test List and write your programmer test to represent the next requirement.

/// <summary>
/// Create a Product, change the Product Name, and verify IsDirty is true.
/// </summary>
[TestMethod]
public void ChangeProductName() {
  Product prod = new Product();
  prod.Name = "New name";
  Assert.IsTrue(prod.IsDirty, "After changing Product.Name, Product.IsDirty should be true" );
}

The preceding test fails to compile because we have no Name property. The next step is to write only enough code for the projects to compile.

public class Product {
  public bool IsDirty = false;
  public string Name;
}

After writing this code the projects both compile. Next you run your unit tests - CheckIsDirty passes (nothing changed to break it), but ChangeProductName failes.

test 2 fails

While the code compiled, we still haven't implemented enough to fulfill the requirement (we are in the Red state). So, we add only enough code to do that.

public class Product {
  public bool IsDirty = false;
  public string Name {
    set { IsDirty = true; }
  }
}

After compiling both projects, both tests pass. We have now fulfilled two requirements. Time to refactor. As we look at the code, the functional code looks as clean as it can be, but there is some duplicate code in the Test project. We can refactor the Test code as follows (yes, we refactor test code too):

[TestClass]
public class ProductTest {
  public ProductTest() {
    // TODO: Add constructor logic here
  }

  Product m_prod;

  [TestInitialize]
  public void Init() {
    m_prod = new Product();
  }

  /// <summary>
  /// Create a Product and verify IsDirty is false.
  /// </summary>
  [TestMethod]
  public void CheckIsDirty() {
    Assert.IsFalse(m_prod.IsDirty);
  }

  /// <summary>
  /// Create a Product, change the Product Name, and verify IsDirty is true.
  /// </summary>
  [TestMethod]
  public void ChangeProductName() {
    m_prod.Name = "New name";
    Assert.IsTrue(m_prod.IsDirty, "After changing Product.Name, Product.IsDirty should be true");
  }
}

You can use the [TestInitialize] attribute to modify a method to be executed prior to every test method that is run (the [ClassInitialize] attribute will cause the method to be executed once prior to when the first test begins).

We have completed our Red/Green/Refactor steps. Now we can move on to the next test in our Test List.

/// <summary>
/// Create a Product, change the Product Name, verify the ProductName is changed.
/// </summary>
[TestMethod]
public void ChangeProductNameVerify() {
  string prodName = "Seven's Death Ray";
  m_prod.Name = prodName;
  Assert.AreEqual(prodName, m_prod.Name,
    "After changing Product.Name, the value should be the same as the value passed in."
    );
}

Notice we are making a new assertion, the AreEqual assertion. Be sure to check the documentation to learn about all of the possible assertions you can make.

Our code doesn't compile because the Product.Name property doesn't have a get accessor, so we need to add one.

public class Product {
  public bool IsDirty = false;
  public string Name {
    get { return null; }
    set { IsDirty = true; }
  }
}

While our code now compiles, the new test fails. The Name property is returning null when "Seven's Death Ray" is expected. So, we update the code.

public class Product {
  public bool IsDirty = false;

  private string m_name;
  public string Name {
    get { return m_name; }
    set {
      IsDirty = true;
      m_name = value;
    }
  }
}

Now all of our tests pass. We again look for refactoring opportunities, and then move on to the next test.

Summary

In this article you learn about Test Driven Development and how to write software using TDD with Visual Studio 2005 Team System. Hopefully you can see that with a little discipline, and a willing ness to do things a little differently than what you are used to, you will be able to develop software knowing that you are writing only the code that is necessary. Additionally you will write code with the confidence that nothing you added, changed or removed cause a breaking change; if all the tests pass, then the code meets its requirements.

I truly hope you are willing to give TDD a try. I think you will find it addictive. It may seem cumbersome, but in reality it isn't, and the pay off for a little extra work is absolutely worth it. In this article it may have seemed like a lot of steps, and time taken to write test-first code. In reality the tests and code in this article took only a few seconds each to complete - it just seemed like longer because I was explaining each step of the way.



Marketplace
(Sponsored Links)
What are the green links?
   



 
Copyright © 2007 CMP Tech LLC |
Privacy Policy (4/10/06) | Your California Privacy Rights (4/10/06) | Terms of Service | Advertising Info | About Us | Help