Constraint Syntax Update was RE: Writing New Constraints for NUnit

"Charlie Poole" <[email protected]> Sun, 28 Sep 2008 12:45:09 -0700
Newsgroups gmane.comp.windows.dotnet.nunit.devel
Message-ID <004d01c921a2$b79e1920$6401a8c0@ferrari>
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-----
> From: Charlie Poole [mailto:[email protected]] 
> Sent: Sunday, September 07, 2008 10:35 AM
> To: '[email protected]'; 
> '[email protected]'; '[email protected]'
> 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.
> 
> 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=/