The out-of-the-box method of performing assertions in unit tests works well, but there are better, more fluid ways of writing assertions. Rather than writing something akin to Assert.That(something, is.EqualTo(somethingElse))
, one could write something.ShouldBe(somethingElse)
. Less boilerplate, easier to read!
There are a few libraries (in the form of NuGet packages for C#/.NET) that help reach this goal. Recently, I discovered Shouldly. Let’s try writing some tests with it together.
Source Code for Example Project
A full version of the code examples below is available here, in my Shouldly Sandbox project. It is in .NET 9 using XUnit.
Shouldly Error Messages
When tests fail, it’s important to have clarity in why they did. Shouldly opts for easy-to-read messages. Here’s an example of our first test failing (screenshot taken in JetBrains Rider). In this example, we have a BattleManager
class. One of the methods is CalculateDamage()
. Our test asserts that the expected damage with the provided strength should be 1030
, but we get 100
back instead. Thus our tests fails. Here’s how Shouldly reports the failure:
Shouldly clearly defines the expectation, the actual result, the stack trace, and more.
Most of the assertion methods accept an optional string? customMessage
parameter, which will show up in failing tests as “Additional Info” should you want to provide any.
Writing Tests with Shouldly
Let’s go over some examples of writing more unit tests against our BattleManager
class, using Shouldly’s assertions to verify behavior.
The Great Migration
The assertions library I’m most familiar with is FluentAssertions. The bulk of assertions in that library are written as foo.Should().Be(bar)
. For others familiar with that library, you’ll find a transition to Shouldly very simple: Merge the methods!
ShouldBe()
For most Shouldly assertions, simply merging the Should()
and Be()
is all you have to do. The previous example becomes foo.ShouldBe(bar)
.
Here’s a full example, which can be found in my Shouldly Sandbox project. In it, we correct the assertion of CalculateDamage()
, as it is indeed supposed to return 100
in this scenario.
// Class under test
namespace ShouldlySandbox;
public class BattleManager
{
public int CalculateDamage(int strength) => strength + 42;
}
// Unit test file
using Shouldly;
using Xunit;
namespace ShouldlySandbox;
public class BattleManagerTests
{
private readonly BattleManager _battleManager = new();
[Fact]
public void CalculatesDamageAs100Given58Strength()
{
const int expectedCalculatedDamage = 100;
const int strength = 58;
int calculatedDamage = _battleManager.CalculateDamage(strength);
// Our first test using Shouldly!
calculatedDamage.ShouldBe(expectedCalculatedDamage);
}
}
You can find a mostly-accurate list of translations from FluentAssertions to Shouldly here. The majority of conversions do this same merge into ShouldBe()
. Convenient!
Let’s go over a few other common assertion scenarios and see the cases where Shouldly differs beyond the merging of the Should()
and Be()
methods.
Counting
To assert a collection has the appropriate number of elements, assert against the Count
or Length
property.
[Fact]
public void TheShopHasFiveItemsForSale()
{
const int expectedNumberOfItems = 5;
List<decimal> shopPrices = _battleManager.GetShopPrices();
shopPrices.Count.ShouldBe(expectedNumberOfItems);
}
Exceptions
Specify the type of exception thrown, and call ShouldThrow<TExceptionType>()
.
[Fact]
public void DamageCalculationRejectsNegativeStrength()
{
const int invalidStrength = -1;
Action calculateDamageCall = () => _battleManager.CalculateDamage(invalidStrength);
calculateDamageCall.ShouldThrow<InvalidStrengthException>();
}
ShouldThrow
returns the thrown Exception, which you can further write assertions against. We can augment our above test to assert the bad strength value is included in the exception message.
InvalidStrengthException thrownException = calculateDamageCall.ShouldThrow<InvalidStrengthException>();
thrownException.Message.ShouldContain(invalidStrength.ToString());
When an exception shouldn’t be thrown, use ShouldNotThrow()
:
[Fact]
public void DamageCalculationAccepts0Strength()
{
const int acceptableStrength = 0;
Action calculateDamageCall = () => _battleManager.CalculateDamage(acceptableStrength);
calculateDamageCall.ShouldNotThrow();
}
For asynchronous calls, await
the assertion and call the relevant Async methods. Remember to change your XUnit tests from public void
to public async Task
.
[Fact]
public async Task ExplodeWillNeverBeImplemented()
{
Func<Task> explodeCall = () => _battleManager.Explode();
await explodeCall.ShouldThrowAsync<NotImplementedException>();
}
[Fact]
public async Task ToBattleShouldNotExplode()
{
Func<Task> toBattleCall = () => _battleManager.ToBattle();
await toBattleCall.ShouldNotThrowAsync();
}
Multiple Assertions
Most of the time, when we have multiple assertions in a unit test, we want to see all of them run regardless of whether the first one fails. Otherwise, a test would have to be run multiple times to progress to the next failing state.
In Shouldly, this is accomplished by using the SatisfyAllConditions method.
[Fact]
public void ShopPricesAreNotTooExpensive()
{
List<decimal> shopPrices = _battleManager.GetShopPrices();
shopPrices.ShouldSatisfyAllConditions(
() => shopPrices.First().ShouldBeLessThan(3m),
() => shopPrices.Sum().ShouldBeLessThan(5000m),
() => shopPrices[1].ShouldBeLessThan(50m),
() => shopPrices[1].ShouldBeGreaterThan(10m)
);
}
This is what one would use to assert multiple conditions on the same item as well as multiple. Note how we do this against shopPrices[1]
, asserting that its price range is ($10, $50)
.
Collections
There are a couple scenarios to consider when asserting that a collection of items is expected:
- The order of items in the collection matters
- The order of items in the collection does not matter
For the former, simply use ShouldBe()
.
[Fact]
public void ShopPricesReturnCorrectAmounts()
{
List<decimal> expectedShopPrices = [2.99m, 45m, 1000m, 65m, 457.22m];
List<decimal> shopPrices = _battleManager.GetShopPrices();
// The order of items in the list matters!
shopPrices.ShouldBe(expectedShopPrices);
}
For the latter, within ShouldBe()
specify ignoreOrder: true
.
[Fact]
public void ShopPricesReturnCorrectAmountsRegardlessOfOrder()
{
List<decimal> expectedShopPrices = [457.22m, 1000m, 2.99m, 45m, 65m];
List<decimal> shopPrices = _battleManager.GetShopPrices();
// The order of items in the list does not matter!
shopPrices.ShouldBe(expectedShopPrices, ignoreOrder: true);
}
And Many More
We’re just getting started with Shouldly here! The above examples (along with the Sandbox project accompanying this post) should be more than enough to get you up and running with this library. Happy testing!