Skip to content

Comparing enum by value doesn't work even with auto conversion #2011

@fakhrulhilal

Description

@fakhrulhilal

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions