user246392
user246392

Reputation: 3017

How to exclude multiple properties from comparison and diff check objects efficiently in FluentAssertions

In FluentAssertions, I can compare the object graphs of two objects of the same type and exclude properties from the comparison.

order.Should().BeEquivalentTo(anotherOrder, options => 
    options.Excluding(o => o.Customer.Name))
           .Excluding(o => /* Add more properties */);

I can then manually assert the excluded properties to make sure they were different or have a specific value:

order.Customer.Name.Should().NotBe(anotherOrder.Customer.Name).Should().Be("John");
// assert other properties

Assuming the list of properties to exclude is fairly large, is there a more concise way of combining the above statements to do diff checks?

Upvotes: 2

Views: 1865

Answers (1)

Chris Schaller
Chris Schaller

Reputation: 16584

A key concept driving FluentAssertions is to reduce complex and repetitive comparison checking and validation messaging down simple expressions that can manage the comparison for you.

In the OP, the example shows excluding Customer.Name from the initial comparison and then is explicitly validating the same field that was originally excluded with a hint that multiple properties need to be both excluded from the first set and separately included in individual assertions.

This concept really goes against what FluentAssertions is designed to do for you. To save you the repetitive code you should construct the specific object to compare against with all the fields set, rather than individual assertions against literal values.

For instance, this forked fiddle demonstrates some of the different ways you can declare your assertions on a simple nested object graph. When there are multiple failed assertions you can see all the results in a single output:

var order = externalProcess.FetchOrder();
var expectedOrder = new Order {
        Id = 201,
        Date = new DateTime(2022,11,01),
        Customer = new Customer {
            Id = 46,
            Name= "Johnothan" 
        }
    };
order.Should().BeEquivalentTo(expectedOrder);

If the retrieved order does not match, then this will generate an exception message with a deep analysis of all the failed assertions:

FluentAssertions.Execution.AssertionFailedException: Expected property root.Customer.Id to be 46, but found 22.
Expected property root.Customer.Name to be "Johnothan" with a length of 9, but "John" has a length of 4, differs near "n" (index 3).

If you wanted to exclude/include multiple specific properties from the assertion, then there are two forms of configuration you can use:

Inline expression based lambda options:

    order.Should().BeEquivalentTo(expectedOrder, options => 
            options.Excluding(o => o.Customer.Name)
                   .Excluding(o => o.Customer.Id)
        );

Method based options:

    order.Should().BeEquivalentTo(expectedOrder, options => 
        {
            options.Excluding(o => o.Customer.Name);
            ...
            options.Excluding(o => o.Customer.Id);
            return options;
        });

But you really shouldn't have overly complex assertion conditions. If your intent was to exclude most of the Customer properties, but not all, then you can exclude the whole Customer property, and selectively include the fields that you need:

    order.Should().BeEquivalentTo(expectedOrder, options => 
            options.Excluding(o => o.Customer)
                   .Including(o => o.Customer.Name)
        );

This style of complementing include and exclude allows you to reduce the number of individual properties that need to be configured, but it also allows you to skip intermediary object relationships.

Consider if Customer has a complex property called Address and we wanted to compare that the Customer.Addressmatched but we want to exclude comparisons on theCustomer` object:

    order.Should().BeEquivalentTo(expectedOrder, options => 
            options.Excluding(o => o.Customer)
                   .Including(o => o.Customer.Address)
        );

That skips the intermediary Customer from the comparison. But what if Order also has an Address and we want to test that the Order.Address equals the Customer.Address in the comparison object, well this is where we can use separate assertions

    order.Should().BeEquivalentTo(expectedOrder, options => 
            options.Excluding(o => o.Customer)
        );
    order.Address.Should().BeEquivalentTo(order.Customer.Address);

There is a concept of mapping but we can't easily use it here because the path depths are expected the be the same, mapping is designed to allow mapping between similar typed properties at the same level but with different names.

If it helps, this was the end of my fiddle from playing around with these options: https://dotnetfiddle.net/TSLOXg

Upvotes: 3

Related Questions