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.
Is the grass greener on the other side? Trying FluentAssertions instead of plain MsTest Assert class and it's WAY better.
— Anders Abel (@anders_abel) September 17, 2013
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.
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.
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.
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.