-
Notifications
You must be signed in to change notification settings - Fork 154
Custom Constraints
You can implement your own custom constraints by creating a class that
inherits from the Constraint
abstract class, which supports performing a
test on an actual value and generating appropriate messages.
Implementations must override the one abstract method ApplyTo<TActual>
which
evaluates the previously stored expected value (if any) against the method's
parameter, the actual value. There are also several virtual methods that may be
overridden to change some default behaviors.
The relevant portions of the Constraint
class are represented below.
namespace NUnit.Framework.Constraints
{
public abstract class Constraint
{
protected Constraint(params object[] args) {}
public abstract ConstraintResult ApplyTo<TActual>(TActual actual);
...
public virtual ConstraintResult ApplyTo<TActual>(ActualValueDelegate<TActual> del) {}
public virtual ConstraintResult ApplyTo<TActual>(ref TActual actual) {}
protected virtual object GetTestObject<TActual>(ActualValueDelegate<TActual> del) {}
public virtual string Description { get; protected set; }
protected virtual string GetStringRepresentation() {}
}
}
The Constraint
constructor accepts zero or more arguments and saves them to be used
in the printed description later. Constraints like NullConstraint
or UniqueItemsConstraint
take no arguments and simply state some condition about the actual value supplied. Constraints
with a single argument usually treat it as the expected value resulting from some operation.
Multiple arguments can be provided where the semantics of the constraint call for it.
The ApplyTo<TActual>(TActual actual)
method must be overridden and provides for the
core implementation of the custom constraint. Whatever logic defines pass or fail
of the constraint and actual/expected values goes into the ApplyTo<TActual>(TActual actual)
method.
For example, a very naive implementation of a reference equality constraint might look like this:
public override ConstraintResult ApplyTo<TActual>(TActual actual)
{
return new ConstraintResult(this, actual, ReferenceEquals(actual, Arguments[0]));
}
The key here is there needs to be some evaluation of the constraint logic, and the return value
must be a ConstraintResult
or subclass thereof. Custom subclasses of ConstraintResult
may
be used to further customize the message provided upon failure, as described below.
Constraints may be called with a delegate to return the actual value instead of the actual
value itself. This serves to delay evaluation of the value. The default implementation
of ApplyTo<TActual>(ActualValueDelegate<TActual> del)
waits for the delegate to
complete if it's an async operation, other immediately calls the delegate if synchronous, and
then calls the abstract ApplyTo<TActual>(TActual actual)
method with the value.
Another overload also exists, ApplyTo<TActual>(ref TActual actual)
. The default implementation
dereferences the value and then calls the abstract ApplyTo<TActual>(TActual actual)
method
with the value. This public virtual method is available by use from calling code but currently
is not used from any framework calls within NUnit itself.
The default implementation of ApplyTo<TActual>(ActualValueDelegate<TActual> del)
does not
simply execute the delegate but actually calls out to another virtual method,
GetTestObject<TActual>(ActualValueDelegate<TActual> del)
. This method can be overridden to
keep the default behavior of ApplyTo<TActual>(ActualValueDelegate<TActual> del)
while still
customizing how the actual value delegate is invoked.
This virtual property is used to provide a description of the constraint for messages. Simple
constant values can be set in the custom constraint's constructor. If more complex logic is
needed, override the property and provide a custom implementation of get
.
Here are a few simple examples from built-in constraints.
public class FalseConstraint : Constraint
{
public FalseConstraint()
{
this.Description = "False";
}
}
public class NullConstraint : Constraint
{
public NullConstraint()
{
this.Description = "null";
}
}
Here are a few complex examples from built-in constraints.
public class AndConstraint : BinaryConstraint
{
public override string Description
{
get { return Left.Description + " and " + Right.Description; }
}
}
public abstract class PrefixConstraint : Constraint
{
public override string Description
{
get
{
return string.Format(
baseConstraint is EqualConstraint ? "{0} equal to {1}" : "{0} {1}",
descriptionPrefix,
baseConstraint.Description);
}
}
}
NUnit calls the GetStringRepresentation
method to return a string representation of the
constraint, including the expected value(s). The default implementation returns the lowercase
display name of the constraint followed by all expected values, separated by a space.
For example, a custom constraint ReferenceEqualsConstraint
with an instance of a custom
MyObject
class as expected value would result in a default string representation of
<referenceequals MyObject>
.
You can override the initial display name only by setting DisplayName
in your constructor.
This public property cannot be overridden, but the Constraint
base class sets it in the
base constructor to be the name of the class, minus the "Constraint" suffix and minus
any generic suffixes.
Having written a custom constraint class, you can use it directly through its constructor:
Assert.That(myObject, new CustomConstraint());
You may also use it in expressions through NUnit's Matches
syntax element:
Assert.That(myObject, Is.Not.Null.And.Matches(new CustomConstraint());
The direct construction approach is not very convenient or easy to read. For its built-in constraints, NUnit includes classes that implement a special constraint syntax, allowing you to write things like...
Assert.That(actual, Is.All.InRange(1, 100));
Custom constraints can support this syntax by providing a static helper class and
extension method on ConstraintExpression
, such as this.
public static class CustomConstraintExtensions
{
public static ContentsEqualConstraint ContentsEqual(this ConstraintExpression expression, object expected)
{
var constraint = new ContentsEqualConstraint(expected);
expression.Append(constraint);
return constraint;
}
}
To fully utilize your custom constraint the same way built-in constraints are used, you'll need to implement three additional classes (which can cover all your constraints, not for each custom constraint).
-
Provide a static class patterned after NUnit's
Is
class, with properties or methods that construct your custom constructor. If you like, you can even call itIs
and extend NUnit'sIs
, provided you place it in your own namespace and avoid any conflicts. This allows you to write things like:Assert.That(actual, Is.Custom(x, y));
with this sample implementation:
public class Is : NUnit.Framework.Is { public static CustomConstraint Custom(object expected) { return new CustomConstraint(expected); } }
-
Provide an extension method for NUnit's
ConstraintExpression
, allowing you to write things like:Assert.That(actual, Is.Not.Custom(x, y));
with this sample implementation:
public static class CustomConstraintExtensions { public static CustomConstraint Custom(this ConstraintExpression expression, object expected) { var constraint = new CustomConstraint(expected); expression.Append(constraint); return constraint; } }
Copyright (c) 2018 The NUnit Project - Licensed under CC BY-NC-SA 4.0
-
NUnit
-
Release Notes
-
License
- Getting Started
- Writing Tests
- Running Tests
- Extending NUnit
- Technical Notes
-
Release Notes
- NUnit Xamarin Runners
- VS Test Adapter
- VS Test Generator
- NUnit Analyzers