Creating Custom Fluent Assertions

September 23, 2022#Software Development
Article
Author image.

Kyle McMaster, Senior Consultant

Fluent Assertions is a library used to make C# test code more readable and expressive. This frees your tests from syntax heavy assertion statements and allows them to reveal their true intention. The library contains a great set of extensions for most assertion statements you would expect to find in common testing scenarios. It is also easily extensible as we will see here shortly. First, I’ll introduce a few snippets so those unfamiliar with the library’s purpose can understand its use and appeal.

public void BarShouldNotBeNull()
{
  var service = new FooService();

  var bar = service.GetBar();

  Assert.NotNull(bar);
}

This is a fairly straightforward test but could be better! Ideally, our test naming convention strategy combined with our assertion statements should read like a story and tell us what the intent of the test is. Let’s take a look at a comparable implementation using Fluent Assertions.

public void BarShouldNotBeNull()
{
  var service = new FooService();

  var bar = service.GetBar();

  bar.Should().NotBeNull();
}

Now our assertion portion of the test reads just like the behavior we would expect! This is a basic example but can be very powerful when customized to match the ubiquitous language of your domain.

A Simple Example from the Ardalis.Result Library

One great way to encourage best practices as a library author is to ship with a set of assertions that can be used to test your library. This is also a great opportunity for the open source community to enrich the ecosystem around a particular library or framework. In the following example, we’ll add extensions to the Ardalis.Result library that will allow us to test our result driven code in a more expressive way. Let’s take a look at one of the tests for the Map method.

[Fact]
public void ShouldProduceSuccess()
{
  var initial = Result<int>.Success(123);
  
  var actual = initial.Map(val => val.ToString());

  actual.IsSuccess.Should().BeTrue();
}

This example starts with a successful Result containing an integer value of 123 and maps it to a successful Result with a string value of “123”. The assertion statement is pretty basic and not very hard to read, but semantically what we are really concerned about is that the Result should be successful and not that some boolean value is true. This pattern of asserting on a boolean flag makes for a good custom assertion statement. The first step to creating a custom assertion is to define a class that inherits from ReferenceTypeAssertions<TSubject, TAssertions> abstract class. This class will contain the extension methods that will be used to assert on the type we are extending. In this case, we are extending the Result<T> type so we will create a ResultTAssertions class as shown below.

public class ResultTAssertions<T> : ReferenceTypeAssertions<Result<T>, ResultTAssertions<T>>
{
  public ResultTAssertions(Result<T> instance) : base(instance) { }

  protected override string Identifier => "Result<T>";

  // Assertions will go here
}

The ReferenceTypeAssertions<TSubject, TAssertions> class is a generic class that takes two type parameters. The first parameter is the type we are extending which is referred to as the Subject of the assertion. The second parameter is a sort of self reference to the type being defined that inherits from ReferenceTypeAssertions<TSubject, TAssertions>. The instance parameter is the instance of the type we are extending that we will be asserting on through the Subject property. We can then add assertion methods that check the conditions we would like to apply. The following example adds an AndConstraint<ResultTAssertions<T>> method that checks the Status of the Result<T> instance. The AndConstraint class is used to chain multiple assertions together. This Execute.Assertion statement is used to perform the actual assertion and will throw an exception if the ForCondition statement evaluates to false. The Given method allows us to capture the value of the Subject property. The BecauseOf and FailWith methods allow us to customize the output of the assertion failure message.

public AndConstraint<ResultTAssertions<T>> BeSuccess(string because = "", params object[] becauseArgs)
{
  Execute.Assertion
    .BecauseOf(because, becauseArgs)
    .Given(() => Subject)
    .ForCondition(s => s.IsSuccess)
    .FailWith("Expected Result to be success but was failure");
  return new AndConstraint<ResultTAssertions<T>>(this);
}

Next, we need to define a Should() extension method to apply the ResultTAssertions<T> to Result<T>. The Should() method is used to create an instance of the ResultTAssertions<T> class passing in the extended Result<T> as the instance parameter of the ResultTAssertions constructor.

public static class ResultTExtensions
{
  public static ResultTAssertions<T> Should<T>(this Result<T> instance) => new(instance);
}

Finally, we can put the pieces together in the updated test method.

[Fact]
public void ShouldBeSuccess()
{
  var initial = Result<int>.Success(123);
  
  var actual = initial.Map(val => val.ToString());

  actual.Should().BeSuccess();
}

Another Example Using Enums Representing Status

Fields that are implemented with enums, or even better SmartEnums, are great candidates for custom assertions since they often represent statuses in application logic. Typically, unit tests often assert on their values which correspond to a semantic meaning or use case in the application’s domain. In the Ardalis.Result library, these values are usually referred to by the name of their value such as “Ok”, “Not Found”, “Unauthorized” rather than the underlying enum value of 0, 1, 2 etc. For the following test, we can create a custom assertion to Results with a Status of NotFound.

[Fact]
public void ShouldBeNotFound()
{
  var result = Result<int>.NotFound();

  result.Status.Should().Be(ResultStatus.NotFound);
}

Similar to the previous example, we will create a ResultStatusAssertions class that inherits from ReferenceTypeAssertions<ResultStatus, ResultStatusAssertions> and add a Should() extension method to the ResultStatus enum to create a ResultStatusAssertions instance. These two classes are shown below.

public static class ResultStatusExtensions
{
  public static ResultStatusAssertions Should(this ResultStatus instance) => new(instance);
}

public class ResultStatusAssertions: ReferenceTypeAssertions<ResultStatus, ResultStatusAssertions>
{
  public ResultStatusAssertions(ResultStatus instance) : base(instance) { }

  protected override string Identifier => "ResultStatus";

  /// <summary>
  /// Asserts a Result's Status is NotFound
  /// </summary>
  /// <param name="because"></param>
  /// <param name="becauseArgs"></param>
  /// <returns></returns>
  public AndConstraint<ResultStatusAssertions> BeNotFound(string because = "", params object[] becauseArgs)
  {
    Execute.Assertion
      .BecauseOf(because, becauseArgs)
      .Given(() => Subject)
      .ForCondition(status => status == ResultStatus.NotFound)
      .FailWith("Expected Result to have status {0} but has {1}", ResultStatus.NotFound, Subject);

    return new AndConstraint<ResultStatusAssertions>(this);
  }
}

Again, this allows us to refactor our test to use the custom assertion. This is a much more expressive way to assert on the status of a Result and can be implemented for all of the values of the ResultStatus enum.

[Fact]
public void ShouldBeNotFound()
{
  var result = Result<int>.NotFound();

  result.Status.Should().BeNotFound();
}

Conclusion

Hopefully this post has given you some ideas on how to create your own custom Fluent Assertions for your test suite. As demonstrated above, custom Fluent Assertions can be a great way to make your tests more expressive and easier to read. When combined with the naming conventions and use cases of a particular framework or library, these assertions can provide a great way to help developers write better tests.

Resources


Copyright © 2024 NimblePros - All Rights Reserved