New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compare memberless records by value, others by members #2578
Comments
Hmm.. I would rather try to build a |
Can you make a concrete suggestion? Do you mean something like: CompareAllRecordsByMembers();
CompareRecordsByMembersButWhenTheyDontHaveMembersCompareByValue();
CompareAllRecordsByValue(); ? |
I must be missing something, but what is there to compare between member-less records? |
Their type. That's why I'd like to have value semantic for them. Long version: Let me start with a simplified example. This shows why we actually have member-less records in our object graph: it's about representing state/data via discriminated unions. public record class FeatureLevel
{
public abstract T Match<T>(
Func<Basic, T> basic,
Func<Professional, T> professional);
public record class Basic : FeatureLevel
{
public override T Match<T>(
Func<Basic, T> basic,
Func<Professional, T> professional)
{
return basic(this);
}
}
public record class Professional : FeatureLevel
{
public required int LicenseCount { get; init; }
public override T Match<T>(
Func<Basic, T> basic,
Func<Professional, T> professional)
{
return professional(this);
}
}
} So usage would be: FeatureLevel result = testee.ComputeFeatureLevel(...);
result.Should().BeEquivalentTo(
new FeatureLevel.Professional
{
LicenseCount = 7
}); which could result in an error similar to:
or
Now this example is not very interesting yet, because I could use However, in real life we commonly have records which are more complex (= bigger tree of objects), so both, records with and without members, exist in the same tree. So this is what we do: BeEquivalentTo(
....,
options => options
.CompareByValue<TypeWithoutMembers>()
.CompareByValue<OtherTypeWithoutMembers>()
.CompareByValue<YetAnotherTypeWithoutMembers>()); That's quite cumbersome and error prone. It's often a few rounds of trial and error until I've specified all the ones I need. And then, If ever I remove a without-members type property from a record, I should remove its Sidenote: now that there is records, mostly the benefit of So thank you @dennisdoomen and @jnyrup for this great library! And especially the continued maintenance and support of it :-) |
It's a bit of a niche feature, but I would be fine with somebody contributing an option like |
This problem is not unique to records but memberless types in general, e.g. #2391.
I see how using value semantics on memberless records makes sense, since synthesized I'm wondering if there is a more general applicable approach that could also work for types without overridden |
Maybe something like |
So the options are as follows, correct?
Out of these I'm rather opinionated on Furthermore, I can make a small case as that it should only affect records:
This behavior helps preventing "false positive" tests in the following cases: For a) it doesn't matter whether the involved types are records or not. => The likelihood of false positives with records is smaller. Counterargument would be, that problem b) also occurs when part of your members are Anyway, I'm sure you guys have better insight into this, so in case you both can agree on a variant as acceptable for a PR, I'd happily do that one. |
I that case, |
Ok, so we'd have
How should the implementation respect combinations of these? Should it (not) matter in which sequence these are called? .ComparingRecordsByMembers()
.CompareMemberlessTypesByType() => Memberless records compared by type vs .CompareMemberlessTypesByType()
.ComparingRecordsByMembers() => Memberless non records compared by type, but or should memberless records still be compared by type? |
It doesn't matter. If the type is memberless, it's compared by type. But now I'm in doubt again whether |
Background and motivation
Some of our records don't have properties, some do.
We like to use member-comparison by default, mainly because this enables much improved error messages. However, for records without members, we get the error
We'd like to be able to configure member-less records to be compared by value, without having to specify this for every single memberless record type.
API Proposal
The existing
ComparingRecordsByMembers
is extended by an overload with abool
parameter indicating whether all records should be compared by members or only the ones which actually do have members.For the implementation, I suggest to replace the backing
bool?
with a 3-state enum which is nullable, thus the following four states:Backwards compatibility:
ComparingRecordsByValue()
would set it toc) force comparison by value
ComparingRecordsByMembers()
would set it tob) force comparison by members
ComparingRecordsByMembers(false)
would set it tob) force comparison by members
ComparingRecordsByMembers(true)
would set it to- d) force comparison by members only for records with members (by value for the others)
Alternatively, there could be a single
ComparingRecordsByMembers
method with default argument value:That would be backwards compatible as long as users recompile after updating.
API Usage
Alternative Designs
and usage:
Note: this is similar to what was proposed in PR #1383 from discussion #1374).
However, I reckon that we may want to have 3 config options:
Having a single list of the delegates gives a simple way to prioritize in case multiple delegates give a different result.
Also, in contrast to the
ComparingRecordsByMembers(bool)
API, this prevents the (possible) misconception that "has members" is in sync with how FluentAssertions detects members.This is an advantage and disadvantage at the same time:
IMemberSelector
, he would have to sync this functionality himself.Risks
Are you willing to help with a proof-of-concept (as PR in that or a separate repo) first and as pull-request later on?
Yes, please assign this issue to me.
The text was updated successfully, but these errors were encountered: