-
Notifications
You must be signed in to change notification settings - Fork 731
Description
Comparing enum by value doesn't work even with auto conversion
Description
Testing object graph containing different enum type, but having the same value, it will not pass even auto conversion is enabled.
This is related with issue #1204.
Reproducing the issue
Pay attention at TemplateNo when compared to TemplateDto.Id (int). I expect the old behavior (v5*) to work again, if TemplateDto.Id is 2, then it should be equal to TemplateNo.AccountCreated.
public enum TemplateNo
{
AccountCreated = 2,
AccountDeactivated = 3
}
public class TemplateDomain
{
public TemplateNo No { get; set; }
public string Subject { get; set; } = string.Empty;
}
public class TemplateDto
{
public int Id { get; set; }
public string EmailSubject { get; set; } = string.Empty;
}
[Fact]
public void EnumTest() {
// arrange
var expected = new TemplateDomain { No = TemplateNo.AccountCreated, Subject = "Account created" };
var actual = new TemplateDto { Id = 2, EmailSubject = "Account created" };
// assert
actual.Should().BeEquivalentTo(expected, opt => opt
.WithMapping<TemplateDto>(domain => domain.No, dto => dto.Id)
.WithMapping<TemplateDto>(domain => domain.Subject, dto => dto.EmailSubject)
.ComparingEnumsByValue().WithAutoConversion()));
// or
actual.Should().BeEquivalentTo(expected, opt => opt
.WithMapping<TemplateDto>(domain => domain.No, dto => dto.Id)
.WithMapping<TemplateDto>(domain => domain.Subject, dto => dto.EmailSubject)
.ComparingEnumsByValue().WithAutoConversionFor(o => o.Path.Contains(nameof(TemplateDomain.No))));
}Expected behavior:
Test should pass when enum is set to test by value and auto conversion is enabled
Actual behavior:
Xunit.Sdk.XunitException
Expected property actual.Id to be equivalent to TemplateNo.AccountCreated {value: 2}, but found 2.
With configuration:
- Use declared types and members
- Compare enums by value
- Compare tuples by their properties
- Compare anonymous types by their properties
- Compare records by their members
- Include non-browsable members
- FluentAssertions.Equivalency.Matching.MappedPathMatchingRule
- FluentAssertions.Equivalency.Matching.MappedPathMatchingRule
- Match member by name (or throw)
- Be strict about the order of items in byte arrays
- Without automatic conversion.
Versions
- Fluent Assertion 6.7.0
- .NET 6
Additional info
It's breaking change for v6.0. Currently I use equivalency step for workaround:
/// <summary>
/// See the problem https://fluentassertions.com/upgradingtov6#enums
/// and solution https://stackoverflow.com/questions/71131277/fluentassertions-6-objectgraph-compare-enum-to-string#answer-71134315
/// </summary>
public class EnumVersusIntegerStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context, IEquivalencyValidator nestedValidator) {
if (comparands.Subject is int subject && comparands.Expectation?.GetType().IsEnum == true) {
AssertionScope.Current
.ForCondition(subject == Convert.ToInt32(comparands.Expectation))
.FailWith(() =>
{
decimal? subjectsUnderlyingValue = ExtractDecimal(comparands.Subject);
decimal? expectationsUnderlyingValue = ExtractDecimal(comparands.Expectation);
string subjectsName = GetDisplayNameForEnumComparison(comparands.Subject, subjectsUnderlyingValue);
string expectationName = GetDisplayNameForEnumComparison(comparands.Expectation, expectationsUnderlyingValue);
return new($"Expected {{context:string}} to be equivalent to {expectationName}{{reason}}, but found {subjectsName}.");
});
return EquivalencyResult.AssertionCompleted;
}
if (comparands.Subject?.GetType().IsEnum == true && comparands.Expectation is int expectation) {
AssertionScope.Current
.ForCondition(Convert.ToInt32(comparands.Subject) == expectation)
.FailWith(() =>
{
decimal? subjectsUnderlyingValue = ExtractDecimal(comparands.Subject);
decimal? expectationsUnderlyingValue = ExtractDecimal(comparands.Expectation);
string subjectsName = GetDisplayNameForEnumComparison(comparands.Subject, subjectsUnderlyingValue);
string expectationName = GetDisplayNameForEnumComparison(comparands.Expectation, expectationsUnderlyingValue);
return new($"Expected {{context:enum}} to be equivalent to {expectationName}{{reason}}, but found {subjectsName}.");
});
return EquivalencyResult.AssertionCompleted;
}
return EquivalencyResult.ContinueWithNext;
}
internal static string GetDisplayNameForEnumComparison(object o, decimal? v) {
if (v is null) {
return '\"' + o.ToString() + '\"';
}
string typePart = o.GetType().Name;
string namePart = o.ToString()!.Replace(", ", "|", StringComparison.Ordinal);
string valuePart = v.Value.ToString(CultureInfo.InvariantCulture);
return $"{typePart}.{namePart} {{{{value: {valuePart}}}}}";
}
internal static decimal? ExtractDecimal(object o) => o.GetType().IsEnum
? Convert.ToDecimal(o, CultureInfo.InvariantCulture)
: null;
}then test it like this
// assert
actual.Should().BeEquivalentTo(expected, opt => opt
.WithMapping<TemplateDto>(domain => domain.No, dto => dto.Id)
.WithMapping<TemplateDto>(domain => domain.Subject, dto => dto.EmailSubject)
.Using(new EnumVersusIntegerStep())));I know I can make it pass right now. But it would be nice to see simpler solution. I will remove my workaround so I don't have to maintain any extra code and less explanation to my teammate for what I'm doing.