Creating a Custom IAssertionRule for FluentAssertions

November 17th 2014 FluentAssertions Unit Testing

The more I use FluentAssertions, the more I like its flexibility and extensibility. In this post I'm going to focus on assertion rules which can be used to define custom equality comparisons for specific types.

To show how it works, we'll create two classes for representing information about a user. The first one will be used in the application back end, when the user is retrieved from the database:

public class User
{
  public string Name { get; set; }
  public string Surname { get; set; }
  public string Username { get; set; }
  public LoginType LoginType { get; set; }
}

public enum LoginType
{
  UsernameAndPassword,
  DomainAccount
}

The second one will be used in the application service layer contract:

public class UserDto
{
  public string Name { get; set; }
  public string Surname { get; set; }
  public string Username { get; set; }
  public LoginTypeDto LoginType { get; set; }
}

public enum LoginTypeDto
{
  UsernameAndPassword,
  DomainAccount
}

In our tests we will want to compare the two classes: the first one will be used to setup the repository contents, and the second one will be returned by the service layer. Comparing whether they contain the same data, is a reasonable requirement:

public void DefaultAssert()
{
  var user = new User
  {
    Name = "Damir",
    Surname = "Arh",
    Username = "damir",
    LoginType = LoginType.UsernameAndPassword
  };
  var repository = new MockUserRepository();
  repository.Users.Add(user);
  var service = new UserService(repository);

  var userDto = service.GetUser("damir");

  userDto.ShouldBeEquivalentTo(user);
}

Although ShouldBeEquivalentTo method already does a property by property comparison, it will still throw, when it compares the two LoginType values:

Expected property LoginType to be UsernameAndPassword, but found UsernameAndPassword.

With configuration:
- Include only the declared properties
- Match property by name (or throw)

UsernameAndPassword values in both enums technically don't have anything in common, only we as developers know, that they represent the same business values. To provide this information to FluentAssertions, we can write a custom IAssertionRule:

public class LoginTypeRule : IAssertionRule
{
  public bool AssertEquality(IEquivalencyValidationContext context)
  {
    var subjectValue = GetValueForEnum(context.Subject);
    var expectationValue = GetValueForEnum(context.Expectation);

    if (subjectValue == null || expectationValue == null)
    {
      // rule not applicable
      return false;
    }

    Execute.Assertion
      .BecauseOf(context.Reason, context.ReasonArgs)
      .ForCondition(subjectValue == expectationValue)
      .FailWith("Expected {context:string} to be {0}{reason}, but found {1}",
        context.Subject, context.Expectation);

    // equality assertion handled
    return true;
  }

  private int? GetValueForEnum(object enumValue)
  {
    if (enumValue is LoginType || enumValue is LoginTypeDto)
    {
      return (int) enumValue;
    }
    return null;
  }
}

We only need to implement a single method: AssertEquality. By taking a closer look at our implementation, we can see which requirements we need to fulfill:

  • We start by checking the type of both values: Expectation and Subject. They both need to be one of our enum types, otherwise the method returns false. This indicates to FluentAssertions that the rule isn't applicable and further assertion rules need to be run.
  • In the next step we compare the two enums by their ordinal value. The comparison is wrapped in some boilerplate code which provides FluentAssertions with all the necessary information to report the assertion failure, when the values don't match.
  • We conclude by returning true, which indicates that the values have been compared and no further assertion rules need to be run.

To use the new assertion rule, we just need to include it in the assertion call at the end of our test:

userDto.ShouldBeEquivalentTo(user, options => options
  .Using(new LoginTypeRule()));

This will insert the rule at the beginning of the assertion rules collection, making the test pass.

Although the above example is a bit contrived, I am sure, you will encounter actual business cases, when creating a new assertion rule could make your tests easier to write and understand. Just keep in mind, that you have this extensibility point at your disposal.

Copyright
Creative Commons License