Click here to Skip to main content
Click here to Skip to main content

Advanced Unit Testing, Part III - Testing Processes

, 28 Sep 2003
Rate this:
Please Sign up or sign in to vote.
Extend Unit Testing So That Entire Processes Can Be Tested
<!-- Add the rest of your HTML here -->

Contents

Introduction

In Part III, I'm going to introduce some extensions to unit testing that, in my opinion, make unit testing more useful for the kind of work that I do.

The New User Interface

I've extended the UI by adding tab pages for the pass, ignore, and fail states.  Test fixtures and their tests are now alphabetized (although not necessarily run in alphabetical order--the sorting is done by the UI, not the unit test library).  And finally, I've added a few features that are reflected in the UI as well.

Advanced Unit Testing - Testing Processes

Most of what I code (and I imagine what other people code) is two things:

  1. functions that do simple, specific, things
  2. processes that glue the functions together in a particular way

And a lot of times, the process is something that involves the user's interaction.  An example of a very simple linear process is a wizard dialog, which prompts the user through a configuration in a very predictable and regimented manner.  The automatic billing case study that I've been using in these articles is an example of a potentially less linear process.  Especially when there's a lot of user interaction, the software has to be made a lot more robust (and flexible) in order to accommodate the different ways that the user is going to interact with the program.  The programming bug in the Therac-25 that resulted in radiation overdose is a good example--the interface involved a concurrent system and failed if the operator corrected an input error within 8 seconds of entering the mistake.  In this particular example:

The general consensus is that the Atomic Energy of Canada Limited is to blame. There was only one person programming the code for this system and he largely did all the testing. The machine was tested for only 2700 hours of use, but for code which controls such a critical machine, many more hours should have been put in to the testing phase. Also Therac-25 was tested as a whole machine rather then in separate modules. Testing in separate modules would have discovered many of the bugs. Also, if the AECL believed that there were problems with the Therac-25 right after the first incident then it is possible that most of the 5 other incidents could have been avoided and possibly the 3 fatalities.1

On the other hand, the X-43A mishap, in which a hypersonic air-breathing flight vehicle lost control in an unmanned test, was blamed not on the failure to test separate modules, but the failure to properly test the system as a whole:

The mishap board found the major contributors to the mishap were modeling inaccuracies in the fin actuation system, modeling inaccuracies in the aerodynamics, and insufficient variations of modeling parameters. The flight mishap could only be reproduced when all of the modeling inaccuracies with uncertainty variations were incorporated in the analysis.2

So here we have two different examples of how, while testing certainly was done, it wasn't done in the right way--people lost their lives and taxpayers saw their money going up in smoke.

Now, returning to the mundane, I have in the previous two articles on Unit Testing:

  • Written some unit tests in the test-first manner (Part I)
  • Wrote stub code to verify that the tests failed (Part II)
  • Corrected the unit tests (Part II)
  • Implemented real functionality so that the unit tests passed (Part II)

But are my unit tests really all that good?  The overall process (automatic billing) consists of a lot of different steps, involves many components, and involves a lot of user interaction:

There's a lot that can go wrong here, that must be done in a particular order, and that are susceptible to the system changing because of the time it takes to complete the process  Consider that it can take weeks for a part to arrive after it's been purchased, and several more weeks after that to receive the invoice.  Or, as sometimes happens, the invoice comes in before the part has been received!  Now, my case study is definitely a simplified version of the purchasing/receiving/billing process at the boatyard where I've written their yard management software, but even simplified, it is a good illustration for the purposes of exploring unit testing.

Reducing Unnecessary Repetition

There's a lot of repetition involved in my case study.  For example, testing whether the ClosePO function works involves setting up:

  • two work orders
  • three parts
  • a vendor
  • an invoice
  • a charge
  • and a partridge in a pear tree

But all this was already done as part of unit testing the individual work orders, parts, vendors, invoices, and charges.  Why not just combine these steps into a single process?

Defining A Process

A process is an ordered sequence of unit tests.  As long as one test passes, the next test is run.  This requires several modifications to MUTE:

  • A test fixture must be designated as a process
  • The tests themselves must have their order designated
  • Tests that are not run because of a failure should be so indicated
  • Running a process forward and in reverse

In Sequence Processes

An in sequence process is one that runs in the order specified by the programmer who wrote the unit tests.  Each unit test typically builds on information verified by the previous unit test.  When a unit test fails, the remaining unit tests in the sequence are designated as "not run" because it would be pointless to run them.  This is displayed with a blue circle for each test not run.  For example:

and the tests not run are listed in the "Not Run" tab:

Out Of Sequence Processes

The question then becomes, what do you test in order to ensure that the code handles itself well when the user or the program does something in an unexpected way (out of sequence)?  Obviously, testing all the combinations is not acceptable.  The purchase order sequence test that I wrote involves 16 steps, and testing every combination of 16 steps is 16!, or 20,922,789,888,000 (that's almost 21 trillion cases!).

What does "out of sequence" mean?  It means that a piece of code is run before another piece of code.  This clearly reduces the number of combinations that have to be analyzed, because the total combinations includes numerous combinations in which some code is still run in sequence, and we're not interested in those because we know that the "in sequence" parts of the process already pass!  There is only one combination that runs all the code out of sequence, and that's the combination in which the process is run in reverse.  So, there are only two tests that need to be performed--forward, in which the process is run forward, and reverse, in which the process is run backwards.

OK, this isn't entirely true.  It is easily possible, for example, to have a piece of code dependent upon two or more external objects.  Testing only in reverse order catches only the first dependency.  Clearly, to catch the second dependency, at least one predecessor (in sequence) must be run.  This condition is not handled in this version (yes, yes, I'll be adding it in the next version as soon as I've put some thought into the implementation issues).

Concurrent Processes

This is something that is very worthy of additional unit test extensions, but I'm not going to get into the issues involved at this point.  Let's keep things simple for now!

New Attributes

To support all this, we need some new attributes.

ProcessTestAttribute

[AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=true)]
public sealed class ProcessTestAttribute : Attribute
{
}

This attribute is attached to a test fixture (a class) to indicate to the test runner that the tests should be run in the order specified by the programmer.  For example:

[TestFixture]
[ProcessTest]
public class POSequenceTest
{
  ...
}

SequenceAttribute

[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
public sealed class SequenceAttribute : Attribute
{
    private int order;

    public int Order
    {
        get {return order;}
    }

    public SequenceAttribute(int i)
    {
        order=i;
    }
}

This attribute is specified for each test case in the process test fixture, numbered from 1 to the number of test cases.  For example:

[Test, Sequence(1)]
public void POConstructor()
{
    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 initialized.");
    Assertion.Assert(po.Invoice==null, "Invoice not initialized.");
    Assertion.Assert(po.Vendor==null, "Vendor not initialized.");
}

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

[Test, Sequence(3)]
public void PartConstructor()
{
    part1=new Part();
    Assertion.Assert(part1.VendorCost==0, "VendorCost is not zero.");
    Assertion.Assert(part1.Taxable==false, "Taxable is not false.");
    Assertion.Assert(part1.InternalCost==0, "InternalCost is not zero.");
    Assertion.Assert(part1.Markup==0, "Markup is not zero.");
    Assertion.Assert(part1.Number=="", "Number is not an empty string.");

    part2=new Part();
    part3=new Part();
}
...

RequiresAttribute

The world is not perfect, and, when running our unit tests in reverse, we don't want the unit test to fail, we want to see if the code being tested fails.  Therefore, there are cases when it is necessary to execute some code earlier in the process in order to ensure that the unit test, which depends on this code, doesn't break.  This attribute handles that.  For example:

[Test, Sequence(4), Requires("PartConstructor")]
public void PartInitialization()
{
    part1.Number="A";
    part1.VendorCost=15;
    Assertion.Assert(part1.Number=="A", "Number did not get set.");
    Assertion.Assert(part1.VendorCost==15, "VendorCost did not get set.");

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

    part3.Number="C";
    part3.VendorCost=25;
}

In order to initialize a part, well, that part has to be constructed first!  Therefore, this unit test requires that the constructor test be run first.

It is very easy to fall into the idea that, for example, closing the PO requires that parts and charges have been assigned to the PO.  This is NOT how the Requires attribute should be used, because all this does is ensure that the process is run in a forward direction.  Rather, this attribute should be used to ensure that parameters that the unit test code needs are already in existence.  The only thing I've ever needed the Requires attribute for is to guarantee that an object exists to which the unit test is about to assign a literal.  Contrast the above example with the following code:

[Test, Sequence(15), Requires("POConstructor")]
public void AddInvoiceToPO()
{
    po.Invoice=invoice;
    Assertion.Assert(invoice.Number==po.Invoice.Number,<BR>          "Invoice not set correctly.");
}

Here, we do NOT require that the invoice object be constructed.  The property should validate this for itself.  However, we DO require that the purchase order object be prior constructed.  A simple "l-value" rule is sufficient to determine if the Requires attribute needs to be used--if the object is on the left side of the equal sign, then yes.  If it is on the right side of the equal sign, then no.

Note that in the definition of the Requires attribute:

[AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
public sealed class RequiresAttribute : Attribute
{
    private string priorTestMethod;

    public string PriorTestMethod
    {
        get {return priorTestMethod;}
    }

    public RequiresAttribute(string methodName)
    {
        priorTestMethod=methodName;
    }
}

multiple attributes may be assigned to the same test.  For example:

[Test]
[Sequence(16)]
[Requires("POConstructor")]
[Requires("WorkOrderConstructor")]
public void ClosePO()
{
  ...
}

ReverseProcessExpectedExceptionAttribute

[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
public sealed class ReverseProcessExpectedExceptionAttribute : Attribute
{
    private Type expectedException;

    public Type ExceptionType 
    {
        get {return expectedException;}
    }

    public ReverseProcessExpectedExceptionAttribute(Type exception)
    {
        expectedException=exception;
    }
}

In a regular unit test, the ExpectedException attribute is used to ensure that the code under test throws the appropriate exception because the unit test is setting up a failure case.  Process tests are set up to succeed--in other words, there shouldn't be any exceptions thrown when the process is run in the forward direction (individual tests that throw exceptions are still part of other unit tests).  Testing a process in the reverse direction may cause once working code to fail, hopefully with an exception thrown by the code, not the framework.  To test this, the ReverseProcessExpectedException attribute has been added to make sure that the code handles and out of order process.

The Case Study

Using the automatic billing case study I've been developing in Parts I and II, I wrote a process test that goes through all the steps involved in getting to the point where the PO can be closed.  Compare this code to the ClosePO unit test written in Part II:

[Test]
[Sequence(16)]
[Requires("POConstructor")]
[Requires("WorkOrderConstructor")]
public void ClosePO()
{
  po.Close();

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

Note that all the setup stuff has already been done.  A lot simpler, isn't it?

Forward Processing

Running this process in the forward direction, everyone is happy:

Reverse Processing

Now let's look at what happens when I run the process in reverse:

Yuck!  Obviously, my code does not handle things being done in the wrong order very well!  Inspecting the failures:

makes it obvious that I'm not handling un-initialized objects very well at all.  Time to fix those.

Fixing ClosePO

An exception is thrown if the invoice does not already exist:

if (invoice==null)
{
  throw(new InvalidInvoiceException());
}

This is an important issue to note to the user--a PO cannot be closed without having an invoice against that PO!

Fixing AddInvoiceToPO

This illustrates the usefulness of testing property assignments.  It could easily have been the unit test itself that was throwing an exception because the the property assignment is not checking to see if the object being passed to it is a valid object!  To fix this, the assignment is modified:

public Invoice Invoice
{
    get {return invoice;}
    set
    {
        if (value==null)
        {
            throw(new InvalidInvoiceException());
        }
        else
        if (value.Number=="")
        {
            throw(new UnassignedInvoiceException());
        }
        // *** NO VENDOR TEST !!! ***
        if (value.Vendor.Name != vendor.Name)
        {
            throw(new DifferentVendorException());
        }
        invoice=value;
    }
}
Testing Properties
 
Technically, the getter should also be tested in our unit tests, and this raises the issue of whether or not the object returning the value should test the value itself, or the object requesting the value.

This issue is complicated by the fact that it is often common practice to "overload information".  Meaning that, if the purchase order returns a NULL, it means that the invoice hasn't yet been set.  While this is easy coding practice, it isn't a good practice.  A method like:

public bool InvoiceExists(void) {return value != null;}

is a much better solution.  Then, the getter can throw an exception if the caller is about to get inappropriate data.

Fixing AddChargeToInvoice

The same issues present themselves here and are easily corrected:

public void Add(Charge c)
{
    if (c==null)
    {
        throw(new InvalidChargeException());
    }
    if (c.Description=="")
    {
        throw(new UnassignedChargeException());
    }
    charges.Add(c);
}
Validating Data Using The Has A Relationship
 
This brings up another design issue--if the Invoice class were written in such a way that it merely returned a collection of charges which the caller manipulates directly, then it would not be possible the catch bad data exceptions.

This points out the benefits of a "has a" relationship--the wrapping class can perform data validation that would otherwise not be possible.

Fixing InvoiceInitialization

Here we have a case where the unit test is throwing an exception because the Invoice class is not testing for valid data.  This is easily fixed:

public Vendor Vendor
{
  get {return vendor;}
  set
  {
    if (value==null)
    {
      throw(new InvalidVendorException());
    }
  vendor=value;
  }
}

Fixing AddPartsToPO

A couple data validation tests are added to fix this problem:

public void Add(Part p, WorkOrder wo)
{
    if (p==null)
    {
        throw(new InvalidPartException());
    }

    if (wo==null)
    {
        throw(new InvalidWorkOrderException());
    }

    if (p.Number=="")
    {
        throw(new UnassignedPartException());
    }

    if (wo.Number=="")
    {
        throw(new UnassignedWorkOrderException());
    }

    if (!vendor.Find(p))
    {
        throw(new PartNotFromVendorException());
    }

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

Fixing AddVendorParts

More of the same...

public void Add(Part p)
{
    if (p==null)
    {
        throw(new InvalidPartException());
    }

    if (p.Number=="")
    {
        throw(new UnassignedPartException());
    }

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

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

It Works!

Running the process is reverse now works, in the sense that all the bad data is validated and the proper exceptions are thrown.

To Assert Or Throw, That Is The Question

Unit testing really brings to the forefront the difference between using assertions (or program by contract) and throwing exceptions (let the caller handle the error).  This doesn't mean that programming by contract requires using assertions--rather, it means that programming by contract shoud not use assertions but rather throw exceptions.  The reason for this is simple--the unit test itself uses assertions to validate data and expects exceptions to be thrown if the unit under test detects a fault.  The unit test then verifies that the exception is expected or not.

Throwing exceptions results in more robust code.  The exception tests can (and should!) be left in production code, so that the higher level functions can gracefully report problems to the user and take corrective actions.  Asserts, when they are removed in production code, simply result in program crashes or erroneous operations when the unexpected happens (which inevitably does).

Using unit testing principles therefore, asserts are quickly going to go the way of the dinosaur.  (Disagreements???)

What We Have Learned

  • Unit tests themselves have bugs and therefore need testing.
  • Writing "test first" code isn't as bad as I thought it would be.
  • There's a lot of redundant setup code that gets written as the functions being tested get higher up in the "object chain".
  • Unit tests don't really ensure good coding and design.  Bad code can pass a unit test as easily as good code.
  • Unit tests test, well, units, not processes.
  • Testing property assignments is useful, especially to check if the class is validating the value.
  • Unit testing changes the paradigm of using asserts to throwing exceptions (I imagine this statement will get lots of discussion)

A Note About The Code

As it currently stands, the code is not very robust.  It doesn't verify that

  • the process sequence starts at 1
  • increments by 1
  • doesn't have any duplicates or skips
  • the Requires functions actually exist.

In other words, some unit tests really need to be written for this thing!  Well, in the next release, it'll be a bit more bullet proofed.

Coming Next...

Well, part IV is not going to talk about scripting.  Part IV is going to look at some other useful additions to unit testing.  Hopefully the next part can wrap up those extensions (this one issue was worthy of an article in itself, in my opinion), so hopefully Part V will cover scripted unit testing.

Footnotes

1 - http://neptune.netcomp.monash.edu.au/
cpe9001/assets/readings/www_uguelph_ca_~tgallagh_~tgallagh.html

2 - http://spaceflightnow.com/news/n0307/23x43a/

References

Checking High-Level Protocols in Low-Level Software: http://research.microsoft.com/~maf/talks/Berkeley-VAULT.ppt

Programming By Contract: http://www.cs.unc.edu/~stotts/COMP204/contract.html

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Comments and Discussions

 
GeneralNunit PinmemberAdam Turner21-Oct-03 15:10 
GeneralRe: Nunit PineditorMarc Clifton21-Oct-03 16:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141030.1 | Last Updated 29 Sep 2003
Article Copyright 2003 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid