I’ve practiced automated testing in a lot of different styles and flavours in .NET now and throughout all of them I’ve felt I’ve gotten far more out of using assertions beyond a simple “working/broken” checker. Whether the scope of the test has been so shallow as to test a class, isolated of all dependencies (all thoroughly over-mocked out of existence) or so grand as to try and verify behaviour at a higher level, against real external services, completely “outside-in”, I feel it’s valuable and low-effort to care about what you’re asserting.

Built in assertions functionally do just what you need them to - they test some expected condition and if that condition is wrong, they fail the test by throwing an exception with some detail. Sure they work but are any of us satisfied with something that simply works and no more? You shouldn’t be!

The key reasons I’ve grown to love Shouldly are:

  • Fantastically fluent and easy to use/discover API
  • Extremely useful errors tailored to your code

Shouldly’s API

Let’s compare against an example with basic XUnit assertions:

[Fact]
public void Calculates_highest_score_as_winner()
{
    var game = new Game();
    game.SetPlayer("Castor");
    game.SetPlayer("Pollux");
    
    game.SetPlayerScore("Castor", 90);
    game.SetPlayerScore("Pollux", 5);

    var winner = game.CalculateWinner();
    
    Assert.Equal("Castor", winner.Name);
    Assert.Equal(90, winner.Score);
}

In my opinion, the thing you’re inspecting in your test should come first and foremost which these assertions (and others) do not do, they prioritise the Assertion library and syntax. Not out of choice or design, but just out of “that’s how we did it” I feel. Shouldly’s fluent API is designed as a set of extension methods which attach onto anything generically, this works more like my brain does and I think helps the things that matter stand out in the tests far more.

In usage, this looks like:

winner.Name.ShouldBe("Castor");
winner.Score.ShouldBe(90);

You can discover almost the whole API by typing .Should and letting intellisense work its magic - it’s very user friendly (the one difference that stands out to me is the quite bespoke syntax around validating that an expression should or should not throw an exception, but I’ll forgive it for that and you can declare the action in a way that follows this anyway but it’s not strictly natural).

I recommend quickly flicking through the official documentation to get a feel for what else they offer you. Some arbitrary examples if you don’t like to click links: enumerable.ShouldBeOneOf, action.ShouldThrow<ArgumentNullException>, enumerable.ShouldBeSubsetOf, dictionary.ShouldContainKeyAndValue, string.ShouldStartWith.

Shouldly’s output

Shouldly’s output goes above and beyond “Your test failed because this specific check failed” as other test libraries sit satisfied with, usually as unhelpful as “Expected 12 but was 7” or the incredibly useless “Expected true but was false”.

Shouldly.ShouldAssertException: winner.Name

Shouldly.ShouldAssertException
winner.Name
    should be
"Castor"
    but was
"Pollux"
    difference
Difference     |  |    |    |    |    |    |   
               | \|/  \|/  \|/  \|/  \|/  \|/  
Index          | 0    1    2    3    4    5    
Expected Value | C    a    s    t    o    r    
Actual Value   | P    o    l    l    u    x    
Expected Code  | 67   97   115  116  111  114  
Actual Code    | 80   111  108  108  117  120
Shouldly.ShouldAssertException: winner.Score

Shouldly.ShouldAssertException
winner.Score
    should be
90
    but was
5

There’s something very, very important happening here: Shouldly is taking the identifiers in the real code I’m running in order to give me this output. It’s telling me that the winner.Name is wrong and, when that’s fixed, that the winner.Score is wrong. This is huge, I aren’t simply being told that “90 is not 5” (I’m not great at maths so I appreciate the reminder but I feel I could’ve worked that out), I’m instead being told which value was set to 90 and what it should have been. If your code is anywhere close to self-documenting, you’ll get some great information for free now.

While I will admit the string comparison is a little more verbose than we need in most cases (it has saved my ass once with two different whitespace characters) it shows Shouldly’s commitment to trying to give you everything you need to know why the test failed before you open it up in your editor. This becomes especially important if your tests are long running or failing very intermittently, you get hard logs of what was going on with a greater degree of precision.

This absolutely doesn’t just extend to a simple property on a class either:

game.CalculateWinner().Score.ShouldBe(90);
game.CalculateWinner().Score
    should be
90
    but was
5

That’s the variable name, class method and return value property all logged because they’re chained. Another bonus for me: Shouldly lets you associate different independent assertions as part of the same overall assertion - we won’t fail fast if one thing is wrong, we’ll keep going and working out what else is wrong before returning a log message indicating a summary of failures:

winner.ShouldSatisfyAllConditions(
    () => winner.Name.ShouldBe("Castor"),
    () => winner.Score.ShouldBe(90));
winner
    should satisfy all the conditions specified, but does not.
The following errors were found ...
--------------- Error 1 ---------------
    winner.Name
        should be
    "Castor"
        but was
    "Pollux"
        difference
    Difference     |  |    |    |    |    |    |   
                   | \|/  \|/  \|/  \|/  \|/  \|/  
    Index          | 0    1    2    3    4    5    
    Expected Value | C    a    s    t    o    r    
    Actual Value   | P    o    l    l    u    x    
    Expected Code  | 67   97   115  116  111  114  
    Actual Code    | 80   111  108  108  117  120  

--------------- Error 2 ---------------
    winner.Score
        should be
    90
        but was
    5

-----------------------------------------

So when this test fails on the build server, I can look at the output and know that the game returned the wrong player entirely, not just that the name or score was assigned wrong.

I’ve had people try to tell me that this is actually bad before, that we shouldn’t encourage people to couple a lot of different assertions and that tests should have only one reason to fail and one reason to change. I think that’s missing the point: Spend more time figuring out what you’re testing rather than dogmatically picking off the most miniscule thing at a time. If you’re testing that “A Constructor Sets Up An Objects External Properties Correctly” that’s the one thing, that’s your unit. Not “This single property works”. Couple the things together that should form part of a whole, and if that coupling needs to break in future then that’s a change in the requirements and a fair reason to change the tests. It’s still “One Reason”.

Finally, most (all?) Shouldly extensions offer the user the ability to add a custom error message that you can use to add even more context to any failures if you want. It’s always the final optional string parameter of an extension, again leaning into the easy-to-predict API it offers. Not a feature that should be overused, I feel, as it can detract from the simplicity but on occasion it is incredibly powerful and valuable when it finally fails and you get Shouldly’s output plus some very specific human-written output that can save hours of debug.

What’s wrong with the defaults?

I’ve used all three popular .NET test libraries throughout my career (MSTest, NUnit and XUnit in that order) and I find the syntax far more janky overall (though NUnit’s constraints model is kinda refreshing) and the output only useful for telling me to debug the test locally to see whats up. I don’t feel Shouldly has any major downsides over these to offset the value it provides beyond being an ‘unnecessary’ dependency and a new API to learn (a non-zero cost, but thanks to its design a very low one imo). The defaults, as typical of defaults everywhere, will do their job like a cheap reliable hammer, but I encourage you to look outside that box and find out what else you can gain.

I have also used FluentAssertions, another popular .NET assertion library and while I’d easily rank it above any of the defaults, for me it doesn’t hold a candle to Shouldly’s output or usage. Every assertion is a chain of calls to build up a full assertion which, while very flexible and kind of dynamic, is more of a pain in the ass to type for me. I’ve not found Shouldly loses anything by offering me .Should... as the sole starting point for everything, and adapting contextually to the type I’m invoking it on. I also found I routinely had to refresh my memory on how to FluentAssertions it and it still never quite mapped onto my (admittedly subjective) mental model of how an assertion should feel, but I will say the documentation is pretty fantastic and it’s worth your time to give it a whirl anyway and see how it feels for you and your team.

Rounding off

Assertions matter and I believe you get a lot of value out of treating them well. Using a good assertions library can be key to that, as can taking care not to over-assert and to make sure you’re asserting behaviour over implementation detail, and I feel this applies to my ethos outside of C# too; consider what alternatives you have and what they offer you around this. Never just be satisfied with the defaults by default!

While I wrote this as a bit of a “me praising and telling you to use a library” post, I hope you take away the values I’m placing in the assertions as my core message here, which I would encourage you to adopt too, and as a matter of delightful convenience how Shouldly provides them to me.