Charlie Poole
2008-09-07 17:35:24 UTC
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.
Actually, adding a constraint is easy. It's documented here:
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
creating it with new. You can write statements like:
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 this duplication. The following
classes need to be updated:
* 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
to this one for TypeOf:
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
a new class. Add a method similar to this:
public static ConstraintExpression TypeOf(Type expectedType)
{
return new PartialConstraintExpression().TypeOf(expectedType);
}
* ConstraintFactory: Add a method similar to this:
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
modifier:
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
EqualConstraint:
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
PartialConstraintExpression will now look like this:
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
Theoretical PostScript:
In case you wonder Why we neeed all the classes:
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=/
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.
Actually, adding a constraint is easy. It's documented here:
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
creating it with new. You can write statements like:
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 this duplication. The following
classes need to be updated:
* 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
to this one for TypeOf:
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
a new class. Add a method similar to this:
public static ConstraintExpression TypeOf(Type expectedType)
{
return new PartialConstraintExpression().TypeOf(expectedType);
}
* ConstraintFactory: Add a method similar to this:
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
modifier:
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
EqualConstraint:
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
PartialConstraintExpression will now look like this:
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
Theoretical PostScript:
In case you wonder Why we neeed all the classes:
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=/