DEV Community

Cover image for NExpect, Level 3: You're the secret sauce!
Davyd McColl
Davyd McColl

Posted on • Edited on

NExpect, Level 3: You're the secret sauce!

In previous posts, I've examined how to do simple and collection-based assertions with NExpect

These have enabled two of the design goals of NExpect:

  • Expect(NExpect).To.Be.Readable();
    • Because code is for co-workers, not compilers. And your tests are part of your documentation.
  • Expect(NExpect).To.Be.Expressive();
    • Because the intent of a test should be easy to understand. The reader can delve into the details when she cares to.

Now, we come on to the third goal, inspired by Jasmine: easy user extension of the testing framework to facilitate expressive testing of more complex concepts.

Most of the "words" in NExpect can be "attached to" with extension methods. So the first question you have to ask is "how do I want to phrase my assertion?". You could use the already-covered .To or .To.Be:

internal static class Matchers
{
  internal static void Odd(
    this IBe<int> be
  )
  {
    be.AddMatcher(actual =>;
    {
      var passed = actual % 2 == 1;
      var message = passed
                    ? $"Expected {actual} not to be odd"
                    : $"Expected {actual} to be odd";
      return new MatcherResult(
        passed,
        message
      );
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The above extension enables the following test:

[Test]
public void ILikeOddNumbers()
{
  // Assert
  Expect(1).To.Be.Odd();
  Expect(2).Not.To.Be.Odd();
}
Enter fullscreen mode Exit fullscreen mode

There are a few concepts to unpack:

.AddMatcher()

This is how you add a "matcher" (term borrowed from Jasmine... Sorry, I couldn't come up with a better name, so it stuck) to a grammar continuation like .To or .Be. Note that we just create an
extension method on IBe where T is the type you'd like to test against, and the internals of that extension basically just add a matcher. This takes in a Func so your actual matcher needs to return an IMatcher result, which really is just a flag about whether or not the test passed and the message to display if the test failed.

Pass or fail?

This is the heart of your assertion and can be as tricky as you like. Obviously, your matcher could also have multiple exit strategies with specific messages about each possible failure. But the bit that takes a little getting used to is that you're writing a matcher which could be used with .Not in the grammar chain, so you should cater for that eventuality.

Meaningful messages

There's a simple strategy here: get the passed value as if you're doing a positive assertion (ie, as if there is no .Not in the chain) and set your message as follows:

  • If you've "passed", the message will only be shown if the expectation was negated (ie, there's a .Not in the chain), so you need to negate your message (see the first message above)
  • If you've "failed", the message will only be show if the message was not negated, so you need to show the relevant message for that scenario.

It turns out that (mostly), we can write messages like so:

internal static class Matchers
{
  internal static void Odd(
    this IBe<int>; be
  )
  {
    be.AddMatcher(actual =>;
    {
      var passed = actual % 2 == 1;
      var message =
        $"Expected {actual} {passed ? "not " : ""}to be odd";
      return new MatcherResult(
        passed,
        message
      );
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Doing this is tedious enough that NExpect offers a .AsNot() extension for booleans:

internal static class Matchers
{
  internal static void Odd(
    this IBe<int>; be
  )
  {
    be.AddMatcher(actual =>;
    {
      var passed = actual % 2 == 1;
      var message =
        $"Expected {actual} {passed.AsNot()}to be odd";
      return new MatcherResult(
        passed,
        message
      );
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Also, NExpect surfaces a convenience extension method for printing values: .Stringify() which will:

  • print strings with quotes
  • null values as "null"
  • and objects and collections in a "JSON-like" format.

Use as follows:

internal static class NumberMatchers
{
  internal static void Odd(
    this IBe<int>; be
  )
  {
    be.AddMatcher(actual =>;
    {
      var passed = actual % 2 == 1;
      var message =
        $"Expected {actual.Stringify()} {passed.AsNot()}to be odd";
      return new MatcherResult(
        passed,
        message
      );
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

You'll have to think (a little) about your first matcher, but it starts getting easier the more you write (:

Now you can write more meaningful tests like those in the demo project

The above is fine if, like me, you can see a pattern you'd like to test for really easily (if you have a kind of "matcher-first" mindset), but does provide a minor barrier to entry for those who like to write a few tests and refactor out common assertions.

Not to worry: NExpect has you covered with .Compose():

internal static class PersonMatchers
{
  internal static IMore<Person> Jane(
    this IA<Person>; a
  )
  {
     return a.Compose(actual =>
     {
        Expect(actual.Id).To.Equal(1);
        Expect(actual.Name).To.Equal("Jane");
     });
  }

// ....
  [Test]
  public void TestJane()
  {
    // Arrange
    var person = new Person() { Id = 1, Name = "Jane", Alive = true };

    // Assert
    Expect(person).To.Be.A.Jane();
  }
}
Enter fullscreen mode Exit fullscreen mode

.Compose uses [CallerMemberName] to determine the name of your matcher and attempts to throw a useful UnmetExpectationException when one of your sub-expectations fails. You may also provide a Func to .Compose to generate a more meaningful message.

.Compose also returns an IMore<T> for the type being tested, which allows fluent continuations. If you're not composing, you can always use the .More() extension method to get the same result:

internal static class PersonMatchers
{
  internal static IMore<Person> Jane(
    this IA<Person>; a
  )
  {
     a.AddMatcher(actual =>
     {
       // imagine fancy matcher logic in here!
     });
     return a.More();
  }}
Enter fullscreen mode Exit fullscreen mode

These are some rather simple examples -- I'm sure you can get much more creative! I know I have (:

Some parts of NExpect are simply there to make fluent extension easier, for example:

  • To.Be.A
  • To.Be.An
  • To.Be.For
  • To.Have

NExpect will be extended with more "grammar parts" as the need arises. If NExpect is missing a "grammar dangler" that you'd like, open an issue on GitHub

Top comments (3)

Collapse
 
fluffynuts profile image
Davyd McColl

I have to apologise for all the "live editing" -- I wanted these three articles to link to one another, but I don't see a way to do that on dev.to without publishing (the temp link is not very similar, that I can see, to the published link). So I kinda had to "publish-and-fix" articles as I went along. I apologise for anyone who caught this mid-publish and perhaps had a broken link or some strange formatting )':

Collapse
 
peledzohar profile image
Zohar Peled

This looks great! BTW, you could set your posts as a series and let Dev.To do the "heavy" lifting for you with the links...

Collapse
 
fluffynuts profile image
Davyd McColl

hey, thanks for the heads-up; I used to wonder how some people got the kind of "table of contents" at the top of their posts. The editor guide literally just mentions series as a name you can put in the frontmatter meta section, without discussion. So I took a guess it would work by inclusion sequence and applied to these posts (:

It would still be nice if the temporary url you see on a preview was predictably related to the final url, so one could embed links at will (as I did, kind of on-the-fly, after publishing)