Discussion:
Constraint Syntax Update was RE: Writing New Constraints for NUnit
Charlie Poole
2008-09-28 19:45:09 UTC
Permalink
Hi All,

I just checked in code that simplifies adding new constraints to NUnit
quite a bit. More changes are still in process, so I won't be posting
a final version of the instructions for a bit. However, here's an update.

1. The IConstraint interface and PartialConstraintExpression class
no longer exist.

2. There is now an IResolveConstraint interface implemented by both
Constraint and ConstraintExpression. This doesn't effect how you
write a constraint, however, since the implementation is in the
base class.

3. Constraint implementers no longer need to provide a separate
nested class to deal with Modifiers.

4. Most new constraints should be represented in the following
classes in order to get the most use out of the syntax:
* Is or Has should normally return an instance of the constraint.
* ConstraintFactory should normally do the same, and will usually
just call the static method in Is or Has
* ConstraintBuilder will normally return an instance of the constraint
*after*pushing*it* onto the constraint stack. (See code for other
constraints in ConstraintBuilder.cs for this.

While this does simplify the writing of constraints a good bit, I'm
working on ways to make it easier. In particular, I think we can
generate the code for most of item #4.

Charlie
-----Original Message-----
Sent: Sunday, September 07, 2008 10:35 AM
Subject: Writing New Constraints for NUnit
Hi All,
I've been asked to provide more info on the increasingly
complicated process of adding a new constraint to NUnit.
Since I still expect it to change - and hopefully get simpler
- I'm doing it on these mailing lists as "current but subject
to change" information.
This is basically a "how-to" but I've added a postscript
about why the design is as it is at the end.
http://nunit.org/?p=customConstraints&r=2.5 It gets more
complicated when you also want to work with the syntax of
NUnit's fluent interface for Asserts.
I generally write constraints test first, jumping between the
tests and the constraint and between the syntax tests and the
syntactic elements. That's hard to describe, so I'll just
tell you what's needed at the end here.
1. The Constraint
I assume you're writing a simple constraint, one that does
not operate on other constraints. Inherit your constraint
class from Constraint and override the two abstract members
Match() and WriteDescriptionTo(). Be sure to set the value of
the protected field "actual" in your Match method, since that
is used in any error message. Depending on the constraint you
are writing, you might need to override other methods, but
leave them alone unless you need to do it.
If your constraint has a generic variation, be sure to
define it within the scope of an #if NET_2_0 condition. If it
operates on any collections, try to make it work with
IEnumerable only and avoid copying the collection.
An important rule for constraints is to check for invalid
arguments and throw an exception. Do NOT just return false.
While this may work for simple cases, it will mess up more
complicated usage where constraints are combined with various
operators. And, of course, it may hide an error.
2. Constraint Tests
Derive one or more classes from ConstraintTestBase. Each of
those will be used to test one instance of the constraint.
How many you need depend on how many variations there are
within your constraint. Be sure to create separate tests for
generic variations -within an #if statement.
Write any extra tests you need, adding them to the same
classes or within a separate fixture.
NOTE: If you stop here, your constraint is useable by
Assert.That( xxx, new MyConstraint(...) );
NOTE: See below if your constraint takes modifiers
3. Syntactic Elements
You will need to pick one or more key words for use in the
NUnit syntax. Pick something that will be meaningful while
keeping it reasonably short.
Currently, you must add your syntactic element in three
places. I'm looking at using code generation to eliminate
* PartialConstraintExpression - look at how other
simple constraints are implemented here. If
your constraint takes modifiers, see below.
Otherwise, you will just need a method similar
public ConstraintExpression TypeOf(Type expectedType)
{
return this.Append(new ExactTypeConstraint(expectedType));
}
* Is/Has - you decide which is better, or consult
on the list if you're convinced that you need
public static ConstraintExpression TypeOf(Type expectedType)
{
return new PartialConstraintExpression().TypeOf(expectedType);
}
public ConstraintExpression TypeOf(Type expectedType)
{
return Is.TypeOf(expectedType);
}
NOTE: It's important to keep the actual logic in the
PartialConstraintBuilder class, with the others depending on it.
NOTE: If your constraint takes no args, use properties rather
than methods - it's easier to read.
4. Syntax Tests
Derive one or more classes from SyntaxTest. You will need one
for each different syntax you need to test.
If you forget to add code to any of the three classes above,
these tests will tell you.
At this point you're done... UNLESS you want to add modifiers
to your class. If your constraint will take any modifiers, I
suggest you do all of the above first, then add the first one
and make it work, then add the others. Come back to this note
if/when you want to add a modifier...
Welcome Back!
In the NUnit syntax, a modifier is a property or method that
returns the object itself, after making some sort of state
change. Existing examples of modifiers are Within(),
IgnoreCase and Descending. Here's a simple
public EqualConstraint IgnoreCase
{
get { caseInsensitive = true; return this; }
}
With the above addition, it's possible to write
new EqualConstraint("hello").IgnoreCase
but NOT
Is.EqualTo("hello").IgnoreCase
In order to make the syntax work, it's currently necessary to
create a ConstraintModifier class.
These classes inherit from ConstraintModifier and are usually
implemented as nested classes within the constraint itself,
so as to allow access to private members of the constraint.
Here's a simplified version of the one for
public class Modifier : ConstraintModifier
{
private EqualConstraint constraint;
public Modifier(EqualConstraint constraint,
ConstraintExpression builder)
: base(constraint, builder)
{
this.constraint = constraint;
}
public Modifier IgnoreCase
{
get { constraint.caseInsensitive = true; return this; }
}
}
After implementing the above, the three syntax classes need
to be changed so our methods return EqualConstraint.Modifier.
ConstraintFactory and Is/Has require no further changes but
public EqualConstraint.Modifier EqualTo(object expected)
{
EqualConstraint constraint = new EqualConstraint(expected);
return new EqualConstraint.Modifier(constraint,
this.Append(constraint));
}
That's "all" there is to it! :-)
Charlie
We want to provide a good object model, while
*at*the*same*time* having only reasonable choices appear in
the intellisense. And we want whatever you can compile to
make sense as much as is possible, so that there aren't too
many runtime checks.
So we need separate PartialConstraintExpression and
ConstraintExpression classes to make sure you can't enter
...And.And... or ...Null.Null... and similar meaningless stuff.
We need Is/Has to provide static methods to get the
expression started.
We need ConstraintFactory, to supply instance methods for use
by those who prefer to derive test classes from
AssertionHelper. We could eliminnate that class by
eliminating the feature.
In particular, the Modifier classes are needed because we are
dealing with a ConstraintExpression, but want the choices
offered to the user to represent those provided for an Equal
Constraint. The Modifier class ties together the whole
expression and the "leading edge" constraint in a way that
lets us have both. If the user ends the expression, the
Modifier's implementation of IConstraint is used. If the user
types "IgnoreCase" the EqualConstraint is used. If the user
types "And" or "Or", that's handled by the ConstraintExpression.
I'm working on a few approaches to this extra class.
-------------------------------------------------------------------------
This SF.Net email is sponsored by the Moblin Your Move Developer's challenge
Build the coolest Linux based applications with Moblin SDK & win great prizes
Grand prize is a trip for two to an Open Source event anywhere in the world
http://moblin-contest.org/redirect.php?banner_id=100&url=/

Loading...