Table of Contents
Extended Constraint Syntax
This specification is about a new syntax for expressing NUnit assertions using the features of .NET framework 3.5 including language improvements. There are two key features this enables: Extension methods permit a modular, extensible fluent constraint syntax, and LINQ expression trees permit using plain C# (or VB) as a expression-based constraint language.
Launchpad Blueprint: http://blueprints.launchpad.net/nunit-3.0/+spec/extended-constraint-syntax
User Stories
Fluent Constraint Syntax
- A user expresses assertions in a fluent fashion
- A user extends NUnit's constraints without modifying NUnit
- An extension developer packages new constraints and their syntax in a single extension
This improves NUnit by making extensibility easier.
Expression-based Constraints
- A user expresses assertions in C# or VB without needing to learn NUnit-specific constraints
- Any boolean expression convertible to an expression tree can form a constraint
- Failed assertions include detailed error messages such as subexpression values - i.e. unlike Debug.Assert.
This improves NUnit by making it easier to learn and less necessary to extend.
Existing Implementations
There are already a couple of implementations implementing a fluent syntax or expression-based syntax or both:
- Extension Methods For Nunit: this is substantially an aliasing mechanism which, without touching the constraints themselves, provides a fluent way to express them
- SharpTestsEx: this is an improvement of NUnitEx and provides an extension-method based constraint syntax in addition to a starting point for an expression-based mechanism of assertions.
- ExpressionToCode: this is an expression-based assertion library. It is a reimplementation of Power Assert .NET, which is itself a port of Groovy's Power Assert.
Extensible Fluent Constraint Proposal
Ideally we would like to be able to use a constraint syntax similar to the current fluent syntax that is extensible. This requires using extension methods instead of static classes, as the following example demonstrates:
Assert.That(1, Is.GreaterThan(0))
This example is not extensible since it uses the static Is class. If I wanted to write something like Is.MuchGreaterThan(int x) and thus a constraint MuchGreaterThan which NUnit doesn't provide, I would have to alter and recompile NUnit's code. Patching NUnit like this and maintaining such a patch is a high barrier to entry.
The same assertion could be written as:
Assert.That(1).Is.GreaterThan(0)
This allows for extensions since any user can define a new extension method for the class of the Is property. Say Is were to return a value of type IIsConstraint, then I could just write
public static void MuchGreaterThan(this IIsConstraint iis) { ... }
Taking syntax shown on the SharpTestsEx homepage as an example, some assertions with the new NUnit syntax could be written as following:
| SharpTestsEx | NUnit 3.0 proposal |
|---|---|
true.Should().Be.True(); | Assert.That(true).Is.True |
“somethig”.Should().Contain(“some”); | Assert.That(“something”).Contains(“some”) |
“somethig”.Should().StartWith(“so”).And.EndWith(“ing”) | Assert.That(“something”).StartsWith(“so”).And.EndsWith(“ing”) |
new[] {1, 2, 3}.Should().Have.SameSequenceAs(new[] { 1, 2, 3 }); | Assert.That(new[] {1, 2, 3}).Is.EquivalentTo(new[] {1, 2, 3}) |
ActionAssert.Throws<ArgumentException>(() => new SillyClass(null)) | Assert.That(() => new SillyClass(null)).Throws<ArgumentException>() |
Unresolved Issues
How would the following actually work?
Assert.That(“something”).StartsWith(“so”).Or.EndsWith(“ing”);
How would StartsWith know that it's not supposed to do anything if it fails, since there is a chance EndsWith might succeed?
Remember, the above code is semantically identical to this:
var temp = Assert.That(“something”).StartsWith(“so”); temp.Or.EndsWith(“ing”);
Extension methods can lead to namespace pollution; in particular if defined on object (which we therefore should try to avoid).
Expression Based Constraint Proposal
NUnit provides a wealth of constraints. This means that knowing how to expression non-trivial constraints isn't always easy (particularly for new or casual users). Finding the appropriate constraint (or combination of constraints) requires knowledge NUnit's many constraints, and the semantics of a particular constraint may not be clear without reading the documentation.
For example, consider the assertion (valid for many uppercase strings, for most cultures):
Assert.That(() => x == x.ToLower().ToUpper())
This expression can be expressed as a standard equality constraint, but doing so means not showing the intermediate steps in the computation. Using expression trees, a failure could be rendered as:
Assert.That failed for: x == x.ToLower().ToUpper() | | | | | | | | | "ABC I" | | | "abc i" | | "ABC İ" | false "ABC İ"
This variant requires very little knowledge of NUnit, yet is still usable even for complex constraints by leveraging a language the user already knows (namely VB or C#).
Possible extensions to this concept could be “Helpers” that recognize specific patterns and improve readability. For instance, if an expression consists of a sequence of && operators, a helper might suppress showing the details of non-failing clauses. Or, if an expression contains multiple DateTime, the helper could ensure the accuracy of the DateTime.ToString is high enough to represent any differences. If an == operator fails but .Equals would have succeeded, this could be mentioned.