Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comparing an object graph against IEnumerable works now as expected #911

Merged
merged 1 commit into from Nov 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
{
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()
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
{
//-----------------------------------------------------------------------------------------------------------
// 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