Skip to content

Commit

Permalink
Comparing an object graph against IEnumerable works now as expected
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Nov 6, 2018
1 parent b74b041 commit e58b723
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 89 deletions.
72 changes: 72 additions & 0 deletions Src/FluentAssertions/Collections/CollectionAssertions.cs
Expand Up @@ -349,6 +349,78 @@ public AndConstraint<TAssertions> BeEquivalentTo(params object[] expectations)
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that a collection of objects is equivalent to another collection of objects.
/// </summary>
/// <remarks>
/// Objects within the collections are equivalent when both object graphs have equally named properties with the same
/// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another
/// and the result is equal.
/// The type of a collection property is ignored as long as the collection implements <see cref="IEnumerable"/> and all
/// items in the collection are structurally equal.
/// Notice that actual behavior is determined by the global defaults managed by <see cref="AssertionOptions"/>.
/// </remarks>
/// <param name="because">
/// An optional formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the
/// assertion is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public AndConstraint<TAssertions> BeEquivalentTo(IEnumerable expectation, string because = "", params object[] becauseArgs)
{
BeEquivalentTo(expectation, config => config, because, becauseArgs);

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that a collection of objects is equivalent to another collection of objects.
/// </summary>
/// <remarks>
/// Objects within the collections are equivalent when both object graphs have equally named properties with the same
/// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another
/// and the result is equal.
/// The type of a collection property is ignored as long as the collection implements <see cref="IEnumerable"/> and all
/// items in the collection are structurally equal.
/// Notice that actual behavior is determined by the global defaults managed by <see cref="AssertionOptions"/>.
/// </remarks>
/// <param name="config">
/// A reference to the <see cref="EquivalencyAssertionOptions{TSubject}"/> configuration object that can be used
/// to influence the way the object graphs are compared. You can also provide an alternative instance of the
/// <see cref="EquivalencyAssertionOptions{TSubject}"/> class. The global defaults are determined by the
/// <see cref="AssertionOptions"/> class.
/// </param>
/// <param name="because">
/// An optional formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the
/// assertion is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public AndConstraint<TAssertions> BeEquivalentTo(IEnumerable expectation,
Func<EquivalencyAssertionOptions<IEnumerable>, EquivalencyAssertionOptions<IEnumerable>> config, string because = "",
params object[] becauseArgs)
{
EquivalencyAssertionOptions<IEnumerable> options = config(AssertionOptions.CloneDefaults<IEnumerable>());

var context = new EquivalencyValidationContext
{
Subject = Subject,
Expectation = expectation,
RootIsCollection = true,
CompileTimeType = typeof(IEnumerable),
Because = because,
BecauseArgs = becauseArgs,
Tracer = options.TraceWriter
};

var equivalencyValidator = new EquivalencyValidator(options);
equivalencyValidator.AssertEquality(context);

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that a collection of objects is equivalent to another collection of objects.
/// </summary>
Expand Down
67 changes: 0 additions & 67 deletions Src/FluentAssertions/Collections/NonGenericCollectionAssertions.cs
Expand Up @@ -301,72 +301,5 @@ private int GetMostLocalCount()

return base.NotContain(new[] { unexpected }, because, becauseArgs);
}

/// <summary>
/// Asserts that a collection of objects is equivalent to another collection of objects.
/// </summary>
/// <remarks>
/// Objects within the collections are equivalent when both object graphs have equally named properties with the same
/// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another
/// and the result is equal.
/// The type of a collection property is ignored as long as the collection implements <see cref="IEnumerable"/> and all
/// items in the collection are structurally equal.
/// Notice that actual behavior is determined by the global defaults managed by <see cref="AssertionOptions"/>.
/// </remarks>
/// <param name="because">
/// An optional formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the
/// assertion is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public void BeEquivalentTo(IEnumerable expectation, string because = "", params object[] becauseArgs)
{
BeEquivalentTo(expectation, config => config, because, becauseArgs);
}

/// <summary>
/// Asserts that a collection of objects is equivalent to another collection of objects.
/// </summary>
/// <remarks>
/// Objects within the collections are equivalent when both object graphs have equally named properties with the same
/// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another
/// and the result is equal.
/// The type of a collection property is ignored as long as the collection implements <see cref="IEnumerable"/> and all
/// items in the collection are structurally equal.
/// </remarks>
/// <param name="config">
/// A reference to the <see cref="EquivalencyAssertionOptions{TSubject}"/> configuration object that can be used
/// to influence the way the object graphs are compared. You can also provide an alternative instance of the
/// <see cref="EquivalencyAssertionOptions{TSubject}"/> class. The global defaults are determined by the
/// <see cref="AssertionOptions"/> class.
/// </param>
/// <param name="because">
/// An optional formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the
/// assertion is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public void BeEquivalentTo(IEnumerable expectation,
Func<EquivalencyAssertionOptions<IEnumerable>, EquivalencyAssertionOptions<IEnumerable>> config, string because = "",
params object[] becauseArgs)
{
EquivalencyAssertionOptions<IEnumerable> options = config(AssertionOptions.CloneDefaults<IEnumerable>());

var context = new EquivalencyValidationContext
{
Subject = Subject,
Expectation = expectation,
RootIsCollection = true,
CompileTimeType = typeof(IEnumerable),
Because = because,
BecauseArgs = becauseArgs,
Tracer = options.TraceWriter
};

var equivalencyValidator = new EquivalencyValidator(options);
equivalencyValidator.AssertEquality(context);
}
}
}
Expand Up @@ -33,7 +33,7 @@ public EnumerableEquivalencyValidator(IEquivalencyValidator parent, IEquivalency

public void Execute<T>(object[] subject, T[] expectation)
{
if (AssertIsNotNull(expectation, subject) && EnumerableEquivalencyValidatorExtensions.AssertCollectionsHaveSameCount(subject, expectation))
if (AssertIsNotNull(expectation, subject) && AssertCollectionsHaveSameCount(subject, expectation))
{
if (Recursive)
{
Expand All @@ -59,6 +59,19 @@ private bool AssertIsNotNull(object expectation, object[] subject)
.FailWith("Expected {context:subject} to be <null>, but found {0}.", new object[] { subject });
}

private static Continuation AssertCollectionsHaveSameCount<T>(ICollection<object> subject, ICollection<T> expectation)
{
return AssertionScope.Current
.WithExpectation("Expected {context:subject} to be a collection with {0} item(s){reason}", expectation.Count)
.AssertEitherCollectionIsNotEmpty(subject, expectation)
.Then
.AssertCollectionHasEnoughItems(subject, expectation)
.Then
.AssertCollectionHasNotTooManyItems(subject, expectation)
.Then
.ClearExpectation();
}

private void AssertElementGraphEquivalency<T>(object[] subjects, T[] expectations)
{
unmatchedSubjectIndexes = new List<int>(subjects.Length);
Expand Down
Expand Up @@ -6,17 +6,6 @@ namespace FluentAssertions.Equivalency
{
internal static class EnumerableEquivalencyValidatorExtensions
{
public static Continuation AssertCollectionsHaveSameCount<T>(ICollection<object> subject, ICollection<T> expectation)
{
return AssertionScope.Current
.WithExpectation("Expected {context:subject} to be a collection with {0} item(s){reason}", expectation.Count)
.AssertEitherCollectionIsNotEmpty(subject, expectation)
.Then
.AssertCollectionHasEnoughItems(subject, expectation)
.Then
.AssertCollectionHasNotTooManyItems(subject, expectation);
}

public static Continuation AssertEitherCollectionIsNotEmpty<T>(this IAssertionScope scope, ICollection<object> subject, ICollection<T> expectation)
{
return scope
Expand Down
Expand Up @@ -88,14 +88,14 @@ private static bool HaveSameDimensions(object subject, Array expectation)
return sameDimensions;
}

private static bool HaveSameRank(object expectation, Array subject)
private static bool HaveSameRank(object subject, Array expectation)
{
var expectationAsArray = (Array)expectation;
var subjectAsArray = (Array)subject;

return AssertionScope.Current
.ForCondition(subject.Rank == expectationAsArray.Rank)
.FailWith("Expected {context:array} to have {0} dimension(s), but it has {1}.", expectationAsArray.Rank,
subject.Rank);
.ForCondition(subjectAsArray.Rank == expectation.Rank)
.FailWith("Expected {context:array} to have {0} dimension(s), but it has {1}.", expectation.Rank,
subjectAsArray.Rank);
}
}

Expand Down
50 changes: 45 additions & 5 deletions Tests/Shared.Specs/CollectionEquivalencySpecs.cs
Expand Up @@ -1217,6 +1217,46 @@ public void When_asserting_equivalence_of_non_generic_collections_it_should_resp
.WithMessage("*Wheels*not have*VehicleId*not have*");
}

[Fact]
public void When_comparing_against_a_non_generic_collection_it_should_treat_it_as_unordered_collection_of_objects()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
List<Type> actual = new List<Type> { typeof(int), typeof(string) };
IEnumerable expectation = new List<Type> { typeof(string), typeof(int) };

//-----------------------------------------------------------------------------------------------------------
// Act
//-----------------------------------------------------------------------------------------------------------
Action act = () => actual.Should().BeEquivalentTo(expectation);

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

[Fact]
public void When_comparing_against_a_non_generic_collection_it_should_treat_it_as_collection_of_objects()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
List<Type> actual = new List<Type> { typeof(int), typeof(string) };
IEnumerable expectation = new List<Type> { typeof(string), typeof(int) };

//-----------------------------------------------------------------------------------------------------------
// Act
//-----------------------------------------------------------------------------------------------------------
Action act = () => actual.Should().BeEquivalentTo(expectation, o => o.WithStrictOrdering());

//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>().WithMessage("Expected*item[0]*String*Int32*item[1]*Int32*String*");
}

[Fact]
public void When_custom_assertion_rules_are_utilized_the_rules_should_be_respected()
{
Expand Down Expand Up @@ -1555,7 +1595,7 @@ public void When_the_expectation_is_not_a_multi_dimensional_array_it_should_thro
// Assert
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>()
.WithMessage("Expected*29*but*contains 23 item(s) less than*");
.WithMessage("Expected*not-a-multi-dimensional-array*but found {1, 2, 3, 4, 5, 6}*");
}

[Fact]
Expand Down Expand Up @@ -1651,13 +1691,13 @@ public void When_the_number_of_dimensions_of_the_arrays_are_not_the_same_it_shou
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var actual = new[,]
var expectation = new[,]
{
{ 1, 2, 3 },
{ 4, 5, 6 }
};

var expectation = new[]
var actual = new[]
{
1, 2
};
Expand All @@ -1672,9 +1712,9 @@ public void When_the_number_of_dimensions_of_the_arrays_are_not_the_same_it_shou
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>()
#if NETCOREAPP1_1
.WithMessage("Expected subject*2 item(s)*but*contains 4 item(s) more than*");
.WithMessage("Expected array*2 dimension(s)*but it has 1*");
#else
.WithMessage("Expected actual*2 item(s)*but*contains 4 item(s) more than*");
.WithMessage("Expected actual*2 dimension(s)*but it has 1*");
#endif
}

Expand Down

0 comments on commit e58b723

Please sign in to comment.