Visitar URL original
Comparing enum by value doesn't work even with auto conversion · Issue #2011 · fluentassertions/fluentassertions · GitHub
Skip to content

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

Closed

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.

Activity

  1. Ruijin92 commented on Dec 5, 2022

    @Ruijin92
    Contributor

    I think the problem here is that the framework tries to convert the int variable to the enum variable. The inversion would map your enums to int variables.

    But i guess it would misrepresent the logic in that case?

    public void EnumTest()
    {
        // arrange
        var expected = new TemplateDto { Id = 2, EmailSubject = "Account created" };
        var actual = new TemplateDomain { No = TemplateNo.AccountCreated, Subject = "Account created" };
    
        // assert
        actual.Should().BeEquivalentTo(expected, opt => opt
            .WithMapping<TemplateDomain>(dto => dto.Id, domain => domain.No)
            .WithMapping<TemplateDomain>(dto => dto.EmailSubject, domain => domain.Subject)
            .WithAutoConversion());
    }
  2. dennisdoomen commented on Dec 27, 2022

    @dennisdoomen
    Member

    I think the problem here is that the framework tries to convert the int variable to the enum variable. The inversion would map your enums to int variables.

    This is exactly what is happening, and we use Convert.ChangeType for that. Unfortunately, that .NET method doesn't support converting from an int to a enum. We would have to add special support for that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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