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.user |
|---|---|
| Message-ID | <004d01c921a2$b79e1920$6401a8c0__33298.9438357346$1222632185$gmane$org@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=/