Assert Builder

Assert Builder

A simple but effective technique to break out of the assert/fix/re-run test cycle, in unit testing.

·

4 min read

Introduction

Unit-tests will often contain a series of Asserts, any or all of which could fail.

When running such tests, the first assert failure encountered is shown and the test stops. Often, after fixing and rerunning the test, you find that further asserts in the test also fail. You have to fix each one in turn, re-running each time. This can be frustrating and inefficient.

Any simple test which has multiple asserts has this potential problem, but some situations are worse than others.

For example, I've recently been working on some tests which test the contents of a large complex data-structure which is generated by some Generator class.

This testing is done by setting up the expected structure using some test specific DTO classes. There's one DTO for each type of object which can be present in the data-structure. Each of these DTOs has an AssertEquals method, which basically goes through all the properties one by one, asserting that they are equal to the corresponding property in the real generated data structure.

Each assert will stop the test, which means you will only gradually see the whole picture as you repeatedly fix and retest.

The AssertBuilder approach

This is where an AssertBuilder comes in.

An Assert Builder is a class that allows developers to assert multiple times, but instead of asserting each time it stores any assert failure messages and allows the test to proceed.

It is somewhat analogous to a StringBuilder.

At some suitable point, after building it up from multiple asserts, you can decide to assert on the whole assertBuilder instance. If there have been any failures the aggregated assert fails, stopping the test. All the accumulated messages will be outputted together. This allows developers to see all of the issues at once, rather than having to fix and re-test each one individually.

public class AssertBuilder
{
    private List<string> _listAssertFailures = new List<string>();

    public void AreEqual<T>(T expected, T actual)
    {
        AreEqual(expected, actual, string.Empty);
    }

    public void AreEqual<T>(T expected, T actual, string message)
    {
        try
        {
            Assert.AreEqual(expected, actual, message, null);
        }
        catch (AssertFailedException ex)
        {
            _listAssertFailures.Add(ex.Message);
        }
    }

    public void CompleteAssert()
    {
        if (!AllAssertsSucceeded)
        {
            string assertMessage = BuildAssertMessage();

            Assert.Fail(assertMessage);
        }
    }

    private string BuildAssertMessage()
    {
        StringBuilder sb = new StringBuilder();

        foreach (var item in _listAssertFailures)
        {
            sb.AppendLine(item.ToString());
        }

        return sb.ToString();
    }

    private bool AllAssertsSucceeded => _listAssertFailures.Count == 0;
}

The example as shown only allows AreEqual calls, but can easily be extended to support whatever is required.

An example of usage is as follows:

[TestMethod]
public void TestUsingAssertBuilder()
{
    int expectedInt = 3;
    string expectedString = "test string";
    DateTime expectedDate = new DateTime(2019, 1, 1);

    AssertBuilder assertBuilder = new AssertBuilder();

    assertBuilder.AreEqual(expectedInt, 3);
    assertBuilder.AreEqual(expectedString, "y");
    assertBuilder.AreEqual(expectedDate, new DateTime(2020, 1, 1), "Dates should be equal");

    assertBuilder.CompleteAssert();
}

Alternative approach

Another approach, which could be more flexible and may allow better stack trace possibilities to show the individual assert failures, is to build up arbitrary assert actions and run them all at once, throwing an AggregateException containing each individual assert exception.

public class AssertBuilder2
{
    private List<AssertFailedException> _assertFailures = new List<AssertFailedException>();

    public void AddAssert(Action assert)
    {
        try
        {
            assert();
        }
        catch (AssertFailedException ex)
        {
            _assertFailures.Add(ex);
        }
    }

    public void RunAsserts()
    {
        if (_assertFailures.Count > 0)
        {
            throw new AggregateException("Multiple assert failures occurred:", _assertFailures);
        }
    }
}

Fluent Assertion's "Assertion Scopes"

Alternatively, if you wanted to use the Fluent Assertions library, this has the concept of an Assertion Scope. Fluent assertions, as the name suggests, allows you to write assertions with a fluent API which can help improve readability.

The Assertion Scopes feature additionally allows asserts to be grouped together, and ensures that all the asserts are run within that group in much the same way as the AssertBuilder ideas above.

Conclusion

As I've shown, whether using Fluent Assertions and AssertionScopes, or a basic AssertBuilder approach, it is possible to avoid the assert fail/fix/retest cycle and be more efficient when testing.

Let's face it, fast and efficient feedback is what we're looking for from tests, and every little helps!