Practical Considerations for Using Disposable Resource-Context Classes for Functional Tests in C#

In my previous post, I described how I use disposable classes to handle complex initialization and cleanup for functional tests in C#. In this post, I’ll cover some practical considerations for using these contexts.

Using Multiple Contexts

Some tests require multiple resources. For example, a test that lists all the records in the database. This can be accomplished with multiple using statements. All of the resources will be cleaned up reliably at the end of the using statement.

[TestMethod]
public void GetRecordsReturnsAllRecords()
{
    var names = new[]
    {
        string.Format("{0}-1", TestContext.TestName),
        string.Format("{0}-2", TestContext.TestName),
        string.Format("{0}-3", TestContext.TestName)
    };

    using (new DatabaseRecordContext(names[0], _database))
    using (new DatabaseRecordContext(names[1], _database))
    using (new DatabaseRecordContext(names[2], _database))
    {
        var records = _database.GetRecords();
        var recordNames = records.Select(x => x.Name).ToArray();

        CollectionAssert.AreEqual(names, recordNames);
    }
}

Interacting with the Context

Sometimes it is useful to implement methods on the context class to interact with it within the scope of the using statement. For example, if it is common to write tests with multiple database records, the previous example could be implemented as follows.

[TestMethod]
public void GetRecordsReturnsAllRecords()
{
    var names = new []
    {
        string.Format("{0}-1", TestContext.TestName),
        string.Format("{0}-2", TestContext.TestName),
        string.Format("{0}-3", TestContext.TestName)
    };

    using (var recordsContext = new DatabaseRecordsContext(_database))
    {
        recordsContext.AddRecord(names[0]);
        recordsContext.AddRecord(names[1]);
        recordsContext.AddRecord(names[2]);

        var records = _database.GetRecords();
        var recordNames = records.Select(x => x.Name).ToArray();

        CollectionAssert.AreEqual(names, recordNames);
    }
}

Interacting with the context is also useful if the test needs a reference to the resource managed by the context. For example, consider a test where a record is returned by unique ID and compared with the record that was originally created. Note that the AddRecord method returns the resource being managed by the context.

[TestMethod]
public void GetRecordByUniqueIdReturnsRecord()
{
    using (var records = new DatabaseRecordsContext(_database))
    {
        var record = records.AddRecord(TestContext.TestName);

        var databaseRecord = _database.GetRecord(record.Id);

        Assert.AreEqual(record.Id, databaseRecord.Id);
        Assert.AreEqual(record.Name, databaseRecord.Name);
    }
}

Exception Handling

One important consideration is whether or not to handle exceptions within the context class. It depends on the situation, but generally, I do not want the context class to obscure exceptions thrown within the test itself. Most test frameworks only report the last exception. Usually I do not want the Dispose method to throw another exception that obscures the test exception. Most often, my approach is to avoid handling exceptions in the constructor of the context class; if the context cannot be constructed correctly then the test should fail. I almost always handle exceptions in the Dispose method; I will report an error message but I will not rethrow the exception.

public class DatabaseRecordContext : IDisposable
{
    private readonly Database _database;

    public readonly string Name;

    public DatabaseRecordContext(string name, Database database)
    {
        Name = name;
        _database = database;

        _database.CreateRecord(Name);
    }

    public void Dispose()
    {
        try
        {
            _database.DeleteRecord(Name);
        }
        catch (DatabaseException ex)
        {
            Console.WriteLine("Error deleting database record {0} : {1}", Name, ex);
        }
    }
}

Calling Dispose in TestCleanup

One of the things I like best about using context classes is that they are reusable. A context class may do all of the initialization and cleanup that you need and every test within a test class may require the same initialization and cleanup. Rather than using the context in a using statement, there is no reason why you cannot use the TestInitialize and TestCleanup pattern by making the context a member of the test class and calling the Dispose method in the TestCleanup method.

[TestClass]
public class DatabaseGetRecordTests
{
    private readonly Database _database = new Database("testdb.domain.tst");
    private DatabaseRecordContext _recordContext;

    public TestContext TestContext { get; set; }

    [TestInitialize]
    public void TestInitialize()
    {
        _recordContext = new DatabaseRecordContext(TestContext.TestName, _database);
    }

    [TestCleanup]
    public void TestCleanup()
    {
        _recordContext.Dispose();
    }

    [TestMethod]
    public void GetRecordReturnsRecord()
    {
        string name = TestContext.TestName;
        var record = _database.GetRecord(name);
        Assert.AreEqual(name, record.Name);
    }
}

Preserving State

Another technique I’ve used is to examine the environment in the constructor of the context class to decide how the Dispose method will ultimately cleanup the resource. For example, consider a set of database configuration parameters. If the parameter is set explicitly, then the database will respect the value of the parameter. If the parameter is not set explicitly, the database will assume a default value for the configuration parameter.

In a context class for setting a database configuration parameter to specific value before running a test, the constructor can inspect the current database configuration. If the parameter is set, then the context can get the current value and reset the parameter to this value in the Dispose method. If the parameter is not currently set, meaning the database is using the default, then simply delete the parameter in the Dispose method.

public class ConfigurationContext : IDisposable
{
    private readonly string _originalValue;

    public readonly string Name;

    public ConfigurationContext(string name, string value)
    {
        Name = name;
        _originalValue = DatabaseConfiguration.GetSetting(name);

        DatabaseConfiguration.Set(name, value);
    }

    public void Dispose()
    {
        if (_originalValue != null)
        {
            DatabaseConfiguration.Set(Name, _originalValue);
        }
        else
        {
            DatabaseConfiguration.DeleteSetting(Name);
        }
    }
}