Writing Custom Constraints in NUnit

NUnit supports several ways to assert against data. The recommended one, constraint-based, allows for a more naturally-read syntax which supports the flexible chaining of conditions to prove simple or complex facets about the system.

It follows the “Assert that” format:

Assert.That(actualValue, Is.EqualTo(expectedValue));

With the following components (listed left to right):

  • actualValue : The actual output of the system under test which you wish to validate
  • Is : A starting clause. The most common is Is but NUnit also defines Has, Does, and others to allow for readable tests
  • EqualTo() : A example function which returns the contraint which validates your data. EqualTo returns an instance of an EqualConstraint class which will internally contains validation logic. Other examples include LessThan() or Even.
  • expectedValue : The data to compare actualValue to using the rules defined by the constraint.

NUnit also contains built-in operators. For example, checking inequality is a matter to prepending the Not operator in front of EqualTo():

Assert.That(actualValue, Is.Not.EqualTo(expectedValue));

Similarly, if a situation requires check a characteristic of a value instead of comparing then a unary constraint like Even could be used:

Assert.That(actualValue, Is.Even);

The built-in constraints will likely meet 99.9% of use cases, but there may be some domain-specific rules which aren’t covered out-of-the-box. For example, a math-oriented program may wish to validate that a number is prime. It would be very nice if a test could be written to read:

Assert.That(actualValue, Is.Prime);

NUnit supports this through custom constraints. Custom constraints are classes which extend NUnit’s own Constraint class.

A PrimeConstraint may look like:

public class PrimeConstraint : Constraint
{
    public override string Description => "A prime value";
    public override ConstraintResult ApplyTo<TActual>(TActual actualValue)
    {
        var actualInt = Convert.ToInt32(actualValue);
        ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(actualInt, 0, nameof(actualInt));
        for (int i = 2; i <= (int)Math.Sqrt(actualInt); i++)
        {
            if (actualInt % i == 0)
            {
                // Not prime if we've found an evenly divisible factor
                return new ConstraintResult(this, actualValue, false);
            }
        }
        return new ConstraintResult(this, actualValue, true);
    }
}

In addition to extending from the Constraint class, the class must implement an ApplyTo<TActual>() method which will validate the actualValue originally passed into Assert.That(). Hooking this into the NUnit syntax tree is then very easy thanks to the new C# 14 extension members feature. Adding a new function onto NUnit’s static Is class and adding a new property onto the ConstraintExpression class can be achieved in one line each.

public static class ConstraintExtensions
{
    extension(NUnit.Framework.Is)
    {
        public static Constraint Prime => new PrimeConstraint();
    }
    extension(ConstraintExpression expression)
    {
        public Constraint Prime => expression.Append(new PrimeConstraint());
    }
}

And that’s it. They can be used in tests seamlessly afterwards as if they were part of NUnit itself.

[Test]
public void Test1()
{
    Assert.That(5, Is.Prime);
    Assert.That(4, Is.Not.Prime);
}

Comments

Leave a Reply

Discover more from Software by Steven

Subscribe now to keep reading and get access to the full archive.

Continue reading