Skip to content

Commit

Permalink
Constrain equivalency comparison of enum members
Browse files Browse the repository at this point in the history
  • Loading branch information
jnyrup committed Feb 9, 2021
1 parent 6170e24 commit deca4db
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 13 deletions.
27 changes: 16 additions & 11 deletions Src/FluentAssertions/Equivalency/EnumEqualityStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAsserti
public bool Handle(IEquivalencyValidationContext context, IEquivalencyValidator parent,
IEquivalencyAssertionOptions config)
{
Execute.Assertion
.ForCondition(context.Subject?.GetType().IsEnum == true)
.FailWith(() =>
{
decimal? expectationsUnderlyingValue = ExtractDecimal(context.Expectation);
string expectationName = GetDisplayNameForEnumComparison(context.Expectation, expectationsUnderlyingValue);
return new FailReason($"Expected {{context:enum}} to be equivalent to {expectationName}{{reason}}, but found {{0}}.", context.Subject);
});

switch (config.EnumEquivalencyHandling)
{
case EnumEquivalencyHandling.ByValue:
Expand Down Expand Up @@ -68,7 +78,7 @@ private static void HandleByValue(IEquivalencyValidationContext context)

private static void HandleByName(IEquivalencyValidationContext context)
{
string subject = context.Subject?.ToString();
string subject = context.Subject.ToString();
string expected = context.Expectation.ToString();

Execute.Assertion
Expand All @@ -89,18 +99,13 @@ private static string GetDisplayNameForEnumComparison(object o, decimal? v)
{
if (o is null || v is null)
{
return "null";
}

if (o.GetType().IsEnum)
{
string typePart = o.GetType().Name;
string namePart = o.ToString().Replace(", ", "|", StringComparison.Ordinal);
string valuePart = v.Value.ToString(CultureInfo.InvariantCulture);
return $"{typePart}.{namePart} {{{{value: {valuePart}}}}}";
return "<null>";
}

return v.Value.ToString(CultureInfo.InvariantCulture);
string typePart = o.GetType().Name;
string namePart = o.ToString().Replace(", ", "|", StringComparison.Ordinal);
string valuePart = v.Value.ToString(CultureInfo.InvariantCulture);
return $"{typePart}.{namePart} {{{{value: {valuePart}}}}}";
}

private static decimal? ExtractDecimal(object o)
Expand Down
88 changes: 86 additions & 2 deletions Tests/FluentAssertions.Specs/Equivalency/BasicEquivalencySpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3676,7 +3676,7 @@ public void When_asserting_enums_typed_as_object_are_equivalent_it_should_fail()
}

[Fact]
public void When_a_numeric_member_is_compared_with_an_enum_it_should_respect_the_enum_options()
public void When_a_numeric_member_is_compared_with_an_enum_it_should_throw()
{
// Arrange
var actual = new
Expand All @@ -3692,10 +3692,94 @@ public void When_a_numeric_member_is_compared_with_an_enum_it_should_respect_the
// Act
Action act = () => actual.Should().BeEquivalentTo(expected, options => options.ComparingEnumsByValue());

// Assert
act.Should().Throw<XunitException>();
}

[Fact]
public void When_a_string_member_is_compared_with_an_enum_it_should_throw()
{
// Arrange
var actual = new
{
Property = "First"
};

var expected = new
{
Property = TestEnum.First
};

// Act
Action act = () => actual.Should().BeEquivalentTo(expected, options => options.ComparingEnumsByName());

// Assert
act.Should().Throw<XunitException>();
}

[Fact]
public void When_null_enum_members_are_compared_by_name_it_should_succeed()
{
// Arrange
var actual = new
{
Property = null as TestEnum?
};

var expected = new
{
Property = null as TestEnum?
};

// Act
Action act = () => actual.Should().BeEquivalentTo(expected, options => options.ComparingEnumsByName());

// Assert
act.Should().NotThrow();
}

[Fact]
public void When_null_enum_members_are_compared_by_value_it_should_succeed()
{
// Arrange
var actual = new
{
Property = null as TestEnum?
};

var expected = new
{
Property = null as TestEnum?
};

// Act
Action act = () => actual.Should().BeEquivalentTo(expected, options => options.ComparingEnumsByValue());

// Assert
act.Should().NotThrow();
}

[Fact]
public void When_zero_and_null_enum_are_compared_by_value_it_should_throw()
{
// Arrange
var actual = new
{
Property = (TestEnum)0
};

var expected = new
{
Property = null as TestEnum?
};

// Act
Action act = () => actual.Should().BeEquivalentTo(expected, options => options.ComparingEnumsByValue());

// Assert
act.Should().Throw<XunitException>();
}

public enum TestEnum
{
First = 1
Expand All @@ -3714,7 +3798,7 @@ public void When_subject_is_null_and_enum_has_some_value_it_should_throw()

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected*to equal EnumULong.UInt64Max {value: 18446744073709551615} by name because comparing enums should throw, but found null*");
.WithMessage("Expected*to be equivalent to EnumULong.UInt64Max {value: 18446744073709551615} because comparing enums should throw, but found <null>*");
}

[Fact]
Expand Down
3 changes: 3 additions & 0 deletions docs/_pages/objectgraphs.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ An option to compare an `Enum` only by name is also available, using the followi
orderDto.Should().BeEquivalentTo(expectation, options => options.ComparingEnumsByName());
```

Note that even though an enum's underlying value equals a numeric value or the enum's name equals some string value, we do not consider those to be equivalent.
In other words, enums are only considered to be equivalent to enums of the same or another type, but you can control whether they should equal by name or by value.

### Collections and Dictionaries ###
Considering our running example, you could use the following against a collection of `OrderDto`s:

Expand Down
13 changes: 13 additions & 0 deletions docs/_pages/upgradingtov6.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,18 @@ Technically nullable enums are handled by `NullableEnumAssertions` which in addi
If you want to compare two enums of different types, you can use `HaveSameValueAs` or `HaveSameNameAs` depending on how _you_ define equality for different enums.
Lastly, if you want to verify than an enum has a specific integral value, you can use `HaveValue`.

When comparing object graphs with enum members, we have constrained when we consider them to be equivalent.
An enum is now only considered to be equivalent to an enum of the same of another type, but you can control whether they should equal by name or by value.
The practical implications are that the following examples now fails.
```cs
var subject = new { Value = "One" };
var expectation = new { Value = MyOtherEnum.One };
subject.Should().BeEquivalentTo(expectation, opt => opt.ComparingEnumsByName());

var subject = new { Value = 1 };
var expectation = new { Value = MyOtherEnum.One };
subject.Should().BeEquivalentTo(expectation, opt => opt.ComparingEnumsByValue());
```

If your assertions rely on the formatting of enums in failure messages, you'll notice that we have given it a facelift.
Previously, formatting an enum would simply be a call to `ToString()`, but to provide more detail we now format `MyEnum.One` as `"MyEnum.One(1)"` instead of `"One"`.

0 comments on commit deca4db

Please sign in to comment.