Improving Unit Testing with FluentAssertions

FluentAssertions is an alternative assertion library for unit tests, to use instead of the methods in Assert class that Microsoft provides. It has much better support for exceptions and some other stuff that improves readability and makes it easier to produce tests.

The coding of Kentor.AuthServices was a perfect opportunity for me to do some real TDD (Test Driven Development) again. I have long thought that the [ExpectedException] attribute that MsTest offers is not enough, so when Albin Sunnanbo suggested that I’d look at FluentAssertions I decided to try it.

Verifying Exceptions

FluentAssertions offers a ShouldThrow() extension method to the Action delegate type. It asserts that invoking a particular action will throw an exception.

// Code from https://github.com/KentorIT/authservices/blob/master/
// Kentor.AuthServices.Tests/Saml2ResponseTests.cs
Action a = () => Saml2Response.Read(response).GetClaims();
 
a.ShouldThrow<InvalidOperationException>()
.WithMessage("The Saml2Response must be validated first.");

Compared to the [ExpectedException] attribute this offers much better control.

The Problem with [ExpectedException]

The problem with [ExpectedException] is that it lacks an important property: exactly specifying where the exception is expected. Consider a test that first grabs a suitable object to test with LINQ and then provokes an exception.

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Test_Using_ExpectedException()
{
  var attributes = typeof(Brand).GetMember("Nmae").Single()
    .GetCustomAttributes(false);
 
  // This should throw.
  attributes.OfType<RequiredAttribute>().First()
    .FormatErrorMessage("InvalidName");
}

When run, this test will be flagged green, despite the typo in the GetMember call. The problem is that GetMember throws an InvalidOperationException which is exactly what is required for the method to pass. There is no way with [ExpectedException] to make sure that the exception is thrown at the right place. There is also no way way to test more than one thing (for example a post condition that an object is in a consistent state after a method threw an exception).

Using FluentAssertions instead

With FluentAssertions it’s much better.

[TestMethod]
public void Test_Using_FluentAssertions()
{
    var attributes = typeof(Brand).GetMember("Nmae").Single()
        .GetCustomAttributes(false);
 
    Action a = () => attributes.OfType<RequiredAttribute>().First()
        .FormatErrorMessage("InvalidName");
 
    a.ShouldThrow<InvalidOperationException>();
}

The code to be tested for an exception is now placed in an action. The ShouldThrow() extension method executes the action and monitors for the exception. The test now properly fails because GetMember("Nmae").Single() throws an exception – which is not expected.

More FluentAssertions

The basic syntax of FluentAssertions is to use a bunch of extension methods that extend everything. This is another example from Kentor.AuthServices.

// From https://github.com/KentorIT/authservices/blob/master/
// Kentor.AuthServices.Tests/IdentityProviderTests.cs
var r = ip.CreateAuthenticateRequest();
 
r.ToXElement().Attribute("Destination").Should().NotBeNull()
   .And.Subject.Value.Should().Be(idpUri);

Here I can use one line to test that the result of ToXElement() has a Destination attribute and that the value of the attribute is correct.

The best: ShouldBeEquivalentTo

The best part of FluentAssertions is ShouldBeEquivalentTo. Combined with anonymous types it’s a really powerful way to test a new method. Especially when doing test driven development this is great because I can compile and run the test before I even touch the live code.

var expected = new
{
  Id = "Saml2Response_Read_BasicParams",
  IssueInstant = new DateTime(2013, 01, 01, 0, 0, 0, DateTimeKind.Utc),
  Status = Saml2StatusCode.Requester,
  Issuer = (string)null
};
 
Saml2Response.Read(response).ShouldBeEquivalentTo(expected);

Unfortunately there is a flaw in the API (or I haven’t learnt to use it correctly). ShouldBeEquivalentTo only tests the properties that are present in the subject of the test and ignores any extra properties found in the expected object. I would have preferred to have it the other way around. That way I could introduce a new property by starting with adding it to the expected object in a test.

In real life however that’s not a huge problem – just add the new property with a return null; getter to the tested object and the test will fail. But if I can wish for something, I’d love if FluentAssertions was extended with this behaviour, at least as a configurable option.

Installation FluentAssertions

Getting started with FluentAssertions is extremely easy. Just add the FluentAssertions Nuget package to the project and then include a using FluentAssertions; in the test file.

Installing FluentAssertions is so simple because it is just another way to write the assertions of the test – it doesn’t change the test infrastructure. That means that the test runner (which might require separate installations) is not affected. That’s a big advantage as having to install separate test runners into Visual Studio for a new project is a real pain.

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1461 on 2013-10-11
  • Dennis Doomen on 2013-10-08

    Thanks for the great post. You’re right that the extraneous properties of the expectation are ignored. I have to look at that, but I’m afraid the current design doesn’t offer this flexibility.

  • James Curran on 2013-10-11

    That’s more than an inconvenience. That’s clearly a bug in FluentAssertions. If a property is given in the Expected object, then it’s EXPECTED — if it’s missing, then the test must fail.

    Conversely, if a property appears in the subject, but not the expected, that’s NOT an error (It’s a bonus ;-) ). The extra property could be an implementation detail, or otherwise irrelevant to the test.

    • Dennis Doomen on 2013-10-14

      Let me phrase it differently then….it’s a result of my original design. Again, as I said, I’ll have to see if it is feasible to change that.

Software Development is a Job – Coding is a Passion

I'm Anders Abel, a systems architect and developer working for Kentor in Stockholm, Sweden.

profile for Anders Abel at Stack Overflow, Q&A for professional and enthusiast programmers

The complete code for all posts is available on GitHub.

Popular Posts

Archives

Series

Powered by WordPress with the Passion for Coding theme.