Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

An NUnit Test Suite Implementation

4.89/5 (10 votes)
26 Aug 2006Public Domain8 min read 1   372  
This article describes a scalable NUnit unit test suite for use on a tiered, database-driven .NET application.

Introduction

In this article, I will describe a scalable NUnit unit test suite for use on a tiered, database-driven .NET application. The suite will define sample generators used to easily create dummy data for tests, and it will use test fixture inheritance for increased scalability and to allow for easy testing of common functionality.

I will focus on testing of the domain model of a sample application. This is usually located in the "middle" of an application and is often called the business logic layer (BLL). It uses the data access layer (DAL) to mediate data transfer to and from the database and drives the behavior of one or more user interfaces (UI) with which the user interacts or in which data is displayed.

This article assumes knowledge of .NET and C#, but does not require experience with unit testing or NUnit in particular.

BLL Implementation

In this sample application, classes in the BLL that implement database operations inherit from a base class called PersistentObject. This class defines the following interface[1]:

C#
public abstract class PersistentObject {
    protected long _uid = long.MinValue;

    /// <summary>
    /// The unique identifier of this object 
    ///  in the database
    /// </summary>
    /// <remarks>
    /// Set when Fill() is called
    /// </remarks>
    public long UID {
        get { return _uid; }
    }

    /// <summary>
    /// Save this object's data to the database. 
    /// </summary>
    public abstract void Save();

    /// <summary>
    /// Fill this object with data fetched from the
    ///  database for the given UID
    /// </summary>
    /// <param name="uid">The unique identifier of
    /// the record to fetch from the database</param>
    public abstract void Fill(long uid);

    /// <summary>
    /// Remove this object from the database
    /// </summary>
    public abstract void Delete();

    /// <summary>
    /// Fetches an object of the given type and with the
    ///  given UID from the database
    /// </summary>
    /// <typeparam name="ConcreteType">
    /// The type of object to fetch
    /// </typeparam>
    /// <param name="uid">
    /// The unique identifier of the object in the database
    /// </param>
    public static ConcreteType Fetch<ConcreteType>(long uid) 
      where ConcreteType : PersistentObject, new() {
        ConcreteType toReturn = new ConcreteType();
        toReturn.Fill(uid);
        return toReturn;
    }
}

Say, for example, the application must save some client data and a client address that can be used elsewhere in the application. The BLL would therefore need to contain Address and Client classes derived from PersistentObject.

C#
public class Address : PersistentObject {
    private string _streetAddress = null;
    private string _city = null;
    private string _state = null;
    private string _zip = null;

    public string StreetAddress {
        get { return _streetAddress; }
        set { _streetAddress = value; }
    }

    public string City {
        get { return _city; }
        set { _city = value; }
    }

    public string State {
        get { return _state; }
        set { _state = value; }
    }

    public string Zip {
        get { return _zip; }
        set { _zip = value; }
    }

    public override void Save() {
        // Call DAL to save fields
        // ...
    }
    public override void Fill(long uid) {
        // Call DAL to fill fields
        // ...
    }
    public override void Delete() {
        // Call DAL to delete object
        // ...
    }    

    /// <summary>
    /// Utility function that returns the Address with 
    ///  the given UID
    /// </summary>
    public static Address Fetch(long addressUID) {
        return PersistentObject.Fetch<Address>(addressUID);
    }    
}

Client is similar, except it contains a property that returns the Client's Address object.

C#
public class Client : PersistentObject {
    private string _firstName = null;
    private string _lastName = null;
    private string _middleName = null;
    private long _addressUID = long.MinValue;

    private Address _addressObject;

    // ...

    public long AddressUID {
        get { return _addressUID; }
        set { _addressUID = value; }
    }

    /// <summary>
    /// On-demand property that returns this Client's 
    ///  Address based on the current value of AddressUID
    /// </summary>
    public Address Address {
        get {
            if (AddressUID == long.MinValue) {
                _addressObject = null;
            }
            else if (_addressObject == null 
              || AddressUID != _addressObject.UID) {
                _addressObject = new Address();
                _addressObject.Fill(AddressUID);
            }
            return _addressObject;
        }
    }

    // ...

}

To save new client data, the user would do something like the following:

C#
// Create the address that the client will link to
Address newAddress = new Address();
newAddress.StreetAddress = StreetAddressInput.Text;
newAddress.City = CityInput.Text;
newAddress.State = StateInput.Text;
newAddress.Zip = ZipInput.Text;
// Save the address to the database
newAddress.Save();

// Create the client
Client newClient = new Client();
newClient.FirstName = FirstNameInput.Text;
newClient.MiddleName = MiddleNameInput.Text;
newClient.LastName = LastNameInput.Text;
// Link to the address
newClient.AddressUID = newAddress.UID;
// Save the client to the database
newClient.Save();

And to retrieve client data elsewhere in the application, the user would do something like the following:

C#
Client existingClient = Client.Fetch(clientUID);
Address clientAddress = existingClient.Address;

Unit Testing Background

The BLL implementation outlined above is relatively standard. One can verify its behavior in any number of ways. The simplest but least robust is to test the UI. Since the UI depends on the BLL, one could conceivably verify the application by running through web pages or dialog boxes by hand. But what if the application has multiple UIs? Obviously, this method is slow, difficult to repeat, prone to human error, and may miss bugs. Also, it may promote bad programming practice in that a naïve coder may fix a symptom in the UI rather than the base cause in the BLL. This is not to say that we should omit UI testing, just that we should not rely on it to verify business logic.

A better option would be to create a simple driver program that calls the BLL method under development. This option would certainly be easier to repeat, but it may be difficult to save drivers for later or run all existing drivers to verify that nothing is broken.

This is where unit tests come in. One can think of a unit test as a simple driver program that one would probably write anyway. The unit testing framework organizes the tests, provides tools to make writing tests easier, and allows one to run tests in aggregate.

Test Suite Implementation

Since this article discusses a .NET application, I will use the NUnit testing framework in the example test suite. NUnit provides several features such as a test execution GUI, built-in assertions, and test attributes that make writing and running tests very easy.

It is most intuitive to create a test fixture (that is, a class containing a series of tests) for each class in the BLL. So, in keeping with the example, we will have ClientTest and AddressTest classes in the example test suite. These basic test fixtures will need to verify that data is added to the database, retrieved, edited, and deleted correctly. We often need to create dummy objects, so these test fixtures will also include some sample generators. Finally, we do not want to have to repeat common test code across many different test fixtures, so we will test the common database operations in a PersistentObjectTest class from which ClientTest and AddressTest both inherit.

I will explain the construction of PersistentObjectTest in parts. First, the class declaration:

C#
/// <summary>
/// Abstract base class for test fixtures that test 
///  classes derived from BLL.PersistentObject
/// </summary>
/// <typeparam name="PersistentObjectType">
/// The type of BLL.PersistentObject that the derived 
///  class tests
/// </typeparam>
public abstract class PersistentObjectTest<PersistentObjectType> 
  where PersistentObjectType : PersistentObject, new() {

This shows that PersistentObjectTest is a generic type that accepts the type of the object that its derived class tests. This type derives from PersistentObject and has an empty constructor. This lets us create sample generators and other utilities in a type-safe, generic manner:

C#
#region Sample Generators

/// <summary>
/// Returns a dummy object
/// </summary>
/// <param name="type">
/// Indicates whether the returned dummy object should
///  be saved to the database or not
/// </param>
public PersistentObjectType GetSample(SampleSaveStatus saveStatus) {
    PersistentObjectType toReturn = new PersistentObjectType();
    FillSample(toReturn);
    if (saveStatus == SampleSaveStatus.SAVED_SAMPLE) {
        toReturn.Save();
        // Check Save() postconditions...
    }
    return toReturn;
}

/// <summary>
/// Fills the given object with random data
/// </summary>
/// <param name="sample">
/// The sample object whose fields to fill
/// </param>
/// <remarks>
/// Should be overridden and extended in 
///  derived classes
/// </remarks>
public virtual void FillSample(PersistentObjectType sample) {
    // nothing to fill in the base class
}

/// <summary>
/// Asserts that all fields in the given objects match
/// </summary>
/// <param name="expected">
///  The object whose data to check against
/// </param>
/// <param name="actual">
/// The object whose fields to test
/// </param>
/// <remarks>
/// Should be overridden and extended in 
///  derived classes
/// </remarks>
public virtual void AssertIdentical
 (PersistentObjectType expected, PersistentObjectType actual) {
    Assert.AreEqual(expected.UID, actual.UID, 
      "UID does not match");
}

#endregion

GetSample() simply returns a dummy object. The implementations of FillSample() and AssertIdentical() are delegated to the derived classes. These three methods are used by other test fixtures to create and test sample objects. The base class uses them to verify the basic database operations in the following test methods:

C#
#region Data Tests

/// <summary>
/// Tests that data is sent to and retrieved from 
///  the database correctly
/// </summary>
[Test()]
public virtual void SaveAndFetch() {
    PersistentObjectType original = 
      GetSample(SampleSaveStatus.SAVED_SAMPLE);
    PersistentObjectType fetched = 
      PersistentObject.Fetch<PersistentObjectType>(original.UID);
    // verify that the objects are identical
    AssertIdentical(original, fetched);
}

/// <summary>
/// Tests that editing an existing object works correctly
/// </summary>
[Test()]
public virtual void EditAndFetch() {
    PersistentObjectType modified = 
      GetSample(SampleSaveStatus.SAVED_SAMPLE);
    // edit fields 
    FillSample(modified);
    // save edits
    modified.Save();
    // make sure edits were reflected in the database
    PersistentObjectType fetched = 
      PersistentObject.Fetch<PersistentObjectType>(modified.UID);
    AssertIdentical(modified, fetched);
}

/// <summary>
/// Tests that deletion works correctly.
/// </summary>
/// <remarks>
/// Expects data retrieval to fail
/// </remarks>
[Test(), 
ExpectedException(typeof(DataNotFoundException))]
public virtual void Delete() {
    PersistentObjectType toDelete = 
      GetSample(SampleSaveStatus.SAVED_SAMPLE);
    long originalUID = toDelete.UID;
    toDelete.Delete();
    // expect failure because the object does not exist
    PersistentObject.Fetch<PersistentObjectType>(originalUID);
}

#endregion

With PersistentObjectTest doing the heavy lifting, the concrete test classes need only define how to fill a sample object and how to check if two sample objects are identical. They can also define additional sample generators, utility functions, and test methods as needed.

C#
[TestFixture()]
public class AddressTest : PersistentObjectTest<Address> {

    public override void FillSample(Address sample) {
        base.FillSample(sample);
        Random r = new Random();
        string[] states = {"IL", "IN", "KY", "MI"};

        sample.City = "CITY" + DateTime.Now.Ticks.ToString();
        sample.State = states[r.Next(0, states.Length)];
        sample.StreetAddress = r.Next().ToString() + " Anywhere Street";
        sample.Zip = r.Next(0, 100000).ToString("00000");
    }

    public override void AssertIdentical(Address expected, Address actual) {
        base.AssertIdentical(expected, actual);
        Assert.AreEqual(expected.City, actual.City, 
          "City does not match");
        Assert.AreEqual(expected.State, actual.State,
          "State does not match");
        Assert.AreEqual(expected.StreetAddress, actual.StreetAddress, 
          "StreetAddress does not match");
        Assert.AreEqual(expected.Zip, actual.Zip, 
          "Zip does not match");
    }

}
[TestFixture()]
public class ClientTest : PersistentObjectTest<Client> {

    public override void FillSample(Client sample) {
        base.FillSample(sample);
        sample.FirstName = "FIRST" + DateTime.Now.Ticks.ToString();
        sample.MiddleName = "MIDDLE" + DateTime.Now.Ticks.ToString();
        sample.LastName = "LAST" + DateTime.Now.Ticks.ToString();
        sample.AddressUID = new AddressTest().GetSample
         (SampleSaveStatus.SAVED_SAMPLE).UID;
    }

    public override void AssertIdentical(Client expected, Client actual) {
        base.AssertIdentical(expected, actual);
        Assert.AreEqual(expected.FirstName, actual.FirstName, 
          "FirstName does not match");
        Assert.AreEqual(expected.MiddleName, actual.MiddleName, 
          "MiddleName does not match");
        Assert.AreEqual(expected.LastName, actual.LastName,
          "LastName does not match");
        Assert.AreEqual(expected.AddressUID, actual.AddressUID, 
          "AddressUID does not match");
    }
}

ClientTest's sample generator uses AddressTest.GetSample() to create a dummy Address when filling a dummy sample Client. This general pattern is used often in this type of test suite. Any test that needs a dummy object simply calls the appropriate sample generator.

When running tests, NUnit looks for any classes marked with the attribute [TestFixture()]. It creates an instance of the class and runs any methods marked with the attribute [Test()]. The [ExpectedException()] attribute tells NUnit that the given method should throw the given exception. The test code itself uses NUnit's Assert object to verify that expected properties hold.

Any test fixture that inherits from an abstract base class also "inherits"[2] any test methods. Therefore, AddressTest, a concrete test fixture, inherits the SaveAndFetch(), EditAndFetch(), and Delete() test methods from PersistentObjectTest. Note that a derived class can override these test methods if, for example, its corresponding BLL class does not support deleting:

C#
[Test()]
public override void Delete() {
    Assert.Ignore("This object does not support deleting");
}

Inheritance

Now that we have the basic test suite implemented, say the requirements change and we need to add a class representing a preferred client that receives discounts and special credit. First we will create a PreferredClient class derived from Client:

C#
public class PreferredClient : Client {
    private double _discountRate = 1;
    private decimal _accountCredit = 0.00M;

    //...

    public override void Save() {
        base.Save();
        // call DAL to save this object's fields
    }

    //...

}

Next, we must create a PreferredClientTest test fixture derived from ClientTest. But this causes a problem: ClientTest inherits from PersistentObjectTest<Client>, but we need PreferredClientTest to inherit indirectly from PersistentObjectTest<PreferredClient> so that PersistentObjectTest's methods use the correct type of object. The solution is to move the generic signature "down the hierarchy" to ClientTest:

C#
/// <summary>
/// Generic tester for classes derived from Client
/// </summary>
public class ClientTest<DerivedClientType> 
: PersistentObjectTest<DerivedClientType> 
    where DerivedClientType : Client, new() {

    public override void FillSample(DerivedClientType sample) {
        base.FillSample(sample);
        sample.FirstName = "FIRST" + DateTime.Now.Ticks.ToString();
        sample.MiddleName = "MIDDLE" + DateTime.Now.Ticks.ToString();
        sample.LastName = "LAST" + DateTime.Now.Ticks.ToString();
        sample.AddressUID = new AddressTest().GetSample
          (SampleSaveStatus.SAVED_SAMPLE).UID;
    }

    public override void AssertIdentical
      (DerivedClientType expected, DerivedClientType actual) {
        base.AssertIdentical(expected, actual);
        Assert.AreEqual(expected.FirstName, actual.FirstName, 
          "FirstName does not match");
        Assert.AreEqual(expected.MiddleName, actual.MiddleName, 
          "MiddleName does not match");
        Assert.AreEqual(expected.LastName, actual.LastName,
          "LastName does not match");
        Assert.AreEqual(expected.AddressUID, actual.AddressUID, 
          "AddressUID does not match");
    }

}

But we need to keep the non-generic tester so Client's tests will still run:

C#
/// <summary>
/// Non-generic tester for base Client type
/// </summary>
[TestFixture()]
public class ClientTest : ClientTest<Client> {
    // add Client-specific tests as needed
}

Finally, we define PreferredClientTest in terms of the generic version of ClientTest:

C#
[TestFixture()]
public class PreferredClientTest : ClientTest<PreferredClient> {

    public override void FillSample(PreferredClient sample) {
        base.FillSample(sample);
        Random r = new Random();
        // some random dollars and cents
        sample.AccountCredit = ((Decimal)r.Next()) + .25M; 
        sample.DiscountRate = r.NextDouble();
    }

    public override void AssertIdentical
     (PreferredClient expected, PreferredClient actual) {
        base.AssertIdentical(expected, actual);
        Assert.AreEqual(expected.AccountCredit, actual.AccountCredit, 
          "AccountCredit does not match");
        Assert.AreEqual(expected.DiscountRate, actual.DiscountRate, 
          "DiscountRate does not match");
    }
}

Note that the FillSample() and AssertIdentical() methods simply extend their base class counterparts. One can easily see how this type of expansion can continue as the application grows; it is simply a matter of adding a subclass and implementing the appropriate methods.

Drawbacks

Primary Keys

This hypothetical test suite makes one glaring assumption: it assumes that PersistentObject is a valid base class for real-world classes. This assumption becomes most apparent in the Fetch/Fill methods which always take a long as a unique database identifier. Often, a real-world database will not be normalized such that all data has a bigint primary key (if only!). One can get around this problem by expanding the generic signature of PersistentObjectTest and PersistentObject.Fetch() to include the type of the derived class' unique identifier.

Dummy Data Overload

Because of its dependence on sample generators, the form of test suite creates a large amount of dummy data in the database. This is acceptable since a large part of testing a database-driven application is verifying that data is saved and retrieved correctly. However, it means that the development application must have a dedicated testing database server that is regularly reset to some known state to prevent dummy data from overshadowing valid data. Also, the recursive nature of the sample generators may make it possible to get into a never-ending sample generation cycle that could very quickly bring a database (not to mention the stack frame) to its knees.

Randomness

The implementation I have outlined assumes that random dummy data will often suffice for most tests that use the generated objects. In other words, the consumer of the sample object must ensure that a generated object meets the desired preconditions. Bounds on randomness can often be achieved with parameterized sample generators such as the following:

C#
/// <summary>
/// Return a client with one of the given first names
/// </summary>
/// <param name="firstNames">
/// The list of possible first names
/// </param>
public static Client GetBoundedSample
  (string[] firstNames, SampleSaveStatus saveStatus) {
    Client toReturn = new ClientTest().GetSample(SampleSaveStatus.UNSAVED_SAMPLE);
    Random r = new Random();
    toReturn.FirstName = firstNames[r.Next(0, firstNames.Length)];
    if (saveStatus == SampleSaveStatus.SAVED_SAMPLE) {
        toReturn.Save();
    }
    return toReturn;
}

However, there is no general, easily-implemented way for the sample generators to control randomness or return a bounded exhaustive list of all possible samples. In fact, exhaustive test generation is an ongoing research problem.

Conclusion

The hypothetical test suite architecture that I have outlined is useful for testing tiered, database-driven applications in which reasonable, random sample data is often needed. By using test fixture inheritance and sample generators, it becomes very easy to expand the test suite as the application grows. It also reduces the amount of code needed to test the most important aspect of a database-driven application: that data travels to and from the database correctly. Variations on this testing implementation have performed well for several .NET applications with several dozen to several thousand classes.

Footnotes

  1. In reality, Save, Fill, and Delete would usually wrap protected overridable methods like DoSave, DoFill, and DoDelete. This would allow the base class to define common pre- and post-database operation steps while leaving the derived class to handle its own data. Also, Delete would usually set an "Ignore" flag rather than completely remove the data from the database. Regardless, we can ignore those complications in this article. Just assume that a derived class would override Save, Fill, and Delete in the obvious manner if the class supports the appropriate database operation.
  2. This is not true inheritance. NUnit uses reflection to find any methods marked with the attribute [Test()] regardless of where the method occurs in the class hierarchy. Also, overriding a test method does not retain the [Test()] attribute.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication