Want to show your appreciation? Please to my charity.

Wednesday, August 03, 2011

Use Linq Expression for Unit Test Assertion

I’m going to use NUnit to illustrate this technique but the same can possibly be applied to any other unit test frameworks.

Although NUnit has a large set of Constraints for you to use, and most of them are intuitive for simple uses cases, but there is still a learning curve for new comers. It can actually become quite complex and a bit difficult to choose which constraint to use when.  The outcome can also be obscure sometimes. Below is an example from a nunit-discuss topic.

Assert.That(array, Contains.Item.With.Property("x").LessThan(100).And.GreaterThan(50));

has a different meaning than this one...

Assert.That(array, Contains.Item.Property("x").LessThan(100).And.Greater.Than(50));

On the same nunit-discuss topic, David Schmitt mentioned the use of Expression. Yes, there are already libraries out there to do assertions using Expression, and they support various unit testing framework. For example, PowerAssert.Net, ExpressionToCode, Satisfyr and etc.

But if you are not ready to jump on any of those (yes, they are quite young and immature), then you can also roll you own implementation very easily. Below is an extension method based on NUnit:

    public static class NUnitExtension
        public static void AssertThat<T>(this T target, Expression<Func<T, bool>> criteria)
            Assert.That(target, new ExpressionConstraint<T>(criteria));
        private class ExpressionConstraint<T> : Constraint
            private readonly Expression<Func<T, bool>> _expression;
            public ExpressionConstraint(Expression<Func<T, bool>> expression)
                _expression = expression;
            public override bool Matches(object actual)
                this.actual = actual;
                return (actual is T) && _expression.Compile()((T)actual);
            public override void WriteDescriptionTo(MessageWriter writer)

Then the earlier example can be  rewritten to something like below.

array.AssertThat(a => a.Any(i => i != null && 5 < i.x && i.x < 10));
array.AssertThat(a => a.All(i => i != null && i.x < 10) && a.Length > 5);

When it fails, you still get meaningful information:

   1:  Test 'MyTest.Test' failed: 
   2:    Expected: <a => (a.All(i => ((i != null) && (i.x < 10))) && (ArrayLength(a) > 5))>
   3:    But was:  < <X(7)>, null, null, null, null, null >
   4:      MyTest.cs(68,0): at NUnitExtension.AssertThat[T](T target, Expression`1 criteria)
   5:      MyTest.cs(60,0): at MyTest.Test()

Now you may ask if this can be part of NUnit itself. Good question! Since Expression is only supported by .Net 3.5 & 4.0 but NUnit needs to support old .Net frameworks, it’s a decision NUnit team had to make if it worth the effort to release a .Net 3.5 specific dll file that contains this one extra feature. But anyway, it is very easy to copy and paste above extension method to your own test project and you are good to go.

Update (8/6/2011): While NUnit already support lamba via PredicateConstraint, the major difference is it uses a delegate instead of Expression. So when test fails the expect message loses the detail. For example, when below assert fails,

Assert.That(new []{new X{x=1}}, Has.Some.Matches<X>(i => 5 < i.x && i.x < 10));

unlike the Expression example that has expression printed as expected, you get an error message with no detail of the lamba logic.

   1:  Test 'MyTest.Test' failed: 
   2:    Expected: some item value matching lambda expression
   3:    But was:  < <X(1)> >
   4:      AccessorTest.cs(27,0): at SharpCut.UnitTests.AccessorTest`1.Test()

1 comment:

Eamon Nerbonne said...


In case you want your expressions to print as C# rather than the odd syntax of the expression tree's ToString, http://code.google.com/p/expressiontocode/ also includes a method ExpressionToCode.ToCode (or, with sub-expression values, AnnotatedToCode).

These might help you implement your own custom assertion syntax; i.e. you can easily replace the PAssert.That logic with your own.

Post a Comment