Breaking with the past (or…Fluent Assertions 2.0 is in beta)

Edit this page | 6 minute read

After many months of development, in particular during in evenings after work and in the weekends, we’re proud to present the first (and hopefully the only) beta of Fluent Assertions 2.0. Together with my good friend and coworker Martin Opdam, and supported by noticeable contributors like Oren Novotny and Ian Obermiller, we managed to introduce a shipload of new features.
 


For instance, Oren worked on the foundations for supporting the new Windows 8 Metro-style apps, whereas Ian created the initial Windows Phone 7.5 version. Martin added support for .NET 4.5, added quite a few new variations and overloads to the existing assertions and also fixed a lot of the reported bugs. I myself introduced a completely new extensible API for comparing two objects graphs for equivalence. As the original founder of this project, I also took charge of safeguarding the overall quality and consistency.

And now that I mention it, after a week of debugging I almost decided to drop Windows Phone support altogether. I was suffering from two really persistent ReflectionTypeLoadExceptions and MissingMethodExceptions. Since I’ve started developing on the .NET platform in 2001, I’ve run into some pretty weird issues, but this was a whole new league for me (read the StackOverflow discussion to learn a bit more about this issue). Fortunately Geert van Horrik pointed me at a blog post he wrote a while back that explained Windows Phone’s lack of support for co- and contravariance. And indeed, that was exactly the problem I was facing all the time.

So in addition to support for the aforementioned .NET flavors and support for MBUnit and the Gallio Automation Platform, what else did we add?

What's new for collections
  • Added (Not)BeInAscendingOrder(), (Not)BeInDescendingOrder(), IntersectWith(otherCollection) and NotIntersectWith(otherCollection).
  • ContainInOrder() got an overload that takes a params object[] argument rather than an IEnumerable in case you don't care about the reason.
  • Added ContainSingle() that asserts there is only a single element in the collection that matches the specified lambda.
  • Added an overload to Equal() that takes a lambda that is used for checking the equality of two collections without relying on the type’s Equals() method. Consider for instance two collections that contain some kind of domain entity persisted to a database and then reloaded. Since the actual object instance is different, if you want to make sure a particular property was properly persisted, you usually do something like this:
    persistedCustomers.Select(c => c.Name).Should().Equal(customers.Select(c => c.Name);
    With this new overload, you can rewrite it into:
    persistedCustomers.Should().Equal(customers, c => c.Name);
  • Fixed a bug that occurred when two collections are compared for equality but the collection contains nulls.

What's new for strings
  • When strings differ in length, it will report the expected and actual lengths as part of the error messages.
  • An ArgumentOutOfRangeException was thrown when asserting that a string started with a specific string and the first string was shorter than the expected string.

What's new for numbers
  • Added support for using Be() on nullable numeric types.
  • Added BeOneOf() to assert that a value matches one of the provided values.
  • Added support for (nullable) decimals
  • BePositive() and BeNegative() now also work for floats and doubles.

What's new for dates and times
  • Added NotBe(). Also added BeOneOf() to verify that the value matches one of the provided values.  
  • Added BeCloseTo() to assert that a date/time is within a specified number of milliseconds from another date/time value. This can be particularly useful if your database truncates date/time values.
  • If a date/time value has non-zero milliseconds then that will be displayed in the error messages .

What's new for comparing object graphs
I’ll dedicate a separate blog post on the new object.ShouldBeEquivalentTo(object) and collection.ShouldAllBeEquivalentTo(collection), but the existing API based on the object.ShouldHave() has benefited from the internal redesign as well.
  • You can now apply the property equality comparisons to entire collections of objects. It doesn't matter what kind of collection type you use, as long as it contains the same number of objects, and the object’s properties match.
  • The error message now includes the index of the mismatching object.
  • An exception was thrown when comparing the properties of an object and one of them causes a cyclic reference. You can now alter that behavior by passing in a value from the CyclicReferenceHandling enum into the IncludingNestedObjects() method.
  • Added support for references to an interface rather than concrete types. You can find an example of that at the corresponding feature request page.
  • Added support for comparing two anonymous types using SharedProperties().
  • Fixed a bug that caused an exception to be thrown for write-only properties. They are now ignored.
  • Fixed a stack overflow exception when formatting an object graph that contains a static property with a cyclic reference.
  • Fixed an exception that was thrown when formatting an object graph and one of the properties throws.
  • The name of a date/time property was not included in the error message when AllProperties() failed on that property

What's new for types
  • Added the static AllTypes class with a static method From(Assembly assembly) as a wrapper around the Types extension method on Assembly. This allows for a more fluent syntax like
    AllTypes.From(assembly)
    .ThatAreDecoratedWith<SomeAttribute>()
    .ThatImplement<ISomeInterface>()
    .ThatAreInNamespace("Internal.Main.Test");
  • Added support for asserting that the properties of an attribute have a specific value. For instance: 
    typeWithAttribute.Should()
    .BeDecoratedWith<DummyClassAttribute>(a => ((a.Name == "Unexpected") && a.IsEnabled));
What other improvements did we include 
  • Fixed an exception when asserting the equality of dictionaries that contain nulls.
  • The NuGet package will automatically add references to the System.Xml and System.Xml.Linq references.
  • Added a ShouldThrow<TException>() method for a Func<Task> to support working with async methods like this:
      Func<Task> slowFunction = async () => { await slowObject.ThrowAsync<ArgumentException>(); };
    slowFunction.ShouldThrow<InvalidOperationException>(); 
  • Improved several of the error messages related to assertions on XDocument and XElement.  
  • All overloads that took the reason and reasonargs parameters have been removed and replaced with optional parameters
  • Allowed ShouldRaisePropertyChangeFor(null) to assert that an object implemented INotifyPropertyChanged raised the changed event for all properties.

What’s new for extensibility
  • The list of IValueFormatter objects on the Formatter class can be altered to insert a custom formatter.
  • Introduced a mechanism to override the way Fluent Assertions formats objects in an error message by annotating a static method with the [ValueFormatter] attribute. If a class doesn’t override ToString(), the built-in DefaultValueFormatter will render an object graph of that object. But you can now override that using a construct like this:
    public static class CustomFormatter
    {
    [ValueFormatter]
    public static string Foo(SomeClassvalue)
    {
    return "Property = " + value.Property;
    }
    }
  • We introduced a mechanism so that the error message of custom assertion extensions can specify the {context} tag that is used to inject the property path in object graph comparisons. For instance, in the date/time assertions, this is used to display date and time. But when this assertion is used as part of a recursive object graph comparison, it will display the property path instead.
    Execute.Verification
    .ForCondition(Subject.Value == expected)
    .BecauseOf(reason, reasonArgs)
    .FailWith("Expected {context:date and time} to be {0}{reason}, but found {1}.", expected, subject.Value);
  • The NuGet package is now based on NuGet 2.0.

Breaking changes
Since we decided to bump up the version from 1.7.1 to 2.0 and we’ve already told you that we comply to Semantic Versioning, you can expect some breaking changes in this release.
  • In order to ensure that all extension methods are always available through the FluentAssertions namespace, we had to get rid of the FluentAssertions.Assertions namespace. Just use a global search-replace to remove all those usage statements.  
  • Many of the assertion classes have moved into dedicated namespaces. You might have to fix any code that inherits from those classes.
  • Remove the obsolete Verify() methods from the Verification class

So what’s next
To get started, get the NuGet package from inside Visual Studio (just make sure you include pre-release versions), or get the ZIP through the CodePlex download page. If you run into issues, I would prefer that you ping me directly on Twitter before you file an issue on CodePlex’s Issue Tracker. By the way, we use the #fluentassertions tag on Twitter. For questions, remarks or suggestions, you can use the Discussions page, StackOverflow, or you can contact me directly by email or Twitter.

Off topic: Although we’re still on CodePlex, the source code of Fluent Assertions is now stored in a Git repository. That should make it a whole lot easier for contributors to fork the code, write a nice improvement, and send us a pull request.

Leave a Comment