Introduction
This is the second article of series of articles related to Test-driven development (TDD) approach in Microsoft.NET. My intention is to illustrate this approach with several real-world examples.
In this article, I will show how programmers can use the test-driven development approach to implement and test a class library.
Getting started with a real-world example using TTD
In order to implement and test the principles of TTD applied to Class Library projects, we're going to create a Class Library project within a solution (see Figure 1).
Figure 1
And then, I will show how to implement using TTD a common data structure in computer science such a queue. As part of the behavior of the instances of the class Queue, we have to include the operations: Dequeue and Enqueue. The Dequeue operation removes and returns objects at the beginning of the queue. The Enqueue operation adds an object to the end of the queue. We're also going to implement the Count property in order to get the number of elements within the queue.
The first in the test-driven development approach is, of course as its name implies, the formulation of a list of tests. One important thing to keep in mind is that the list of tests is dynamic in order to add or remove tests for testing in different environments.
Let's specify the list of tests or test cases for the Queue data structure, as follows:
- Create a Queue instance and verify that is empty.
- Enqueue an object to the Queue instance and verify that is not empty.
- Enqueue one object and then dequeue it from the Queue instance and verify that is empty again. Check is the same object.
- Dequeue when there are no objects in the queue.
- Enqueue 3 objects remembering its state and check that is not empty. Then, dequeue 2 objects and check that are removed in the correct order as well as the empty property is still equal to true.
In order to implement these test cases, we have to define the interface between the Queue instances and the test cases. One way that I follow is to specify the interface of the Queue class without any implementation and then we reference the interface and call the methods from the test cases.
I like a lot this approach because when we define interfaces for the classes, conceptually we're separating the abstraction layer from the implementation. So, our class instances provide its interfaces as roles and communication mechanisms (independent of the implementation) to external entities which want to communicate to and consume the services. If later we want to change the implementation of the interface (to upgrade the services or for possible errors discovered in the test phase); then we make the changes in an agile way without changing the interface and breaking the line with the consumers of the interface services. From the point of view of technology is an advantage too, because, the application which uses our library doesn't need to re-build or change some code because the interface is stable.
This is why I think that TDD approach is as much a design mechanisms as a testing technique.
Let's define the IQueue interface for the Queue class (see Listing 1).
using System;
using System.Collections.Generic;
using System.Text;
namespace QueuePkg
{
public interface IQueue
{
bool IsEmpty { get; }
void Enqueue(object objInstance);
object Dequeue();
}
}
Listing 1
Now let's specify that the Queue class implements the interface in Listing 1. This can be automatically done using the new features of Visual Studio.NET 2005 (see Listing 2).
using System;
using System.Collections.Generic;
using System.Text;
namespace QueuePkg
{
public class Queue : IQueue
{
#region IQueue Members
public bool IsEmpty
{
get { throw new Exception("The method or operation is not implemented."); }
}
public void Enqueue(object objInstance)
{
throw new Exception("The method or operation is not implemented.");
}
public object Dequeue()
{
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
}
Listing 2
Now let's create a test suite as shown in Figure 2.
Figure 2
You need to add a reference to the NUnit framework (see Figure 3) and the QueuePkg (see Figure 4) assemblies. As well you need to add the associated using statements in C#.
Figure 3
Figure 4
And then add a QueueTestFixture fixture class (annotated with the TextFixture attribute) to the test suite in order to implement the group of test cases (see Listing 3).
using System;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using QueuePkg;
namespace QueueTestSuitePkg
{
[TestFixture]
public class QueueTestFixture
{
}
}
Listing 3
Now it's time to write code for the test cases using test methods annotated with Test attribute. It's remarkable to say that these methods must be declared as public, be an instance method (non-static), with return type as void, and take no parameters.
Let's implement the first test:"Create a Queue instance and verify that is empty" using the test method Empty (see Listing 4).
[Test]
public void Empty()
{
Queue objQueue = new Queue();
Assert.IsTrue(objQueue.IsEmpty);
}
isting 4
Now let's implement the second test:"Enqueue an object to the Queue instance and verify that is not empty." using the test method EnqueueOne (see Listing 5).
[Test]
public void EnqueueOne()
{
Queue objQueue = new Queue();
objQueue.Enqueue(1);
Assert.IsFalse(objQueue.IsEmpty, "IsEmpty must be false");
}
Listing 5
Now let's implement the third test:"Enqueue one object and then dequeue it from the Queue instance and verify that is empty again. Check is the same object." using the test method EnqueueDequeue (see Listing 6).
[Test]
public void EnqueueDequeue()
{
Queue objQueue = new Queue();
int nExpected = 1;
objQueue.Enqueue(nExpected);
Assert.IsFalse(objQueue.IsEmpty, "IsEmpty must be false");
int nActual = (int) objQueue.Dequeue();
Assert.AreEqual(nExpected, nActual,"Expected is not equal to actual");
}
Listing 6
Now let's go to the fourth test:"Dequeue when there are no objects in the queue." and let's implement it using the test method DequeueWhenEmpty (see Listing 7). It's remarkable to say that we can agree what to do when a dequeue operation is performed against an empty queue. In this case, we're going to throw an InvalidOperationException exception because it's an error we don't expect to occur.
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void DequeueWhenEmpty()
{
Queue objQueue = new Queue();
objQueue.Dequeue();
}
Listing 7
And finally, let's implement the fifth test:" Enqueue 3 objects remembering its state and check that is not empty. Then, dequeue 2 objects and check that are removed in the correct order as well as the empty property is still equal to true" using the test method Enqueue3Dequeue2 (see Listing 8).
[Test]
public void Enqueue3Dequeue2()
{
Queue objQueue = new Queue();
int nExpected1 = 1;
int nExpected2 = 2;
int nExpected3 = 3;
objQueue.Enqueue(nExpected1);
objQueue.Enqueue(nExpected2);
objQueue.Enqueue(nExpected3);
int nActual1 = (int) objQueue.Dequeue();
Assert.AreEqual(nExpected1,nActual1);
int nActual2 = (int) objQueue.Dequeue();
Assert.AreEqual(nExpected2, nActual2);
Assert.IsFalse(objQueue.IsEmpty, "IsEmpty must be false");
}
Listing 8
As the second step in the Test-Driven Development approach, we have to implement the business logic of the Queue class in order to later test it (see Listing 9).
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace QueuePkg
{
public class Queue : IQueue
{
private ArrayList m_arrQueue;
public Queue()
{
this.m_arrQueue = new ArrayList();
}
#region IQueue Members
public bool IsEmpty
{
get
{
return this.m_arrQueue.Count == 0;
}
}
public void Enqueue(object objInstance)
{
this.m_arrQueue.Add(objInstance);
}
public object Dequeue()
{
object objResult = null;
if (this.IsEmpty)
{
throw new InvalidOperationException("Cannot dequeue an empty queue");
}
objResult = this.m_arrQueue[0];
this.m_arrQueue.RemoveAt(0);
return objResult;
}
#endregion
}
}
Listing 9
Now let's test our business logic. Right-click on the QueueTestSuitePkg project and select Properties from the context menu. In the Debug tab, set GUI NUnit test runner (see Figure 5).
Figure 5
Now let's build the solution and run the test. When the GUI NUnit test runner is run for the first time, we need to load the test project (see Figure 6).
Figure 6
Then click on the Run button, and see the result of the tests (see Figure 7).
Figure 7
Let's do a final examination of test units. Let's supposed you have a failing test, but you cannot identify the problem by simply looking at the code, then you need to use the integration of Visual Studio.NET and the GUI NUnit test runner.
The first is to set the GUI NUnit test runner as the default test runner as we did before (see Figure 5). Second, you need to set a break point in the failing test code and start the application in Debug mode. Let's demonstrate this using the Enqueue3Dequeue2 test method (see Figure 8).
Figure 8
Then run the test suite in Debug mode (see Figure 9).
Figure 9
Conclusion
In this article, I've illustrated how programmers can use the test-driven development approach to implement and test a class library. Now you can apply this approach to your own business solutions.