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

Advanced Unit Testing, Part II - Core Implementation

Rate me:
Please Sign up or sign in to vote.
4.78/5 (34 votes)
22 Sep 200322 min read 208.7K   2.6K   198   18
This article illustrates how a unit test automation framework is implemented and continues the case study developed in Part I.

Image 1

Contents

Introduction

Part I

In Part II, I will be developing a unit test application similar to NUnit and use it to test the implementation of the case study on automatic part billing.

Why develop a unit test automation program again when several already exist?  Well, for one of the reasons that makes re-use difficult--I want something that I can call "my own".  Specifically, I'm looking at some of the requirements of an automated unit test application and seeing similarities to that of a scripting framework, so I thought it would be useful to take a cut at the idea of putting together some re-usable components that can be applied to both unit testing and scripting.

Since a cutesy acronym is the norm for applications like this, I'm going to call mine "Marc's Unit Test Extensions", or MUTE (I'm sure that'll generate lots of witty remarks in itself).  So, without further ado, I'm going to dive right into the organization, object models, and code.

Things You Will See

The code for MUTE covers some interesting topics:

  • Loading an assembly
  • Identifying the namespaces in the assemblies
  • Identifying the classes in the namespaces
  • Identifying the methods in the classes
  • Identifying attributes for classes and methods
  • Invoking methods using reflection
  • The difference between using delegate reflection and MethodInfo reflection with regards to capturing exceptions
  • Creating custom attributes with attribute parameters
  • Creating a notification event

Component Organization

MUTE is organized into several logical blocks:

Image 2

General Purpose Helper Library

This consists of a small set tools that I use in different applications.

String Helpers

The string helpers implement the several functions that I find useful because I typically parse strings knowing what character I'm looking for, not it's index.

  • LeftOf - everything to the left of the first occurrence of a character
public static string LeftOf(string src, char c)
{
  int idx=src.IndexOf(c);
  if (idx==-1)
  {
    return src;
  }

  return src.Substring(0, idx);
}
  • RightOf - everything to the right of the first occurrence of a character
public static string RightOf(string src, char c)
{
  int idx=src.IndexOf(c);
  if (idx==-1)
  {
    return "";
  }
  
  return src.Substring(idx+1);
}
  • LeftOfRightmostOf - everything to the left of the rightmost occurrence of a character
public static string LeftOfRightmostOf(string src, char c)
{
  int idx=src.LastIndexOf(c);
  if (idx==-1)
  {
    return src;
  }
  return src.Substring(0, idx);
}
  • RightOfRightmostOf - everything to the right of the rightmost occurrence of a character
public static string RightOfRightmostOf(string src, char c)
{
  int idx=src.LastIndexOf(c);
  if (idx==-1)
  {
    return src;
  }
  return src.Substring(idx+1);
}

Unit Test Attribute And Assertion Definitions

This assembly consists of the necessary definitions for implementing a unit test class.  The attributes that are associated with a unit test class and its methods must are defined.  Similarly, the assertions that the unit test methods can perform are defined.  This assembly is the only assembly that needs to be referenced by a unit test assembly.

Attribute Definitions

There are six attributes in the basic unit test:

  • TestFixture - applied to a class
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=true)]
public sealed class TestFixtureAttribute : Attribute
{
}
  • Test - applied to a method
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
public sealed class TestAttribute : Attribute
{
}
  • SetUp - applied to a method
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
public sealed class SetUpAttribute : Attribute
{
}
  • TearDown - applied to a method
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
public sealed class TearDownAttribute : Attribute
{
}
  • ExpectedException - applied to a method
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
public sealed class ExpectedExceptionAttribute : Attribute
{
  private Type expectedException;

  public Type ExceptionType 
  {
    get
    {
      return expectedException;
    }
  }

  public ExpectedExceptionAttribute(Type exception)
  {
    expectedException=exception;
  }
}
  • Ignore - applied to a method
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
public sealed class IgnoreAttribute : Attribute
{
  private string reason;

  public string Reason
  {
    get
    {
      return reason;
    }
  }

  public IgnoreAttribute(string reason)
  {
    this.reason=reason;
  }
}

Assertion Definitions

I've kept this really simple.  There's one assertion that tests for equality:

public class Assertion
{
  public static void Assert(bool test, string message)
  {
    if (!test)
    {
      Trace.Write(message);
      throw(new AssertionException(message));
    }
  }
}
And as you can see, it throws an exception when the test fails:
public class AssertionException : Exception
{
  public AssertionException(string message) : base(message)
  {
  }
}

Unit Test Core Engine

This component consists of two pieces:

  • general assembly parsing functions, which extract out the classes and methods in an assembly and their attributes
  • unit test automation, which consists of creating test fixtures, managing the classes and methods in a test fixture, and running the tests

This a large piece of code and will be discussed further in its own section.

Unit Test Windows Application

The Window Forms application consists of three sections:

  • a tree view showing all the unit test classes, their methods, and the specific test results
  • a progress bar providing the user with feedback as to the progress of the test cases
  • a summary of test results, showing the count of passed, ignored, and failed tests

While not as elaborate as NUnit (for example, changing the color of the progress bar requires using an owner draw bar!), several features of NUnit have been "borrowed" in my implementation, most notably, the use of green, yellow and red icons in the tree view to illustrate individual unit test results.  These results percolate up the tree to the assembly--ignored tests have priority over passed tests, and failed tests have priority over ignored and passed tests.

Also, the Windows application can only test one assembly at a time.  Eventually, this limitation will be corrected.

Loading An Assembly

Using the XMLRegistry class written by Nadeem Ghias (this was a really simple way to do this), the program loads the last known assembly:

XmlRegistry reg=new XmlRegistry("config.xml");
XmlRegistryKey regKey=reg.RootKey;
XmlRegistryKey lastSelectionKey=regKey.GetSubKey("LastSelections", true);
assemblyFilename=lastSelectionKey.GetStringValue("FileName", "");
if (assemblyFilename != "")
{
  LoadAssembly();
}

The assembly loading is straight forward and should be self-explanatory.

private void LoadAssembly()
{
  testRunner=new UTCore.TestRunner();
  testRunner.LoadAssembly(assemblyFilename);
  testRunner.ParseAssemblies();
  testRunner.testNotificationEvent+=new TestNotificationDelegate(TestCompleted);
  PopulateTreeView();
  lblNumTests.Text=testRunner.NumTests.ToString()+" tests.";
}

Notice that the test runner object is already capable of handling multiple unit test assemblies, so we're most of the way to upgrading the GUI at some point.

The Test Notification Event

This event is used as a notification that a test has completed.  It updates the progress bar, sets the image state of the associated method in the tree view to the test result state, and percolates the image state up the tree.

private void TestCompleted(TestAttribute ta)
{
  TreeNode tn=testToTreeMap[ta] as TreeNode;
  if (tn != null)
  {
    int testState=Convert.ToInt32(ta.State);
    tn.ImageIndex=testState;
    tn.SelectedImageIndex=testState;
    ++testCounts[testState];
    UpdateTreeParent(tn.Parent, testState);
    UpdateTestCounts();
    ++pbarTestProgress.Value;
  }
}

The TestAttribute.State enumeration is intentionally designed to track with the order of images in the tree view's image list, so we can take advantage of casting the enumeration directly to an image list index:

gray circle   Untested
green circle   Pass
yellow circle   Ignore
red circle   Fail

This same index is also used to update our test counts--the number of passed, ignored, and failed test counts.  Updating the GUI to reflect these test counts is trivial:

private void UpdateTestCounts()
{
  txtPassCount.Text=testCounts[1].ToString();
  txtIgnoreCount.Text=testCounts[2].ToString();
  txtFailCount.Text=testCounts[3].ToString();
}
I suppose a lot of people will scream at this kind of coupling, but it harks back to my firmware days when this sort of stuff was necessary and useful.  And you shouldn't scream anyways, because Extreme Programming (XP) says things should be done as simply as possible, because you can always fix it later with refactoring.  He he.  Nothing like using the arguments of "the other camp" when convenient to further my own goals, eh?

Percolating the test results up the tree is equally trivial.  The rule is simple--if the test state has higher priority (based on its ordinal value) than the current state, set the image index to the new state.  Note the abuse between ordinality, image index, test state, and priority.  I love it!

private void UpdateTreeParent(TreeNode tn, int testState)
{
  if (tn != null)
  {
    int currentState=tn.ImageIndex;
    if (testState > currentState)
    {
      tn.ImageIndex=testState;
      tn.SelectedImageIndex=testState;
      UpdateTreeParent(tn.Parent, testState);
    }
  }
}

Populating The Tree View

Populating the tree view consists of iterating through different collections:

  • The assemblies:
private void PopulateTreeView()
{
  tvUnitTests.Nodes.Clear();
  TreeNode tnAssembly;
  foreach(AssemblyItem ai in testRunner.AssemblyCollection.Values)
  {
    tnAssembly=tvUnitTests.Nodes.Add(ai.FullName);
    ...
  • The namespaces in each assembly:
...
UniqueCollection namespaces=new UniqueCollection();
foreach (TestFixture tf in testRunner.TestFixtures)
{
  if (tf.Assembly.FullName==tnAssembly.Text)
  {
    namespaces.Add(tf.Namespace, tf);
  }
}

foreach (DictionaryEntry item in namespaces)
{
  string ns=item.Key.ToString();
  TreeNode tnNamespace=tnAssembly.Nodes.Add(ns);
  tnNamespace.ImageIndex=0;
  tnNamespace.SelectedImageIndex=0;
...  
  • The classes in each namespace
...
UniqueCollection classes=new UniqueCollection();
foreach (TestFixture tf in testRunner.TestFixtures)
{
  if (tf.Namespace==ns)
  {
    foreach (TestAttribute ta in tf.Tests)
    {
      classes.Add(ta.TestClass.ToString(), tf);
    }
  }
}

foreach (DictionaryEntry itemClass in classes)
{
  string className=itemClass.Key.ToString();
  TreeNode tnClass=tnNamespace.Nodes.Add(className);
  tnClass.ImageIndex=0;
  tnClass.ImageIndex=0;
...
  • The methods in each class:
...
UniqueCollection methods=new UniqueCollection();
foreach(TestAttribute ta in tf.Tests)
{
  methods.Add(ta.TestMethod.ToString(), ta);
}

foreach (DictionaryEntry method in methods)
{
  string methodName=method.Key.ToString();
  TreeNode tnMethod=tnClass.Nodes.Add(methodName);
  tnMethod.ImageIndex=0;
  tnMethod.SelectedImageIndex=0;
  testToTreeMap.Add(method.Value, tnMethod);
  tnMethod.Tag=method.Value;          // currently not used
}
...

Running The Tests

When the user clicks on the Run button, the tests are run:

private void btnRun_Click(object sender, System.EventArgs e)
{
  pbarTestProgress.Value=0;
  pbarTestProgress.Maximum=testRunner.NumTests;
  testCounts[0]=0;
  testCounts[1]=0;
  testCounts[2]=0;
  testCounts[3]=0;
  testRunner.RunTests();
}

and the event notification handles all the rest.  That's it for the user interface!

The Unit Test Core Engine

A high level block diagram of the test apparatus can be illustrated as:

Image 3

In general, I have attempted to separate the assembly information from the unit test apparatus.  The TestRunner class maintains both a collection of assemblies and a collection of test fixtures.  Using information in the assemblies, it creates the test fixtures.  Each test fixture maintains information about the fixture--which methods are setup, teardown, ignored, etc.  Every attribute is associated with a class, and, expect for the TestFixtureAttribute, is also associated with a method.

The following UML diagram provides some detail to the high level diagram. I'll be discussing each of these classes in the following subsections.

Image 4

UniqueCollection

This is the base class for the key-value collections of different elements of an assembly.  The collection of assemblies itself must have unique keys (the assembly name), within an assembly, the namespace names are unique keys, within a namespace, the class names are unique keys, and finally, within a class the test methods names are unique.  Note that test methods are never overloaded because they all have the same signature (void x(), where "x" is the test method name).

The UniqueCollection class is trivial, in that it overrides the Add method and prevents duplicate keys from being inserted.  The primary purpose of this class is to improve the readability of the code that uses this class--the test for uniqueness is applied in the container rather than the code that uses the container.

public class UniqueCollection : Hashtable
{
  public override void Add(object key, object val)
  {
    if (!this.Contains(key))
    {
      base.Add(key, val);
    }
  }
Note that any duplication is simply ignored without checking if the associated value is still the same.  As Microsoft is fond of saying: "this is by design" (which I always read as "we're too lazy to do it right").

The remaining collection classes are really nothing more than stubs that help the readability of the code by using appropriate nomenclature for their contents:

  • AssemblyCollection
public class AssemblyCollection : UniqueCollection
{
  public AssemblyCollection()
  {
  }
}
  • NamespaceCollection
public class NamespaceCollection : UniqueCollection
{
  public NamespaceCollection()
  {
  }

  public void LoadClasses()
  {
    foreach (NamespaceItem ni in Values)
    {
      ni.LoadClasses();
    }
  }
}
  • ClassCollection
public class ClassCollection : UniqueCollection
{
  public ClassCollection()
  {
  }

  public void LoadMethods()
  {
    foreach (ClassItem ci in Values)
    {
      ci.LoadMethods();
    }
  }
}
  • MethodCollection
public class MethodCollection : UniqueCollection
{
  public MethodCollection()
  {
  }
}

The NamespaceCollection and ClassCollection implement a "helper iterator" to load class and method information, respectively.

AssemblyItem - Collecting Namespaces

This class loads the assembly and parses out the namespaces.  I've attempted to retain some generality to each of the parsing functions.  As a result, there is a bit of redundancy in each of the parsers, which could be avoided by coding a single, optimized, parser.  However, this approach is less general and less readable, and since a highly optimized algorithm isn't necessary, I chose an implementation that seemed more maintainable and readable.

Using an AssemblyItem object, the assembly, namespaces, classes, and methods can be loaded into the appropriate collections:

public void Load(string assemblyName)
{
  assembly=Assembly.LoadFrom(assemblyName);
}

public void LoadNamespaces()
{
  namespaceCollection=GetNamespaceCollection();
  namespaceCollection.DumpKeys("Namespaces:");
}

public void LoadClasses()
{
  namespaceCollection.LoadClasses();
}

public void LoadMethods()
{
  foreach(NamespaceItem ni in namespaceCollection.Values)
  {
    ni.ClassCollection.LoadMethods();  
  }
}

The most interesting feature in the AssemblyItem class is GetNamespaceCollection, which has to inspect the methods in the assembly in order to identify the namespace in which the method exists:

private NamespaceCollection GetNamespaceCollection()
{
  NamespaceCollection nc=new NamespaceCollection();
  Type[] types=assembly.GetTypes();
  foreach (Type type in types)
  {
    MethodInfo[] methods=type.GetMethods(Options.BindingFlags);
    foreach (MethodInfo methodInfo in methods)
    {
      string nameSpace=methodInfo.DeclaringType.Namespace;
      nc.Add(nameSpace, new NamespaceItem(assembly, nameSpace));
    }
  }
  return nc;
}

In this method, all types are inspected.  For each type, a collection of methods is obtained from the type that meets the criteria of being public instances:

private static BindingFlags bindingFlags=BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;

The DeclaredOnly flag is important so that only members at the level of the current type hierarchy are returned--otherwise, parent members are returned also, which includes System objects that we don't want.

The namespaces for the methods in the collection of methods is extracted and added to the namespace collection. Note that this method merely extracts namespace names. As I said earlier, the readability and structure of the code is more important to me than an optimized algorithm. Also note how the UniqueCollection container is used, so that duplicate namespace names can simply be ignored here.

NamespaceItem - Collecting Classes

Each NamespaceItem object is responsible for maintaining the collection of classes within that namespace.  The process of determining the collection of classes within a namespace is similar to loading the namespaces:

private ClassCollection GetClassCollection()
{
  ClassCollection classCollection=new ClassCollection();
  Type[] types=assembly.GetTypes();
  foreach (Type type in types)
  {
    if (type.IsClass)
    {
      MethodInfo[] methods=type.GetMethods(Options.BindingFlags);
      foreach (MethodInfo methodInfo in methods)
      {
        if (methodInfo.DeclaringType.Namespace==namespaceName)
        {
          string className=
            StringHelpers.RightOfRightmostOf(methodInfo.DeclaringType.FullName, '.');
          Type t=methodInfo.DeclaringType;
          classCollection.Add(className,
            new ClassItem(assembly, namespaceName, className, type));
        }
      }
    }
  }
  return classCollection;
}

First, all the types of the assembly are inspected. For each type that is a class, the collection of methods are inspected. The class name is extracted from each method that belongs to the specific namespace and added to the class collection (again, ignoring duplicate class entries).

ClassItem - Collecting Methods

Acquiring the collection of methods in a specific class is a similar process:

private MethodCollection GetMethodCollection()
{
  MethodCollection methodCollection=new MethodCollection();
  Type[] types=assembly.GetTypes();
  foreach (Type type in types)
  {
    MethodInfo[] methods=type.GetMethods(Options.BindingFlags);
    foreach (MethodInfo methodInfo in methods)
    {
      if (methodInfo.DeclaringType.Namespace==namespaceName)
      {
        string className=StringHelpers.RightOfRightmostOf(
          methodInfo.DeclaringType.FullName, '.');
        if (className==this.className)
        {
          string methodName=methodInfo.Name;
          methodCollection.Add(methodName,
            new MethodItem(assembly, namespaceName, className,
            methodName, methodInfo));
        }
      }
    }
  }
  return methodCollection;
}

Here, we can see a similar process again--the assembly types are inspected, and those that are qualified with the correct namespace and class names are added to the method collection for that class.

TestRunner - Creating Test Fixtures Out Of The Assembly Information

This class maintains a collection of assemblies and is responsible for running the unit tests.  Assemblies are loaded into the TestRunner, which maintains a collection of these assemblies:

public void LoadAssembly(string file)
{
  AssemblyItem ai=new AssemblyItem();
  ai.Load(file);
  ai.LoadNamespaces();
  ai.LoadClasses();
  ai.LoadMethods();
  assemblyCollection.Add(file, ai);
}

The TestRunner is responsible for converting the assembly information into test fixtures. This consists of walking through the assembly-namespace-class-method tree and inspecting the attributes associated with each class and method: 

public void ParseAssemblies()
{
  numTests=0;

  foreach (AssemblyItem ai in assemblyCollection.Values)
  {
    foreach (NamespaceItem ni in ai.NamespaceCollection.Values)
    {
      foreach (ClassItem ci in ni.ClassCollection.Values)
      {
        TestFixture tf=new TestFixture();
        // ... Parse Class Attributes ...
        if (tf.HasTestFixture)
        {
          foreach(MethodItem mi in ci.MethodCollection.Values)
          {
            // ... Parse Method Attributes ...
          }
          testFixtureList.Add(tf);
          numTests+=tf.NumTests;
        }
      }
    }
  }
}

Because there is a one-to-one association between a class and a test fixture, the algorithm creates a test fixture for each class and throws it away if the class ends up not having a test fixture attribute.

Once the test fixtures are created, running the tests is a simple matter of working through each test fixture and telling it to run the tests in the fixture:

public void RunTests()
{
  foreach(TestFixture tf in testFixtureList)
  {
    tf.RunTests(testNotificationEvent);
  }
}

Abstracting Attributes

I wanted a system that handled attributes with some automation, so that instead of writing a big switch statement to handle the different attributes, the "smarts" are put into the attributes themselves.  As illustrated in the UML diagram, all of the attributes are classes derived from TestUnitAttribute.  The derived attributes are instantiated using the Activator function.  This is illustrated in the section of code that registers attributes associated with a class:

foreach (object attr in ci.Attributes)
{
  // verify that attribute class is "UnitTest"
  string attrStr=attr.ToString();
  attrStr=StringHelpers.RightOfRightmostOf(attrStr, '.');
  Trace.WriteLine("Class: "+ci.ToString()+", Attribute: "+attrStr);
  try
  {
    Type t=Type.GetType("UTCore."+attrStr);
    TestUnitAttribute tua=Activator.CreateInstance(t) as TestUnitAttribute;
    tua.Initialize(ci, null, attr);
    tua.SelfRegister(tf);
  }
  catch(TypeLoadException)
  {
    Trace.WriteLine("Attribute "+attrStr+"is unknown");
  }
}

This code instantiates an attribute class found in the UTCore assembly having the same name as the attribute.  As these attributes are all derived from TestUnitAttribute, we can now work with the base class to initialize it with some tracking information and tell the attribute to register itself with the test fixture.  The same process takes place for parsing method attributes:

foreach (object attr in mi.Attributes)
{
  // verify that attribute class is "UnitTest"
  string attrStr=attr.ToString();
  attrStr=StringHelpers.RightOfRightmostOf(attrStr, '.');
  Trace.WriteLine("Method: "+mi.ToString()+", Attribute: "+attrStr);
  try
  {
    Type t=Type.GetType("UTCore."+attrStr);
    TestUnitAttribute tua=Activator.CreateInstance(t) as TestUnitAttribute;
    tua.Initialize(ci, mi, attr);
    tua.SelfRegister(tf);
  }
  catch(TypeLoadException)
  {
    Trace.WriteLine("Attribute "+attrStr+"is unknown");
  }
}

These two processes are identical except for the attribute source and a debugging statement.  The advantage of this implementation is that it keeps the "knowledge" of what the attribute does within the attribute itself.  The TestRunner doesn't care what the attribute does, it merely instantiates them for the class and its methods.

TestUnitAttribute Classes

In the basic MUTE, there are six attributes. You can see from the code how each attribute registers itself differently with the test fixture (which we'll look at next).

  • TestFixtureAttribute
public class TestFixtureAttribute : TestUnitAttribute
{
  public TestFixtureAttribute()
  {
  }

  public override void SelfRegister(TestFixture tf)
  {
    tf.AddTestFixtureAttribute(this);
  }

}
  • TestAttribute
public class TestAttribute : TestUnitAttribute
{
  public enum TestState
  {
    Untested=0,
    Pass,
    Ignore,
    Fail,
  }

  TestState state;

  public TestState State
  {
    get
    {
      return state;
    }
    set
    {
      state=value;
    }
  }

  public TestAttribute()
  {
    state=TestState.Untested;
  }

  public override void SelfRegister(TestFixture tf)
  {
    tf.AddTestAttribute(this);
  }
}
  • SetUpAttribute
public class SetUpAttribute : TestUnitAttribute
{
  public SetUpAttribute()
  {
  }

  public override void SelfRegister(TestFixture tf)
  {
    tf.AddSetUpAttribute(this);
  }
}
  • TearDownAttribute
public class TearDownAttribute : TestUnitAttribute
{
  public TearDownAttribute()
  {
  }

  public override void SelfRegister(TestFixture tf)
  {
    tf.AddTearDownAttribute(this);
  }
}
  • ExpectedExceptionAttribute
public class ExpectedExceptionAttribute : TestUnitAttribute
{
  public ExpectedExceptionAttribute()
  {
  }

  public override void SelfRegister(TestFixture tf)
  {
    mi.ExpectedException=attr as UnitTest.ExpectedExceptionAttribute;
  }
}
  • IgnoreAttribute
public class IgnoreAttribute : TestUnitAttribute
{
  public IgnoreAttribute()
  {
  }

  public override void SelfRegister(TestFixture tf)
  {
    mi.Ignore=true;
  }
}

TestFixture - Running The Tests

This class is a physical representation of the concept of a test fixture.  It manages the associated test fixture attribute (which is associated with the test fixture class), the setup and teardown methods to run for each test (one of each per test fixture), and a list of tests to run.  The primary purpose of the test fixture is to run the tests:

public void RunTests(TestNotificationDelegate testNotificationEvent)
{
  object instance=tfa.CreateClass();
  foreach (TestAttribute ta in testList)
  {
    if (!ta.IgnoreTest())
    {
      try
      {
        if (sua != null) sua.Invoke(instance);
        ta.Invoke(instance);
        // If we get here, the test did not throw an exception.
        // Was it supposed too?
        if (ta.ExpectedExceptionType != null)
        {
          Trace.WriteLine("***Fail***: "+ta.TestMethod.ToString()+
            " Expected exception not encountered");
          ta.State=TestAttribute.TestState.Fail;
        }
        else
        {
          Trace.WriteLine("***Pass***: "+ta.TestMethod.ToString());
          ta.State=TestAttribute.TestState.Pass;
        }
      }

      catch(UnitTest.AssertionException e)
      {
        Trace.WriteLine("***Fail***: "+ta.TestMethod.ToString()+
          " Exception="+e.Message);
        ta.State=TestAttribute.TestState.Fail;
      }

      catch(Exception e)
      {
        if (e.GetType() != ta.GetExpectedExceptionType())
        {
          Trace.WriteLine("***Fail***: "+ta.TestMethod.ToString()+
            " Exception="+e.Message);
          ta.State=TestAttribute.TestState.Fail;
        }
        else
        {
          Trace.WriteLine("***Pass***: "+ta.TestMethod.ToString()+
            " Exception="+e.Message);
          ta.State=TestAttribute.TestState.Pass;
        }
      }
      finally
      {
        if (tda != null) tda.Invoke(instance);
      }
    }
    else
    {
      Trace.WriteLine("***Ignore***: "+ta.TestMethod.ToString());
      ta.State=TestAttribute.TestState.Ignore;
    }
    testNotificationEvent(ta);
  }
}

As we can see from this code, for each test the setup method (if it exists) is invoked, then the test, then any teardown method.  Any exceptions are compared to the expected exception.  Since failed assertions generate an exception, assertion failure is handled with this mechanism as well.  Ignored tests are, well, ignored!  The resulting state (pass, ignore, or fail) is stored in the test attribute and the test notification event is fired.

Invoking The Test Methods

The tests are run from the MethodItem class:

public void Invoke(object classInstance)
{
  // Delegates requires that methods have a specific signature and are public.
  // Delegates are faster than "methodInfo.Invoke".

  Type utdType=typeof(UnitTestDelegate);
  UnitTestDelegate utd=Delegate.CreateDelegate(utdType, classInstance, methodName)
    as UnitTestDelegate;
  try
  {
    utd();
  }
  catch(Exception e)
  {
    throw(e);
  }
}

Given that the tests are all have the same signature (a public method which returns void and has no parameters), we can create a delegate of the same type:

private delegate void UnitTestDelegate();

and execute the delegate.  Catching the exception is simple--it is merely re-thrown to the test fixture which handles it.

However, at some point I'd like to extend MUTE so that it doesn't have to rely on a specific method signature.  When this is done, the way the exception is handled is going to have to be changed, along with the way the method is invoked.  This code, which currently commented out, illustrates the difference:

// The invoke function allows us to call functions with different parameter lists,
// and ones that are not public.
// However, this changes how we handle exceptions

//  try
//  {
//    methodInfo.Invoke(classInstance,
//      BindingFlags.Public | BindingFlags.NonPublic |
//      BindingFlags.InvokeMethod | BindingFlags.Static,
//      null, null, null);
//  }
//  catch(Exception e)
//  {
//    throw(e.InnerException);
//  }
Note that in this code the InnerException is thrown.

The Case Study

In Part I, I developed a case study and wrote the unit tests for it.  The astute reader will note that I made a mistake in the test cases, leaving off the text to display when the program asserts, which I fixed in the code you can download here.  In fact, there were a lot of problems with the code, which just shows that I really shouldn't write code without a compiler handy to make sure I don't do a bunch of stupid things.  Of course, the way the C# compiler works, you have to fix some things first before the compiler can chunk along far enough to find the next set of problems!

Compiler Errors

Somewhere, I read that the first test in any unit testing is, when you try to compile your unit tests, you get a bunch of compiler errors.  Woohoo!  That wasn't hard to do!

Stubs In General

Now that we have achieved that monumentus event, the next step is to write the classes and stubs for all the methods.  At the end of implementing the method stubs, the unit tests should run, but every one of them should fail.

Exceptions

The first step is to implement the exceptions that the unit tests call for:

public class DuplicatePartException : Exception
{
}

public class UnassignedPartException : Exception
{
}

public class BadChargeSlipNumberException : Exception
{
}

public class UnassignedChargeException : Exception
{
}

public class BadWorkOrderNumberException : Exception
{
}

public class DuplicateChargeSlipException : Exception
{
}

public class UnassignedChargeSlipException : Exception
{
}

public class UnassignedWorkOrderException : Exception
{
}

public class PartNotFromVendorException : Exception
{
}

public class DifferentVendorException : Exception
{
}

public class IncorrectChargeSlipException : Exception
{
}

public class UnassignedInvoiceException : Exception
{
}

Getters And Setters

OK, do we write stubs for getters and setters, or just implement them?  And if we write stubs for them, what values do we return?  This is one area where unit testing breaks down.  While some may argue with me, I definitely think that in the XP paradigm there is a point to writing getter/setter unit tests.  This point is not so much to test some trivial code, but rather as a documentation tool--that the class being tested must implement getter/setter functionality.  This goes along with "the code is the documentation" philosophy.  Even though unit testing getter/setter functions is sort of pointless, is provides a clue to the implementer as to the getters/setters that the class implements, their nomenclature, and how they are expected to work.

Returning back to the question of writing stubs for getters and setters, I personally feel that it's a waste of time to write "do nothing" or "do the wrong thing" stubs, simply to test the unit test.  It seems a lot more productive to write the real functionality (when trivial).  This means that the unit test will pass, which is sufficient, in my mind.

Constructors And Member Initialization

Members are automatically initialized to certain values in debug mode.

Compiler Warnings

In addition to eliminating compiler errors, warnings need to be eliminated also.  The most common kind of warning that I encountered is:

"x is never assigned to, and will always have its default value null"

This brings up another implementation decision.  For collections like ArrayList and Hashtable, do you assign them to null or instantiate the collection?  As with setters/getters, it seems ridiculous to not instantiate the collection when it is trivial to do so.

Tests With Multiple Asserts

The tests that I created have, in some cases, multiple assert statements.  After implementing the stubs, I realized that this is not particularly a good idea.  When the test passes, I know that none of the asserts failed.  However, when one statement asserts, I only know that none of the previous asserts failed, but I know nothing of the asserts below the one that failed.  Therefore, the unit is strong in testing for success but weak in identifying all the failures.

The Class Stubs

The stubs are all very basic and, for the most part, ensure that the unit tests fail.  The exceptions are the setter and getter methods that have already been implemented with real code.  In this section, I'm going to review the (now fixed) unit tests and show the corresponding stub for each class.

Part, Unit Test

[TestFixture]
public class PartTest
{
  [Test]
  public void ConstructorInitialization()
  {
    Part part=new Part();
    Assertion.Assert(part.VendorCost==0, "VendorCost is not zero.");
    Assertion.Assert(part.Taxable==false, "Taxable is not false.");
    Assertion.Assert(part.InternalCost==0, "InternalCost is not zero.");
    Assertion.Assert(part.Markup==0, "Markup is not zero.");
    Assertion.Assert(part.Number=="", "Number is not an empty string.");
  }

  [Test]
  public void SetVendorInfo()
  {
    Part part=new Part();
    part.Number="FIG 4RAC #R11T";
    part.VendorCost=12.50;
    part.Taxable=true;
    part.InternalCost=13.00;
    part.Markup=2.0;
  
    Assertion.Assert(part.Number=="FIG 4RAC #R11T", "Number did not get set.");
    Assertion.Assert(part.VendorCost==12.50, "VendorCost did not get set.");
    Assertion.Assert(part.Taxable==true, "Taxable did not get set.");
    Assertion.Assert(part.InternalCost==13.00, "InternalCost did not get set.");
    Assertion.Assert(part.Markup==2.0, "Markup did not get set.");
  }
}

Part, Stub

public class Part
{
  private double vendorCost;
  private bool taxable;
  private double internalCost;
  private double markup;
  private string number;

  public double VendorCost
  {
    get {return vendorCost;}
    set {vendorCost=value;}
  }

  public bool Taxable
  {
    get {return taxable;}
    set {taxable=value;}
  }

  public double InternalCost
  {
    get {return internalCost;}
    set {internalCost=value;}
  }

  public double Markup
  {
    get {return markup;}
    set {markup=value;}
  }

  public string Number
  {
    get {return number;}
    set {number=value;}
  }
}

Vendor, Unit Test

[TestFixture]
public class VendorTest
{
  private Vendor vendor;

  [SetUp]
  public void VendorSetUp()
  {
    vendor=new Vendor();
  }

  [Test]
  public void ConstructorInitialization()
  {
    Assertion.Assert(vendor.Name=="", "Name is not an empty string.");
    Assertion.Assert(vendor.PartCount==0, "PartCount is not zero.");
  }

  [Test]
  public void VendorName()
  {
    vendor.Name="Jamestown Distributors";
    Assertion.Assert(vendor.Name=="Jamestown Distributors", "Name did not get set.");
    }

  [Test]
  public void AddUniqueParts()
  {
    CreateTestParts();
    Assertion.Assert(vendor.PartCount==2, "PartCount is not 2.");
  }

  [Test]
  public void RetrieveParts()
  {
    CreateTestParts();
    Part part;
    part=vendor.Parts[0];
    Assertion.Assert(part.Number=="BOD-13-25P", "PartNumber is wrong.");
    part=vendor.Parts[1];
    Assertion.Assert(part.Number=="BOD-13-33P", "PartNumber is wrong.");
  }

  [Test, ExpectedException(typeof(DuplicatePartException))]
  public void DuplicateParts()
  {
    Part part=new Part();
    part.Number="Same Part Number";
    vendor.Add(part);
    vendor.Add(part);
  }

  [Test, ExpectedException(typeof(UnassignedPartException))]
  public void UnassignedPartNumber()
  {
    Part part=new Part();
    vendor.Add(part);
  }

  void CreateTestParts()
  {
    Part part1=new Part();
    part1.Number="BOD-13-25P";
    vendor.Add(part1);
  
    Part part2=new Part();
    part2.Number="BOD-13-33P";
    vendor.Add(part2);
  }  
}

Vendor, Stub

public class Vendor
{
  private string name;
  private PartsArray partsArray;
  private PartsHashtable parts;

  public string Name
  {
    get {return name;}
    set {name=value;}
  }

  public int PartCount
  {
    get {return parts.Count;}
  }

  public PartsHashtable Parts
  {
    get {return parts;}
  }

  public void Add(Part p)
  {
  }

  public Vendor()
  {
    parts=new PartsHashtable();
    partsArray=new PartsArray();
  }
}

Charge, Unit Test

[TestFixture]
public class ChargeTest
{
  [Test]
  public void ConstructorInitialization()
  {
    Charge charge=new Charge();
    Assertion.Assert(charge.Description=="", "Description is not an empty string.");
    Assertion.Assert(charge.Amount==0, "Amount is not zero.");
  }
  
  [Test]
  public void SetChargeInfo()
  {
    Charge charge=new Charge();
    charge.Description="Freight";
    charge.Amount=8.50;
  
    Assertion.Assert(charge.Description=="Freight", "Description is not set.");
    Assertion.Assert(charge.Amount==8.50, "Amount is not set correctly.");
  }
}

Charge, Stub

public class Charge
{
  private string description;
  private double amount;

  public string Description
  {
    get {return description;}
    set {description=value;}
  }

  public double Amount
  {
    get {return amount;}
    set {amount=value;}
  }
}

ChargeSlip, Unit Test

[TestFixture]
public class ChargeSlipTest
{
  private ChargeSlip chargeSlip;

  [SetUp]
  public void SetUp()
  {
    chargeSlip=new ChargeSlip();
  }

  [Test]
  public void ConstructorInitialization()
  {
    Assertion.Assert(chargeSlip.Number=="", "Number is not initialized correctly.");
    Assertion.Assert(chargeSlip.PartCount==0, "PartCount is not zero.");
    Assertion.Assert(chargeSlip.ChargeCount==0, "ChargeCount is not zero.");
  }

  [Test]
  public void ChargeSlipNumberAssignment()
  {
    chargeSlip.Number="123456";
    Assertion.Assert(chargeSlip.Number=="123456", "Number is not set correctly.");
    }

  [Test, ExpectedException(typeof(BadChargeSlipNumberException))]
  public void BadChargeSlipNumber()
  {
    chargeSlip.Number="12345";      // must be six digits or letters
  }

  [Test]
  public void AddPart()
  {
    Part part=new Part();
    part.Number="VOD-13-33P";
    chargeSlip.Add(part);
    Assertion.Assert(chargeSlip.PartCount==1, "PartCount is wrong.");
  }

  [Test]
  public void AddCharge()
  {
    Charge charge=new Charge();
    charge.Description="Freight";
    charge.Amount=10.50;    
    chargeSlip.Add(charge);
    Assertion.Assert(chargeSlip.ChargeCount==1, "ChargeCount is wrong.");
  }

  [Test]
  public void RetrievePart()
  {
    Part part=new Part();
    part.Number="VOD-13-33P";
    chargeSlip.Add(part);
    Part p2=chargeSlip.Parts[0];
    Assertion.Assert(p2.Number==part.Number, "Part numbers do not match.");
  }

  [Test]
  public void RetrieveCharge()
  {
    Charge charge=new Charge();
    charge.Description="Freight";
    charge.Amount=10.50;    
    chargeSlip.Add(charge);
    Charge c2=chargeSlip.Charges[0];
    Assertion.Assert(c2.Description==charge.Description,
      "Descriptions do not match.");
  }

  [Test, ExpectedException(typeof(UnassignedPartException))]
  public void AddUnassignedPart()
  {
    Part part=new Part();
    chargeSlip.Add(part);
  }

  [Test, ExpectedException(typeof(UnassignedChargeException))]
  public void UnassignedCharge()
  {
    Charge charge=new Charge();
    chargeSlip.Add(charge);
  }
}

ChargeSlip, Stub

public class ChargeSlip
{
  private string number;
  private PartsArray parts;
  private ChargesArray charges;

  public string Number
  {
    get {return number;}
    set {number=value;}
  }

  public int PartCount
  {
    get {return parts.Count;}
  }

  public int ChargeCount
  {
    get {return charges.Count;}
  }

  public void Add(Part p)
  {
  }

  public void Add(Charge c)
  {
  }

  public PartsArray Parts
  {
    get {return parts;}
  }

  public ChargesArray Charges
  {
    get {return charges;}
  }

  public ChargeSlip()
  {
    parts=new PartsArray();
    charges=new ChargesArray();
  }
}

WorkOrder, Unit Test

[TestFixture]
public class WorkOrderTest
{
  private WorkOrder workOrder;

  [SetUp]
  public void WorkOrderSetUp()
  {
    workOrder=new WorkOrder();
  }

  [Test]
  public void ConstructorInitialization()
  {
    Assertion.Assert(workOrder.Number=="", "Number not initialized.");
    Assertion.Assert(workOrder.ChargeSlipCount==0, "ChargeSlipCount not initialized.");
  }

  [Test]
  public void WorkOrderNumber()
  {
    workOrder.Number="112233";
    Assertion.Assert(workOrder.Number=="112233", "Number not set.");
    }

  [Test, ExpectedException(typeof(BadWorkOrderNumberException))]
  public void BadWorkOrderNumber()
  {
    workOrder.Number="12345";
  }

  [Test]
  public void AddChargeSlip()
  {
    ChargeSlip chargeSlip=new ChargeSlip();
    chargeSlip.Number="123456";
    workOrder.Add(chargeSlip);
    Assertion.Assert(workOrder.ChargeSlipCount==1, "ChargeSlip not added.");
  }

  [Test]
  public void RetrieveChargeSlip()
  {
    ChargeSlip chargeSlip=new ChargeSlip();
    chargeSlip.Number="123456";
    workOrder.Add(chargeSlip);
    ChargeSlip cs2=workOrder.ChargeSlips[0];
    Assertion.Assert(chargeSlip.Number==cs2.Number, "ChargeSlip numbers do not match.");
  }

  [Test, ExpectedException(typeof(DuplicateChargeSlipException))]
  public void DuplicateChargeSlip()
  {
    ChargeSlip chargeSlip=new ChargeSlip();
    chargeSlip.Number="123456";
    workOrder.Add(chargeSlip);
    workOrder.Add(chargeSlip);
  }

  [Test, ExpectedException(typeof(UnassignedChargeSlipException))]
  public void UnassignedChargeSlipNumber()
  {
    ChargeSlip chargeSlip=new ChargeSlip();
    workOrder.Add(chargeSlip);
  }
}

WorkOrder, Stub

public class WorkOrder
{
  private string number;
  private ChargeSlipHashtable chargeSlips;
  private ChargeSlipArray chargeSlipsArray;

  public string Number
  {
    get {return number;}
    set {number=value;}
  }

  public int ChargeSlipCount
  {
    get {return chargeSlips.Count;}
  }

  public ChargeSlipArray ChargeSlips
  {
    get {return chargeSlipsArray;}
  }

  public WorkOrder()
  {
    chargeSlips=new ChargeSlipHashtable();
    chargeSlipsArray=new ChargeSlipArray();
  }

  public void Add(ChargeSlip cs)
  {
  }
}

Invoice, Unit Test

[TestFixture]
public class InvoiceTest
{
  private Invoice invoice;

  [SetUp]
  public void InvoiceSetUp()
  {
    invoice=new Invoice();
  }

  [Test]
  public void ConstructorInitialization()
  {
    Assertion.Assert(invoice.Number=="", "Number not initialized.");
    Assertion.Assert(invoice.ChargeCount==0, "ChargeCount not initialized.");
    Assertion.Assert(invoice.Vendor==null, "Vendor not initialized.");
  }

  [Test]
  public void InvoiceNumber()
  {
    invoice.Number="112233";
    Assertion.Assert(invoice.Number=="112233", "Number not set.");
    }

  [Test]
  public void InvoiceVendor()
  {
    Vendor vendor=new Vendor();
    vendor.Name="Nantucket Parts";
    invoice.Vendor=vendor;
    Assertion.Assert(invoice.Vendor.Name==vendor.Name, "Vendor name not set.");
  }

  [Test]
  public void AddCharge()
  {
    Charge charge=new Charge();
    charge.Description="Freight";
    invoice.Add(charge);
    Assertion.Assert(invoice.ChargeCount==1, "Charge count wrong.");
  }

  [Test]
  public void RetrieveCharge()
  {
    Charge charge=new Charge();
    charge.Description="123456";
    invoice.Add(charge);
    Charge c2=invoice.Charges[0];
    Assertion.Assert(charge.Description==c2.Description,
      "Charge description does not match.");
  }

  [Test, ExpectedException(typeof(UnassignedChargeException))]
  public void UnassignedChargeNumber()
  {
    Charge charge=new Charge();
    invoice.Add(charge);
  }
}

Invoice, Stub

public class Invoice
{
  private string number;
  private Vendor vendor;
  private ChargesArray charges;

  public string Number
  {
    get {return number;}
    set {number=value;}
  }

  public Vendor Vendor
  {
    get {return vendor;}
    set {vendor=value;}
  }

  public int ChargeCount
  {
    get {return charges.Count;}
  }

  public ChargesArray Charges
  {
    get {return charges;}
  }

  public Invoice()
  {
    charges=new ChargesArray();
  }

  public void Add(Charge c)
  {
  }
}

Customer, Unit Test

[TestFixture]
public class CustomerTest
{
  private Customer customer;

  [SetUp]
  public void CustomerSetUp()
  {
    customer=new Customer();
  }

  [Test]
  public void ConstructorInitialization()
  {
    Assertion.Assert(customer.Name=="", "Name not initialized.");
    Assertion.Assert(customer.WorkOrderCount==0, "WorkOrderCount not initialized.");
  }

  [Test]
  public void CustomerName()
  {
    customer.Name="Marc Clifton";
    Assertion.Assert(customer.Name=="Marc Clifton", "Name not set.");
  }

  [Test]
  public void AddWorkOrder()
  {
    WorkOrder workOrder=new WorkOrder();
    workOrder.Number="123456";
    customer.Add(workOrder);
    Assertion.Assert(customer.WorkOrderCount==1, "Work order not added.");
  }

  [Test]
  public void RetrieveWorkOrder()
  {
    WorkOrder workOrder=new WorkOrder();
    workOrder.Number="123456";
    customer.Add(workOrder);
    WorkOrder wo2=customer.WorkOrders[0];
    Assertion.Assert(workOrder.Number==wo2.Number, "WorkOrder numbers do not match.");
  }

  [Test, ExpectedException(typeof(UnassignedWorkOrderException))]
  public void UnassignedWorkOrderNumber()
  {
    WorkOrder workOrder=new WorkOrder();
    customer.Add(workOrder);
  }
}

Customer, Stub

public class Customer
{
  private string name;
  private WorkOrderArray workOrders;

  public string Name
  {
    get {return name;}
    set {name=value;}
  }

  public int WorkOrderCount
  {
    get {return workOrders.Count;}
  }

  public WorkOrderArray WorkOrders
  {
    get {return workOrders;}
  }

  public Customer()
  {
    workOrders=new WorkOrderArray();
  }

  public void Add(WorkOrder wo)
  {
  }
}

PurchaseOrder, Unit Test

[TestFixture]
public class PurchaseOrderTest
{
  private PurchaseOrder po;
  private Vendor vendor;

  [SetUp]
  public void PurchaseOrderSetUp()
  {
    po=new PurchaseOrder();
    vendor=new Vendor();
    vendor.Name="West Marine";
    po.Vendor=vendor;
  }

  [Test]
  public void ConstructorInitialization()
  {
    PurchaseOrder po=new PurchaseOrder();
    Assertion.Assert(po.Number=="", "Number not initialized.");
    Assertion.Assert(po.PartCount==0, "PartCount not initialized.");
    Assertion.Assert(po.ChargeCount==0, "ChargeCount not initizlied.");
    Assertion.Assert(po.Invoice==null, "Invoice not initialized.");
    Assertion.Assert(po.Vendor==null, "Vendor not initialized.");
  }

  [Test]
  public void PONumber()
  {
    po.Number="123456";
    Assertion.Assert(po.Number=="123456", "Number not set.");
  }

  [Test]
  public void AddPart()
  {
    WorkOrder workOrder=new WorkOrder();
    workOrder.Number="123456";
    Part part=new Part();
    part.Number="112233";
    vendor.Add(part);
    po.Add(part, workOrder);
    WorkOrder wo2;
    Part p2;
    po.GetPart(0, out p2, out wo2);
    Assertion.Assert(p2.Number==part.Number, "Part number does not match.");
    Assertion.Assert(wo2.Number==workOrder.Number, "Work order number does not match.");
  }

  [Test, ExpectedException(typeof(PartNotFromVendorException))]
  public void AddPartNotFromVendor()
  {
    WorkOrder workOrder=new WorkOrder();
    workOrder.Number="123456";
    Part part=new Part();
    part.Number="131133";
    po.Add(part, workOrder);
  }

  [Test, ExpectedException(typeof(DifferentVendorException))]
  public void AddInvoiceFromDifferentVendor()
  {
    Vendor vendor1=new Vendor();
    vendor1.Name="ABC Co.";
    po.Vendor=vendor1;
    Invoice invoice=new Invoice();
    invoice.Number="123456";
    Vendor vendor2=new Vendor();
    vendor2.Name="XYZ Inc.";
    invoice.Vendor=vendor2;
    po.Invoice=invoice;
  }

  [Test, ExpectedException(typeof(UnassignedWorkOrderException))]
  public void UnassignedWorkOrderNumber()
  {
    WorkOrder workOrder=new WorkOrder();
    Part part=new Part();
    part.Number="112233";
    po.Add(part, workOrder);
  }

  [Test, ExpectedException(typeof(UnassignedPartException))]
  public void UnassignedPartNumber()
  {
    WorkOrder workOrder=new WorkOrder();
    workOrder.Number="123456";
    Part part=new Part();
    po.Add(part, workOrder);
  }

  [Test, ExpectedException(typeof(UnassignedInvoiceException))]
  public void UnassignedInvoiceNumber()
  {
    Invoice invoice=new Invoice();
    po.Invoice=invoice;
  }

  [Test]
  public void ClosePO()
  {
    WorkOrder wo1=new WorkOrder();
    WorkOrder wo2=new WorkOrder();

    wo1.Number="000001";
    wo2.Number="000002";

    Part p1=new Part();
    Part p2=new Part();
    Part p3=new Part();

    p1.Number="A";
    p1.VendorCost=15;

    p2.Number="B";
    p2.VendorCost=20;

    p3.Number="C";
    p3.VendorCost=25;

    vendor.Add(p1);
    vendor.Add(p2);
    vendor.Add(p3);

    po.Add(p1, wo1);
    po.Add(p2, wo1);
    po.Add(p3, wo2);

    Charge charge=new Charge();
    charge.Description="Freight";
    charge.Amount=10.50;
    po.Add(charge);

    po.Close();

    // one charge slip should be added to both work orders
    Assertion.Assert(wo1.ChargeSlipCount==1,
      "First work order: ChargeSlipCount not 1.");
    Assertion.Assert(wo2.ChargeSlipCount==1,
      "Second work order: ChargeSlipCount not 1.");

    ChargeSlip cs1=wo1.ChargeSlips[0];
    ChargeSlip cs2=wo2.ChargeSlips[0];

    // three charges should exist for charge slip #1: two parts and one freight charge
    Assertion.Assert(cs1.PartCount + cs1.ChargeCount==3,
      "Charge slip 1: doesn't have three charges.");

    // the freight for CS1 should be 10.50 * (15+20)/(15+20+25) = 6.125
    Assertion.Assert(cs1.Charges[0].Amount==6.125,
      "Charge slip 1: charge not the correct amount.");

    // two charges should exist for charge slip #2: one part and one freight charge
    Assertion.Assert(cs2.PartCount + cs2.ChargeCount==2,
      "Charge slip 2: doesn't have two charges.");

    // the freight for CS2 should be 10.50 * 25/(15+20+25) = 4.375  (also = 10.50-6.125)
    Assertion.Assert(cs2.Charges[0].Amount==4.375,
      "Charge slip 2: charge not the correct amount.");

    // while we have a unit test that verifies that parts are added to charge slips
    // correctly, we don't have a unit test to verify that the purchase order
    // Close process does this correctly.

    Part cs1p1=cs1.Parts[0];
    Part cs1p2=cs1.Parts[1];
    if (cs1p1.Number=="A")
    {
      Assertion.Assert(cs1p1.VendorCost==15,
        "Charge slip 1, vendor cost not correct for part A.");
    }
    else if (cs1p1.Number=="B")
    {
      Assertion.Assert(cs1p1.VendorCost==20,
        "Charge slip 1, vendor cost not correct for part B.");
    }
    else
    {
      throw(new IncorrectChargeSlipException());
    }

    Assertion.Assert(cs1p1.Number != cs1p2.Number,
      "Charge slip part numbers are not unique.");

    if (cs1p2.Number=="A")
    {
      Assertion.Assert(cs1p2.VendorCost==15,
        "Charge slip 1, vendor cost is not correct for part A.");
    }
    else if (cs1p2.Number=="B")
    {
      Assertion.Assert(cs1p2.VendorCost==20,
        "Charge slip 1, vendor cost is not correct for part B.");
    }
    else
    {
      throw(new IncorrectChargeSlipException());
    }

    Assertion.Assert(cs2.Parts[0].Number=="C",
      "Charge slip 2, part number is not correct.");
    Assertion.Assert(cs2.Parts[0].VendorCost==25,
      "Charge slip 2, vendor cost is not correct for part C.");    
  }
}

PurchaseOrder, Stub

public class PurchaseOrder
{
  private string number;
  private Vendor vendor;
  private Invoice invoice;
  private PartsHashtable parts;
  private ChargesArray charges;

  public string Number
  {
    get {return number;}
    set {number=value;}
  }

  public Invoice Invoice
  {
    get {return invoice;}
    set {invoice=value;}
  }

  public Vendor Vendor
  {
    get {return vendor;}
    set {vendor=value;}
  }

  public int PartCount
  {
    get {return parts.Count;}
  }

  public int ChargeCount
  {
    get {return charges.Count;}
  }

  public PurchaseOrder()
  {
    parts=new PartsHashtable();
    charges=new ChargesArray();
  }

  public void Add(Part p, WorkOrder wo)
  {
  }

  public void Add(Charge c)
  {
  }

  public void GetPart(int index, out Part p, out WorkOrder wo)
  {
    p=null;
    wo=null;
  }

  public void Close()
  {
  }
}

Running The Unit Tests

Image 5

Running the unit tests reveals what we expect--that all the functions fail except for the simple getter/setter methods we implemented.

Implementing Real Functionality

Once the stubs have been implemented, we can now fill them out with some real functionality and get the indicators to start turning green.  This is a simple process of inspecting the MUTE to see what tests failed and implement the functionality until the failure goes away.

Part

Image 6

The Part class needs nothing more than a constructor:

public Part()
{
  vendorCost=0;
  taxable=false;
  internalCost=0;
  markup=0;
  number="";
}
and voila! The part test passes:

Image 7

Vendor

Image 8

The vendor class needs a bit more work:

  • an addition to the constructor
public Vendor()
{
  parts=new PartsHashtable();
  partsArray=new PartsArray();
  name="";
}
  • the part collection implemented, along with duplicate part and unassigned part testing
public void Add(Part p)
{
  if (p.Number=="")
  {
    throw(new UnassignedPartException());
  }

  if (parts.Contains(p.Number))
  {
    throw(new DuplicatePartException());
  }

  parts.Add(p.Number, p);
  partsArray.Add(p);
}

and voila!  The vendor test passes:

Image 9

Charge

Image 10

This class simply needs a constructor.

public Charge()
{
  description="";
  amount=0;
}
and the unit test passes:

Image 11

ChargeSlip

Image 12

There's lots wrong with this class!  It needs:

  • initialization in its constructor:
public ChargeSlip()
{
  parts=new PartsArray();
  charges=new ChargesArray();
  number="";
}
  • validation in the charge slip number setter:
public string Number
{
  get {return number;}
  set
  {
    if (value.Length != 6)
    {
      throw(new BadChargeSlipNumberException());
    }
    number=value;
  }
}
  • Parts need to be validated and added (duplicates are OK?)
public void Add(Part p)
{
  if (p.Number=="")
  {
    throw(new UnassignedPartException());
  }
  parts.Add(p);
}
  • Charges need to be validated and added (duplicates are OK?)
public void Add(Charge c)
{
  if (c.Description=="")
  {
    throw(new UnassignedChargeException());
  }
  charges.Add(c);
}

The result:

Image 13

A good example of the difficulty in writing good unit tests

This class illustrates the problems with incomplete unit testing.  With the vendor class, we specifically implemented a test to ensure that duplicate parts are not allowed.  What about charge slips?  Are duplicate parts and charges allowed?  Well, actually, yes.  But there is not unit test written to ensure that the programmer didn't implement part and charge uniqueness.  This demonstrates that a unit test should not only test that a function does something, but it should also test that a function does not do something.

WorkOrder

Image 14

This class requires:

  • some constructor initialization:
public WorkOrder()
{
  chargeSlips=new ChargeSlipArray();
  number="";
}
  • work order number validation:
public string Number
{
  get {return number;}
  set
  {
    if (value.Length != 6)
    {
      throw(new BadWorkOrderNumberException());
    }
    number=value;
  }
}
  • charge slip validation and collection:
public void Add(ChargeSlip cs)
{
  if (cs.Number=="")
  {
    throw(new UnassignedChargeSlipException());
  }

  if (chargeSlips.Contains(cs.Number))
  {
    throw(new DuplicateChargeSlipException());
  }
  chargeSlips.Add(cs.Number, cs);
  chargeSlipsArray.Add(cs);
}
Image 15
A good example of bad code that passes the unit test

Several of these classes implement both an ArrayList and a Hashtable.  The ArrayList is used for ordinal indexing of the collection and the Hashtable is used to quickly determine if a duplicate entry exists.  The two lists are used in parallel.  Now, this is really bad code, and in no way would I ever implement something like this in real life.  But it does point out several problems beyond the incompleteness of the .NET collection classes.  From the perspective of unit testing, it illustrates that bad code can be written that ends up passing the unit tests.

Is there a way in which the implementation itself can be tested to ensure some level of quality?  Yes and no.  I suppose the point of pair programming is that this kind of implementation would not happen, but I don't buy that.  Two dumb programmers do not add up to one smart programmer.  I suppose you could say that I'm adhering to the XP's idea of "keep it as simple as possible and refactor later", but I don't buy that either.  Why not just do it right from the beginning.  I suppose this kind of bad programming can be caught with code walkthroughs, and that's a good thing unless you're like me, a consultant, and there really isn't anyone with which to share my awful code.

So, there are a couple unit tests that can be written to "contain" stupidity.  One involves measuring performance and the other involves measuring memory allocation.  Both of these I'll discuss more in Part III, so for the moment, I'm going to leave this terrible code in place so we can write unit tests to fix it!

Invoice

Image 16

This class needs:

  • some additional work in the constructor
public Invoice()
{
  charges=new ChargesArray();
  number="";
  vendor=null;
}
  • validation and collection of the charges
public void Add(Charge c)
{
  if (c.Description=="")
  {
    throw(new UnassignedChargeException());
  }
  charges.Add(c);
}

and then we have success:

Image 17

Customer

Image 18

This class requires:

  • some additional constructor initialization
public Customer()
{
  workOrders=new WorkOrderArray();
  name="";
}
  • validation and collection of work orders
public void Add(WorkOrder wo)
{
  if (wo.Number=="")
  {
    throw(new UnassignedWorkOrderException());
  }
  workOrders.Add(wo);
}

and then we have success:

Image 19

PurchaseOrder

Image 20

Finally, this class puts it all together.  For it to pass:

  • the constructor needs to initialize the private members:
public PurchaseOrder()
{
  parts=new PartsArray();
  charges=new ChargesArray();
  number="";
  vendor=null;
  invoice=null;
}
  • adding a part must be validated, checked if it exists for the current vendor, and added to the collection:
public void Add(Part p, WorkOrder wo)
{
  if (p.Number=="")
  {
    throw(new UnassignedPartException());
  }
  if (wo.Number=="")
  {
    throw(new UnassignedWorkOrderException());
  }
  if (!vendor.Find(p))
  {
    throw(new PartNotFromVendorException());
  }
  parts.Add(p, wo);
}
  • getting a part needs to be implemented (observe the kludge here in the indexing mechanism):
public void GetPart(int index, out Part p, out WorkOrder wo)
{
  p=null;
  wo=null;

  foreach (DictionaryEntry item in parts)
  {
    if (--index < 0)
    {
      p=item.Key as Part;
      wo=item.Value as WorkOrder;
      break;
    }
  }
}
  • the invoice number needs to be validated and must match the vendor for the purchase order:
public Invoice Invoice
{
  get {return invoice;}
  set
  {
    if (value.Number=="")
    {
      throw(new UnassignedInvoiceException());
    }
    if (value.Vendor.Name != vendor.Name)
    {
      throw(new DifferentVendorException());
    }
    invoice=value;
  }
}
  • the Close method has to be implemented:
public void Close()
{
  // Collect all the different work orders the parts go to.
  // For each work order, create a charge slip
  Hashtable woList=new Hashtable();
  int n=1;    // we always start with charge slip #000001
  string nStr="000000";
  double totalPartCost=0;
  foreach (DictionaryEntry item in parts)
  {
    if (!woList.Contains(item.Value))
    {
      ChargeSlip cs=new ChargeSlip();
      string s=n.ToString();
      cs.Number=nStr.Substring(0, 6-s.Length)+s; 
      woList[item.Value]=cs;

      // add the new charge slip to the work order
      (item.Value as WorkOrder).Add(cs);
    }
    
    // For each charge slip, add the part to
    // the charge slip.
    ChargeSlip cs2=woList[item.Value] as ChargeSlip;
    cs2.Add(item.Key as Part);
    totalPartCost+=(item.Key as Part).VendorCost;
  }

  // For each work order, get the total parts amount on
  // its corresponding charge slip.
  foreach (DictionaryEntry item in woList)
  {
    ChargeSlip cs=item.Value as ChargeSlip;
    double csPartCost=0;
    for (int i=0; i<cs.PartCount; i++)
    {
      csPartCost+=cs.Parts[i].VendorCost;
    }

    // The charge amount added to the charge slip =
    // csPartCost * chargeAmt / totalPartCost
    for (int i=0; i<charges.Count; i++)
    {
      Charge charge=new Charge();
      charge.Amount=csPartCost * charges[i].Amount / totalPartCost;
      charge.Description=charges[i].Description;
      cs.Add(charge);
    }
  }
}

After writing this code, the PurchaseOrder unit test passes!

Image 21

and even better, the entire assembly passes its unit tests:

Image 22

More incomplete unit testing

Here, the implementation uses a Hashtable to collect the parts, which implies that part objects added to the collection must be unique.  Because there is not unit test to validate this, there is no test in the PurchaseOrder class that generates an exception if two of the same part objects are added to the purchase order.  At some point, this will probably happen in the real system and everyone will wonder why the program crashed.  But that's OK, because once we figure it out, we can add the appropriate unit test!  (No really, I'm not being sarcastic, I'm really not!)

Other Debugging Techniques

Note the complete lack of other useful debugging techniques, namely instrumentation--the ability to track what's going, functions that can provide dumps of the collections, and asserts to verify parameters.  It really is necessary to practice these disciplines as well.  A framework that provides automatic instrumentation (something like the Application Automation Layer), or a methodology (like Aspect Oriented Programming) are two possible solutions to this problem.  The point though is that some thought needs to be put into how other debugging aids are going to be incorporated into your project.  Relying on the team members to "remember" to put in asserts, instrumentation, and collection dumps (to name a few) is probably the least desirable "methodology".

What's Next

Unit Testing MUTE

I suppose this is an obvious thing to do, isn't it?  Well, I think the case study is more interesting that writing UT's for MUTE, so I'll get around to it one of these days (as usual, anyone wanting to contribute is more than welcome).

More Complete User Interface

There's some things that need to be added to GUI, namely providing some more specific information as to the test results--assertion message, exception, etc.  I'll do this in Part III.

Mock Objects

It would be fun to work with a mock object, such as a data access layer object, and discuss the issues involved with using mock objects vs. production objects.  I should be able to get to that in Part III as well.

Advanced Unit Testing

In Part III, I'll look at extending the unit tests in ways that I feel would make it more useful.  Suggestions are welcome!

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
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionTest first..? Pin
Roy Osherove24-Sep-03 7:33
Roy Osherove24-Sep-03 7:33 
AnswerRe: Test first..? Pin
Marc Clifton24-Sep-03 7:57
mvaMarc Clifton24-Sep-03 7:57 
GeneralRe: Test first..? Pin
Roy Osherove24-Sep-03 11:46
Roy Osherove24-Sep-03 11:46 
GeneralRe: Test first..? Pin
Lai Shiaw San Kent28-Sep-03 17:02
Lai Shiaw San Kent28-Sep-03 17:02 

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.