Introduction to Unit Testing in .NET using TDD
Test Driven Development - noun, also TDD - The step-by-step process of creating an application that is well tested by codifying expectations as tests, then implementing the solution to that test. As implied by the name, the tests drive the implementation, instead of meandering randomly through the code, changing things willy-nilly.
TDD changes the mindset of development so that it alternates between the big-picture thinking and detailed thinking in an orderly process.
Here are the steps:
# identify the behavior you expect
# write code that ensures that behavior (the test)
# run the code, seeing it fail
# write the simplest code you can write that will correctly pass the test
# see it pass
# optionally refactor and test again
# repeat
EXAMPLE
In this example, I'm using csTest, though there are several good free and pay frameworks for .NET.
1. Identify the behavior you expect
I need to be able to store names. I want to be able to enter a first name, middle initial, and last name, then see the full name.
I don't need to test C#. We assume that if we type 'class Name' that a class named Name will be available. We want to wait until there is some interaction, complexity, etc., to write a test. Here's what I'll write first:
2. Write code that ensures that behavior (the test)
3. Run the test, seeing it fail.
csUnit provides a Visual Studio plugin, which I use to run the test. Though this may be counter-intuitive, I'm happy to see that it fails.
" !http://4.bp.blogspot.com/_VeJd3vmTCZg/SUvTfe1Nh6I/AAAAAAAAIB0/_CEfMQbju7Q/s320/Fullscreen+capture+12192008+115900+AM.jpg! ":http://4.bp.blogspot.com/_VeJd3vmTCZg/SUvTfe1Nh6I/AAAAAAAAIB0/_CEfMQbju7Q/s1600-h/Fullscreen+capture+12192008+115900+AM.jpg
one test, one failure
4. write the simplest code you can write that will correctly pass the test
Now that we have a failing test, we need to make it pass. Add this to the Name class:
I run the test. Blah! Expected "John E. Doe" got "JohnEDoe". Oh, right. Ok. Change the implementation of ToString:
5. see it pass
" !http://1.bp.blogspot.com/_VeJd3vmTCZg/SUvWEZDq0BI/AAAAAAAAIB8/wC2UoA-AecE/s320/Fullscreen+capture+12192008+121143+PM.jpg! ":http://1.bp.blogspot.com/_VeJd3vmTCZg/SUvWEZDq0BI/AAAAAAAAIB8/wC2UoA-AecE/s1600-h/Fullscreen+capture+12192008+121143+PM.jpg one test, zero failures
Success!
6. Optionally, refactor
We can refactor if we want. I almost always want. :)
It seems like this functionality would be more logically in a property called FullName. This makes the method more like what I've heard called an Executable Comment.
7. Repeat
Great, so let's move on. Now, of course, I need to address the possibility that there might not be a middle name.
" !http://3.bp.blogspot.com/_VeJd3vmTCZg/SUvlMDulNXI/AAAAAAAAICE/Q9fJDtPm5xA/s320/Fullscreen+capture+12192008+11544+PM.jpg! ":http://3.bp.blogspot.com/_VeJd3vmTCZg/SUvlMDulNXI/AAAAAAAAICE/Q9fJDtPm5xA/s1600-h/Fullscreen+capture+12192008+11544+PM.jpg
two tests, one failure
Now, implement the solution.
two tests, zero failures... perfect!
Just for the sake of example, I'm going to show how unit tests allow you to avoid some kinds of FUD. I want to refactor FullName to pull out the formatting of the middle initial:
I skipped a step while extracting that method, but let's say I just overlooked it. I run the tests. Since I only refactored, no functionality should change.
two tests, one failure
expected "John E. Doe" but got "John Doe"
This gives me a really good idea of where to look for a problem. Ah ha! I forgot to add the call to FormattedMiddleInitial. I add in that call, and get the results I expect:
two tests, zero failures
TDD changes the mindset of development so that it alternates between the big-picture thinking and detailed thinking in an orderly process.
Here are the steps:
# identify the behavior you expect
# write code that ensures that behavior (the test)
# run the code, seeing it fail
# write the simplest code you can write that will correctly pass the test
# see it pass
# optionally refactor and test again
# repeat
EXAMPLE
In this example, I'm using csTest, though there are several good free and pay frameworks for .NET.
1. Identify the behavior you expect
I need to be able to store names. I want to be able to enter a first name, middle initial, and last name, then see the full name.
I don't need to test C#. We assume that if we type 'class Name' that a class named Name will be available. We want to wait until there is some interaction, complexity, etc., to write a test. Here's what I'll write first:
1 2 3 4 5 6 7 8 9 10 11 |
using System;using System.Collections.Generic;
using System.Text;
namespace Edu{
public class Name
{
public string FirstName;
public char MiddleInitial;
public string LastName;
}
}
|
2. Write code that ensures that behavior (the test)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using System;
using System.Collections.Generic;
using System.Text;
using csUnit;
using Edu;
namespace EduTest{
[TestFixture]
public class NameTests
{
[Test]
public void ToStringConcatinatesFirstMiddleAndLast()
{
Name john = new Name();
john.FirstName = "John";
john.MiddleInitial = 'E';
john.LastName = "Doe";
Assert.Equals("John E. Doe", john.ToString());
}
}
}
|
3. Run the test, seeing it fail.
csUnit provides a Visual Studio plugin, which I use to run the test. Though this may be counter-intuitive, I'm happy to see that it fails.
" !http://4.bp.blogspot.com/_VeJd3vmTCZg/SUvTfe1Nh6I/AAAAAAAAIB0/_CEfMQbju7Q/s320/Fullscreen+capture+12192008+115900+AM.jpg! ":http://4.bp.blogspot.com/_VeJd3vmTCZg/SUvTfe1Nh6I/AAAAAAAAIB0/_CEfMQbju7Q/s1600-h/Fullscreen+capture+12192008+115900+AM.jpg
one test, one failure
4. write the simplest code you can write that will correctly pass the test
Now that we have a failing test, we need to make it pass. Add this to the Name class:
1 2 3 4 5 6 7 8 9 10 |
public override string ToString()
{
StringBuilder full_name = new StringBuilder();
full_name.Append(FirstName);
full_name.Append(MiddleInitial);
full_name.Append(LastName);
return full_name.ToString();
}
}
}
|
I run the test. Blah! Expected "John E. Doe" got "JohnEDoe". Oh, right. Ok. Change the implementation of ToString:
1 2 3 4 5 6 7 8 9 10 |
public override string ToString()
{
StringBuilder full_name = new StringBuilder();
full_name.Append(FirstName);
full_name.Append(' ');
full_name.Append(MiddleInitial);
full_name.Append(". ");
full_name.Append(LastName);
return full_name.ToString();
}
|
5. see it pass
" !http://1.bp.blogspot.com/_VeJd3vmTCZg/SUvWEZDq0BI/AAAAAAAAIB8/wC2UoA-AecE/s320/Fullscreen+capture+12192008+121143+PM.jpg! ":http://1.bp.blogspot.com/_VeJd3vmTCZg/SUvWEZDq0BI/AAAAAAAAIB8/wC2UoA-AecE/s1600-h/Fullscreen+capture+12192008+121143+PM.jpg one test, zero failures
Success!
6. Optionally, refactor
We can refactor if we want. I almost always want. :)
It seems like this functionality would be more logically in a property called FullName. This makes the method more like what I've heard called an Executable Comment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public string FullName
{
get
{
StringBuilder full_name = new StringBuilder();
full_name.Append(FirstName);
full_name.Append(' ');
full_name.Append(MiddleInitial);
full_name.Append(". ");
full_name.Append(LastName);
return full_name.ToString();
}
}
public override string ToString()
{
return FullName;
}
|
7. Repeat
Great, so let's move on. Now, of course, I need to address the possibility that there might not be a middle name.
1 2 3 4 5 6 7 8 9 |
[Test]
public void FullNameHandlesMiddleInitialIntelligently()
{
Name jane = new Name();
jane.FirstName = "Jane";
jane.LastName = "Doe";
Assert.Equals("Jane Doe", jane.FullName);
}
|
" !http://3.bp.blogspot.com/_VeJd3vmTCZg/SUvlMDulNXI/AAAAAAAAICE/Q9fJDtPm5xA/s320/Fullscreen+capture+12192008+11544+PM.jpg! ":http://3.bp.blogspot.com/_VeJd3vmTCZg/SUvlMDulNXI/AAAAAAAAICE/Q9fJDtPm5xA/s1600-h/Fullscreen+capture+12192008+11544+PM.jpg
two tests, one failure
Now, implement the solution.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public string FullName
{
get
{
StringBuilder full_name = new StringBuilder();
full_name.Append(FirstName);
if (MiddleInitial.HasValue)
{
full_name.Append(' ');
full_name.Append(MiddleInitial);
full_name.Append('.');
}
full_name.Append(' ');
full_name.Append(LastName);
return full_name.ToString();
}
}
|
two tests, zero failures... perfect!
Just for the sake of example, I'm going to show how unit tests allow you to avoid some kinds of FUD. I want to refactor FullName to pull out the formatting of the middle initial:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public string FormattedMiddleInitial
{
get
{
StringBuilder initial = new StringBuilder();
if (MiddleInitial.HasValue)
{
initial.Append(' ');
initial.Append(MiddleInitial);
initial.Append('.');
}
return initial.ToString();
}
}
public string FullName
{
get
{
StringBuilder full_name = new StringBuilder();
full_name.Append(FirstName);
full_name.Append(' ');
full_name.Append(LastName);
return full_name.ToString();
}
}
|
I skipped a step while extracting that method, but let's say I just overlooked it. I run the tests. Since I only refactored, no functionality should change.
two tests, one failure
expected "John E. Doe" but got "John Doe"
This gives me a really good idea of where to look for a problem. Ah ha! I forgot to add the call to FormattedMiddleInitial. I add in that call, and get the results I expect:
two tests, zero failures