Why I created Fluent Assertions in the first place

Edit this page | 4 minute read

A few weeks ago I read The value of open-source is the vision not the source code and that made me think about my own reasons for starting Fluent Assertions, now more than a year ago. In the light of that article, lets briefly go over my own goals for Fluent Assertions.
The intention of a unit test must be clear from the code
An intention revealing unit test has the following characteristics
  1. The arrange, act and assertion parts are clearly distinguishable
  2. The name represents the functional scenario the test verifies
  3. The assertion provides sufficient context to understand why a specific value is expected
For instance, consider this small test.

        [TestMethod]
       
public void When_assigning_the_endorsing_authorities_it_should_update_the_property
()
        {
           
//-----------------------------------------------------------------------------------------------------------

            // Arrange
            //-----------------------------------------------------------------------------------------------------------
            Permit permit = new PermitBuilder().Build();
 
           
//-----------------------------------------------------------------------------------------------------------

            // Act
            //-----------------------------------------------------------------------------------------------------------
            permit.AssignEndorsingAuthorities(new[] { "john", "jane"});
 
           
//-----------------------------------------------------------------------------------------------------------

            // Assert
            //-----------------------------------------------------------------------------------------------------------
            permit.EndorsingAuthorities.Should().BeEquivalentTo(new[] { "john", "jane"
});
        }
Yes, I agree that it's a very simple test and that this scenario should be tested as part of a larger scope. Nonetheless, it clearly illustrates my three points. Now consider this test .

        [TestMethod]
       
public void When_a_substance_is_specified_that_is_not_required_it_should_throw
()
        {
           
//-----------------------------------------------------------------------------------------------------------

            // Arrange
            //-----------------------------------------------------------------------------------------------------------
            var permit = new PermitBuilder().Build();           
           
var someUser = new UserBuilder().Build
();
 
           
var dataMapper = new InMemoryDataMapper
(permit, someUser);
           
var service = new CommandServiceBuilder().Using(dataMapper).Build
();
 
           
//-----------------------------------------------------------------------------------------------------------

            // Act
            //-----------------------------------------------------------------------------------------------------------
            try
            {
                service.
Execute(new AddMeasurementsCommand
                {
                   
Id = permit.Id,
                   
Version = permit.Version
,
                   
Username = someUser.Username
,
                   
Measurements = new
[]
                    {
                       
new MeasurementData("Oxygen", 1.1d, new DateTime
(2011, 4, 13, 16, 30, 0))
                    },
                });

               
Assert.Fail("The expected exception was not thrown"
);
            }

           
//-----------------------------------------------------------------------------------------------------------

            // Assert
            //-----------------------------------------------------------------------------------------------------------
            catch (InvalidOperationException exc)
            {
               
Assert.IsTrue(exc.Message.Contains("not required"
));
            }
        }
It is testing a particular business action against one of its business rules. It's quite easy to understand, but there's still a lot of noise caused by the try...catch construction and the additional Assert.Fail() to assert that an exception was thrown at all. Less important, but still a bit obscure is the usage of the DateTime constructor. With Fluent Assertions, we can do better.
            //-----------------------------------------------------------------------------------------------------------
            // Act
            //-----------------------------------------------------------------------------------------------------------
            Action action = () => service.Execute(new AddMeasurementsCommand
            {
               
Id = permit.Id,
               
Version = permit.Version
,
               
Username = user.Username
,
               
Measurements = new
[]
                {
                   
new MeasurementData("Oxygen", 1.1d, 13.April(2011).At
(16, 30)),
                },
            });
 
           
//-----------------------------------------------------------------------------------------------------------

            // Assert
            //-----------------------------------------------------------------------------------------------------------
            action
                .
ShouldThrow<InvalidOperationException
>()
                .
WithMessage("not required", ComparisonMode.Substring
);

Something similar is also possible for verifying that events have been raised, with special support for the INotifyPropertyChanged interface so common in Silverlight and WPF projects. I blogged about that earlier this year.
A unit test that fails should explain as best as possible what went wrong
Nothing is more annoying then a unit test that fails without clearly explaining why. More than often, you need to set a breakpoint and start up the debugger to be able to figure out what went wrong. Jeremy D. Miller once gave the advice to "keep out of the debugger hell" and I can only agree with that.
For instance, only test a single condition per test case. If you don't, and the first condition fails, the test engine will not even try to test the other conditions. But if any of the others fail, you'll be on your own to figure out which one. I often run into this problem when developers try to combine multiple related tests that test a member using different parameters into one test case. If you really need to do that, consider using a parameterized test that is being called by several clearly named test cases.
Obviously I designed Fluent Assertions to help you in this area. Not only by using clearly named assertion methods, but also by making sure the failure message provides as much information as possible. Consider this example:
"1234567890".Should().Be("0987654321");

This will be reported as:

clip_image001[6]

The fact that both strings are displayed on a separate line is on purpose and happens if any of them is longer than 8 characters. However, if that's not enough, all assertion methods take an optional formatted reason with placeholders, similarly to String.Format, that you can use to enrich the failure message. For instance, the assertion

new[] { 1, 2, 3 }.Should().Contain(item => item > 3, "at least {0} item should be larger than 3", 1);
will fail with:

clip_image002[6]
The code itself should be a great example of what high-quality code should look like
This goal should be rather obvious. I don't only want to deliver a great framework, I also want people to learn from it. And isn't that the single biggest reason why people join open-source projects? However, the article I mentioned before states that the vision behind an open-source project should be much more important than the quality of the source code. As you might expect, I don't agree with that. In fact, one of the challenges I ran into was my desire to control all code contributions so that they complied with Clean Code, my own coding guidelines and my ideas about unit testing.
At first the article made me doubt about the approach to take, but then I decided that the quality of the code was just as important. I now respond to all contribution requests with some information on the way I'd like FA to see evolve. Additionally, I've set-up a dedicated contribution branch that they can use. Depending on the quality of their contribution, merging them into the main branch requires a corresponding amount of work at my side. I know that this might keep some contributors away, but up to now, most of of them agreed with my approach and were willing to deliver high-quality code.

Leave a Comment