Reputation: 3017
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
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 the
Customer` 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