|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Contents
IntroductionIn 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 SeeThe code for MUTE covers some interesting topics:
Component OrganizationMUTE is organized into several logical blocks:
General Purpose Helper LibraryThis consists of a small set tools that I use in different applications. String HelpersThe 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.
public static string LeftOf(string src, char c) { int idx=src.IndexOf(c); if (idx==-1) { return src; } return src.Substring(0, idx); }
public static string RightOf(string src, char c) { int idx=src.IndexOf(c); if (idx==-1) { return ""; } return src.Substring(idx+1); }
public static string LeftOfRightmostOf(string src, char c) { int idx=src.LastIndexOf(c); if (idx==-1) { return src; } return src.Substring(0, idx); }
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 DefinitionsThis 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 DefinitionsThere are six attributes in the basic unit test:
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=true)] public sealed class TestFixtureAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)] public sealed class TestAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)] public sealed class SetUpAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)] public sealed class TearDownAttribute : Attribute { }
[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; } }
[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 DefinitionsI'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 EngineThis component consists of two pieces:
This a large piece of code and will be discussed further in its own section. Unit Test Windows ApplicationThe Window Forms application consists of three sections:
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 AssemblyUsing 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 EventThis 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
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 ViewPopulating 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); ...
... 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; ...
... 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; ...
... 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 TestsWhen 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 EngineA 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.
UniqueCollectionThis 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 ( The 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 AssemblyItem - Collecting NamespacesThis 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 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 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:
The 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 NamespaceItem - Collecting ClassesEach 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 MethodsAcquiring 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 InformationThis class maintains a collection of assemblies and is responsible for running the unit tests. Assemblies are loaded into the public void LoadAssembly(string file) { AssemblyItem ai=new AssemblyItem(); ai.Load(file); ai.LoadNamespaces(); ai.LoadClasses(); ai.LoadMethods(); assemblyCollection.Add(file, ai); } The 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 AttributesI 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 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 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 TestUnitAttribute ClassesIn 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); } }
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; } } TestFixture - Running The TestsThis 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 MethodsThe tests are run from the 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:
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 StudyIn 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 ErrorsSomewhere, 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 GeneralNow 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. ExceptionsThe 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 SettersOK, 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 InitializationMembers are automatically initialized to certain values in debug mode. Compiler WarningsIn 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 Tests With Multiple AssertsThe 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 StubsThe 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, Stubpublic 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, Stubpublic 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, Stubpublic 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, Stubpublic 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, Stubpublic 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, Stubpublic 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, Stubpublic 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, Stubpublic class PurchaseOrder { private string number; private Vendor vendor; | ||||||||||||||||||||||||||||