Disposable Resource-Context Classes for Functional Tests in C#
Automated tests generally follow the Arrange, Act, Assert pattern. First, establish the preconditions of the test, then execute the functionality under test, and, finally, assert that the result meets the expectation. The Arrange portion of a unit test is usually trivial. Functional tests, however, can often have fairly involved preconditions and the same code can be repeated across many tests. For example, to test CRUD operations for a database application, a test will often need a specific database record to act on.
Furthermore, unlike a unit test which has no external dependencies, a functional test may impact subsequent tests if it does not leave the environment in an acceptable initial state for those tests. Resetting or redeploying the test environment can take a significant amount of time and, therefore, can often not be performed between each and every test. There is also some benefit in having a diverse set of tests operate on the same environment, without reinitialization, in order to identify integration issues.
In order to minimize the need for reinitialization, a functional test should reliably cleanup after itself. For this purpose, most automated test frameworks have test-method and test-class cleanup routines that are guaranteed to be executed, even if the test fails. These routines can be too coarse, however, if one needs to finely control the scope of the cleanup, or if the cleanup does not apply uniformly across all the tests in the test class.
In this post I will describe how I like to use disposable context-classes in C# functional tests to both reliably arrange and cleanup a test, and to avoid code duplication across tests.
Motivation
I want to create a resource, for example, a record in a database, so that I can operate on the resource in a test and then have the resource cleaned up reliably when I’m done using it. A standard approach is to put the creation of the resource in the TestInitialize
method and the cleanup of the resource in the TestCleanup
method.
[TestClass] public class DatabaseGetRecordTests { private readonly Database _database = new Database("testdb.domain.tst"); public TestContext TestContext { get; set; } [TestInitialize] public void TestInitialize() { _database.CreateRecord(TestContext.TestName); } [TestCleanup] public void TestCleanup() { _database.DeleteRecord(TestContext.TestName); } [TestMethod] public void GetRecordReturnsRecord() { var name = TestContext.TestName; var record = _database.GetRecord(name); Assert.AreEqual(name, record.Name); } }
This approach, however, does not promote a lot of code reuse across test classes. Unless you plan carefully, you’ll soon find many test classes with largely similar initialization and cleanup. It also does not address the case where you want the scope of the resource to exist for a portion of the test, but not the entirety of the test.
Furthermore, sometimes an otherwise logical grouping of tests do not require identical initialization and cleanup for each test. For example, you may have a set of tests that exercise the return values for each API method of a web service. One test may ensure that null is returned when getting a resource that does not exist. A second test may ensure that the correct resource is returned if it does exists. These tests require different initialization and cleanup.
A reasonable approach for the tests that require resource cleanup is to put the cleanup in a try
/finally
statement so that the resource will be cleaned up reliably within the finally
statement.
[TestClass] public class DatabaseGetRecordTests { private readonly Database _database = new Database("testdb.domain.tst"); public TestContext TestContext { get; set; } [TestMethod] public void GetRecordReturnsNullIfRecordDoesNotExist() { Assert.IsNull(_database.GetRecord(TestContext.TestName)); } [TestMethod] public void GetRecordReturnsRecord() { var name = TestContext.TestName; _database.CreateRecord(TestContext.TestName); try { var record = _database.GetRecord(name); Assert.AreEqual(name, record.Name); } finally { _database.DeleteRecord(name); } } }
While these tests will be reliable, the original intent of the test becomes less clear with the added initialization and cleanup. And here I’m using a trivial example. Functional tests often have very complex initialization and cleanup that I would not want to repeat in each test.
Disposable Resource-Context
In C#, resource scope can be controlled with the IDisposable
interface. Objects that implement this interface are commonly used within a using
statement. A using
statement is basically the equivalent of the try
/finally
statement in the previous example, where the Dispose
method of the object is guaranteed to be called at the end of the using
statement.
I like to create disposable classes that create the resource in the constructor and delete the resource when the Dispose
method is called. These self-contained classes encapsulate resource initialization and cleanup and are easily reusable across tests.
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() { _database.DeleteRecord(Name); } } [TestClass] public class DatabaseGetRecordTests { private readonly Database _database = new Database("testdb.domain.tst"); public TestContext TestContext { get; set; } [TestMethod] public void GetRecordReturnsNullIfRecordDoesNotExist() { Assert.IsNull(_database.GetRecord(TestContext.TestName)); } [TestMethod] public void GetRecordReturnsRecord() { string name = TestContext.TestName; using (new DatabaseRecordContext(name, _database)) { var record = _database.GetRecord(name); Assert.AreEqual(name, record.Name); } } }
I usually use dependency injection for things like the database session, rather than having the context class create its own session. It is almost always true that the test itself requires this session and it makes sense to use the same one. Not having to deal with session management also makes the context class more streamlined.
Note that I do not implement the full dispose pattern. For example, I do not implement a destructor nor do I prevent the Dispose
method from throwing exceptions. The reason for this is that the context should only be used in a using
statement in a test method. I’m not concerned about garbage collection of the context under any other condition.
In my next post, I’ll discuss some practical considerations for using disposable context-classes in functional tests.