Contents
- Introduction
- Component Organization
- The Unit Test Core Engine
- The Case Study
- Compiler Errors
- Stubs In General
- The Class Stubs
- Part, Unit Test
- Part, Stub
- Vendor, Unit Test
- Vendor, Stub
- Charge, Unit Test
- Charge, Stub
- ChargeSlip, Unit Test
- ChargeSlip, Stub
- WorkOrder, Unit Test
- WorkOrder, Stub
- Invoice, Unit Test
- Invoice, Stub
- Customer, Unit Test
- Customer, Stub
- PurchaseOrder, Unit Test
- PurchaseOrder, Stub
- Running The Unit Tests
- Implementing Real Functionality
- Other Debugging Techniques
- What's Next
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.
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
MUTE is organized into several logical blocks:
This consists of a small set tools that I use in different applications.
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);
}
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.
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;
}
}
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)
{
}
}
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.
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.
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.
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 consists of iterating through different collections:
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;
}
...
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!
A high level block diagram of the test apparatus can be illustrated as:
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.
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:
public class AssemblyCollection : UniqueCollection
{
public AssemblyCollection()
{
}
}
public class NamespaceCollection : UniqueCollection
{
public NamespaceCollection()
{
}
public void LoadClasses()
{
foreach (NamespaceItem ni in Values)
{
ni.LoadClasses();
}
}
}
public class ClassCollection : UniqueCollection
{
public ClassCollection()
{
}
public void LoadMethods()
{
foreach (ClassItem ci in Values)
{
ci.LoadMethods();
}
}
}
public class MethodCollection : UniqueCollection
{
public MethodCollection()
{
}
}
The NamespaceCollection
and ClassCollection
implement a "helper iterator" to load class and method information, respectively.
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.
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).
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.
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();
if (tf.HasTestFixture)
{
foreach(MethodItem mi in ci.MethodCollection.Values)
{
}
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);
}
}
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)
{
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)
{
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.
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).
public class TestFixtureAttribute : TestUnitAttribute
{
public TestFixtureAttribute()
{
}
public override void SelfRegister(TestFixture tf)
{
tf.AddTestFixtureAttribute(this);
}
}
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);
}
}
public class SetUpAttribute : TestUnitAttribute
{
public SetUpAttribute()
{
}
public override void SelfRegister(TestFixture tf)
{
tf.AddSetUpAttribute(this);
}
}
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;
}
}
public class IgnoreAttribute : TestUnitAttribute
{
public IgnoreAttribute()
{
}
public override void SelfRegister(TestFixture tf)
{
mi.Ignore=true;
}
}
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 (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.
The tests are run from the MethodItem
class:
public void Invoke(object classInstance)
{
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:
Note that in this code the
InnerException
is thrown.
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!
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!
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.
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
{
}
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.
Members are automatically initialized to certain values in debug mode.
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.
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 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.
[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.");
}
}
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;}
}
}
[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);
}
}
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();
}
}
[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.");
}
}
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;}
}
}
[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";
}
[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);
}
}
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();
}
}
[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);
}
}
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)
{
}
}
[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);
}
}
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)
{
}
}
[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);
}
}
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)
{
}
}
[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();
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];
Assertion.Assert(cs1.PartCount + cs1.ChargeCount==3,
"Charge slip 1: doesn't have three charges.");
Assertion.Assert(cs1.Charges[0].Amount==6.125,
"Charge slip 1: charge not the correct amount.");
Assertion.Assert(cs2.PartCount + cs2.ChargeCount==2,
"Charge slip 2: doesn't have two charges.");
Assertion.Assert(cs2.Charges[0].Amount==4.375,
"Charge slip 2: charge not the correct amount.");
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.");
}
}
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 reveals what we expect--that all the functions fail except for the simple getter/setter methods we implemented.
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.
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:
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:
This class simply needs a constructor.
public Charge()
{
description="";
amount=0;
}
and the unit test passes:
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:
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.
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);
}
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!
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:
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:
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()
{
Hashtable woList=new Hashtable();
int n=1;
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;
(item.Value as WorkOrder).Add(cs);
}
ChargeSlip cs2=woList[item.Value] as ChargeSlip;
cs2.Add(item.Key as Part);
totalPartCost+=(item.Key as Part).VendorCost;
}
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;
}
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!
and even better, the entire assembly passes its unit tests:
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!)
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".
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).
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.
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.
In Part III, I'll look at extending the unit tests in ways that I feel would make it more useful. Suggestions are welcome!