NExpect: not just pretty syntax!

fluffynuts

Davyd McColl

Posted on August 21, 2020

NExpect: not just pretty syntax!

I've written about NExpect a few times before:

If you'd like to learn more first, open those up in other tabs and have a look. I'll wait...

Animated image of a guest on the Tonight Show saying "I'll be here when you're ready", and sliding out of sight

I've had a preference for using NExpect myself, but I'm obviously biased: I had a developer-experience with NUnit assertions which I found lacking, so I made a whole new extensible assertions library.

But there's always been something that I haven't been able to qualify about why I prefer NExpect over NUnit assertions. I've even gone so far as to tell people to just use either, because they're both good, and I don't want to be that guy who tells people to use his stuff.

Though the deep-equality testing is really convenient and NUnit doesn't do that...

Today, that changes. I have a good reason to promote NExpect over NUnit now, apart from all of the obvious benefits of NExpect:

  • fluid expression
  • extensibility
  • deep-equality testing
  • better collection matching

Today I found that NExpect can tell you earlier when you've broken something than NUnit can.

Explain how?

Consider this innocuous code:

using NUnit.Framework;

[TestFixture]
public class Tests
{
  [Test]
  public void ShouldPassTheTest()
  {
    var result = FetchTheResult();
    Assert.That(result, Is.EqualTo(1));
    // or, the olde way:
    Assert.AreEqual(result, 1);
  }

  private int FetchTheResult()
  {
    return 1;
  }
}

Of course, that passes.

The interesting bit here is the usage of var, which, in C#, means "figure out the type of the result at compile-time and just fill it in". Long ago, that line would have had to have been:

int result = FetchTheResult();

var has some distinct advantages over the prior system. It's:

  • shorter to write
  • you only have to remember one "muscle-memory" to store a result (always var ___ = ___)
  • it means that if you do change return types, things are updated for you.

In theory (and practice), it makes you quicker on the first run and when you refactor.

The problem comes in when those strong types are discarded by code which compiles perfectly. The compiler can't save you from yourself every time!

Enter the refactor

When we update the above so that FetchTheResult now returns a complex object, the code will still compile:

using NUnit.Framework;
[TestFixture]
public class Tests
{
  [Test]
  public void ShouldPassTheTest()
  {
    var result = FetchTheResult();
    Assert.That(result, Is.EqualTo(1));
    // or, the olde way:
    Assert.AreEqual(result, 1);
  }

  public class Result
  {
    public int Value { get; set; }
    public DateTime Created { get; set; }
  }
  private int FetchTheResult()
  {
    return new Result()
    {
      Value = 1,
      Created = DateTime.Now
    };
  }
}

for the intent of the flow of logic, we're still returning the value 1, but we've also attached a DateTime property to that to indicate when that result was created. Rebuilding, we find that everything builds just fine, and perhaps we forget to re-run tests (or perhaps this function is very far away from where the result is being used, so we don't realise that we just broke a test somewhere else).

This is because the NUnit assertions fall back on object for types:

  • Assert.That is genericised for the first parameter, so it has a fixed type there, but takes a Constraint for the second parameter -- and a Constraint can house anything, because it casts down to object
  • Assert.AreEqual has an overload that expects two objects, so it will fall back on that, and also compile.

The test will fail -- if you remember to run it (or when CI runs it).

So how does NExpect help?

If we'd written the first code like so:

using NUnit.Framework;
using NExpect;
using static NExpect.Expectations;

[TestFixture]
public class Tests
{
  [Test]
  public void ShouldPassTheTest()
  {
    var result = FetchTheResult();
    Expect(result).To.Equal(1);
  }

  private int FetchTheResult()
  {
    return 1;
  }
}

then the refactor would have caused a compilation failure at second line of the test, since NExpect carries the type of result through to the .Equal method.

actually, it does a bit of up-casting trickery so that you

can, for example, Expect((byte)1).To.Equal(1);, but that's
beside the point for this particular post...

So the second the refactor had gone through, the test wouldn't compile, which means I could find the failure even before running the tests and update them accordingly, instead of waiting for tests to fail at a later date.

Conclusion

Strongly-typed languages have a certain amount of popularity because the types can help us to avoid common errors. This is why TypeScript is so popular. C# is strongly typed, but there are ways that the strength of that typing can be diluted, and one of those ways is found in NUnit assertions.

NExpect protects you here and alerts you about potentially breaking changes to your code before even running tests. Neat, huh?

I think I'll pat myself on the back for that 🤣

Little girl pats herself on the back

💖 💪 🙅 🚩
fluffynuts
Davyd McColl

Posted on August 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related