Search code examples
c#unit-testingnunitfluent-interface

How to write a fluent constraint in NUnit that requires parenthesis


I recently started working with the Constraint functionality of NUnit and ran into the following question. How can I write a constraint using the fluent expression syntax where the order of execution is important and in normal C# programming is solved with parenthesis?

In the following example I define two separate assertions:

  1. A string should start with 1 or 2 and in all cases the string should end with 5
  2. A string should start with 1 or 2, and in the cases where the string starts with 2 it should end with 5

To assert this, I can think about three ways; classic, fluent constraints and constraints using compound constraints. So this results in 6 tests and some test cases.

private class SourceForParenthesisTest : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        yield return new TestCaseData("2").Throws(typeof(AssertionException));
        yield return new TestCaseData("3").Throws(typeof(AssertionException));
        yield return new TestCaseData("15");
        yield return new TestCaseData("25");
        yield return new TestCaseData("35").Throws(typeof(AssertionException));
    }
}

[TestCase("1", ExpectedException = typeof(AssertionException))]
[TestCaseSource(typeof(SourceForParenthesisTest))]
public void WithParenthesisClassic(string i)
{
    var res = (i.StartsWith("1") || i.StartsWith("2")) && i.EndsWith("5");
    Assert.True(res);
}

[TestCase("1", ExpectedException = typeof(AssertionException))]
[TestCaseSource(typeof(SourceForParenthesisTest))]
public void WithParenthesisOperatorConstraint(string i)
{
    Assert.That(i, (Is.StringStarting("1") | Is.StringStarting("2")) & Is.StringEnding("5"));
}

[TestCase("1", ExpectedException = typeof(AssertionException), Ignore = true, IgnoreReason = "Not clear how to write this fluent expression")]
[TestCaseSource(typeof(SourceForParenthesisTest))]
public void WithParenthesisConstraint(string i)
{
    Assert.That(i, Is.StringStarting("1").Or.StringStarting("2").And.StringEnding("5"));
}

[TestCase("1")]
[TestCaseSource(typeof(SourceForParenthesisTest))]
public void NoParenthesisClassic(string i)
{
    var res = i.StartsWith("1") || i.StartsWith("2") && i.EndsWith("5");
    Assert.True(res);
}

[TestCase("1")]
[TestCaseSource(typeof(SourceForParenthesisTest))]
public void NoParenthesisOperatorConstraint(string i)
{
    Assert.That(i, Is.StringStarting("1") | Is.StringStarting("2") & Is.StringEnding("5"));
}

[TestCase("1")]
[TestCaseSource(typeof(SourceForParenthesisTest))]
public void NoParenthesisConstraint(string i)
{
    Assert.That(i, Is.StringStarting("1").Or.StringStarting("2").And.StringEnding("5"));
}

The actual problem is in WithParenthesisConstraint (assert 1 as listed above), I couldn't think about a way how to write the constraint correctly and this results in one failing test case that I have set to ignored.

How do I write this assert to work as expected?


Solution

  • I can't see an obvious way to do this either. My first thought is that you need to resolve the expression up to a given point, which leave's your assertion looking like this:

    Assert.That(i, ((IResolveConstraint)Is.StringStarting("1").Or.StringStarting("2"))
                                          .Resolve().With.StringEnding("5"))
    

    Which is obviously a bit messy. You can add your own extension method to make it a bit more pleasant, using something like this (you can obviously rename the extension method to anything you think is appropriate):

    static class MyExtensions {
        public static Constraint Evaluate(this Constraint exp) {           
            return ((IResolveConstraint)exp).Resolve();
        }
    }
    

    Which would make your test code assertion this:

    Assert.That(i, (Is.StringStarting("1").Or.StringStarting("2")).Evaluate()
                                          .And.StringEnding("5"));
    

    Which is a bit nicer but isn't ideal so may not be what you're looking for.

    It may also be worth considering combining the Evaluate and And constraints to make it easier to read. Perhaps an extension method like this:

    public static ConstraintExpression OnlyIf(this Constraint exp) {
        return ((IResolveConstraint)exp).Resolve().And;
    }
    

    To give test code like this:

    Assert.That(i, (Is.StringStarting("1").Or.StringStarting("2")).OnlyIf()
                                                                  .StringEnding("5"));