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 Sep 17, 2018
1 parent 68ca857 commit 93a1972
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 99 deletions.
69 changes: 68 additions & 1 deletion Src/FluentAssertions/Collections/CollectionAssertions.cs
Expand Up @@ -349,6 +349,71 @@ 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>
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 Expand Up @@ -1371,7 +1436,9 @@ public AndConstraint<TAssertions> HaveElementPreceding(object successor, object
.Then
.Given(subject => PredecessorOf(successor, subject))
.ForCondition(predecessor => predecessor.IsSameOrEqualTo(expectation))
.FailWith("but found {0}.", predecessor => predecessor);
.FailWith("but found {0}.", predecessor => predecessor)
.Then
.ClearExpectation();

return new AndConstraint<TAssertions>((TAssertions)this);
}
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);
}
}
}
16 changes: 14 additions & 2 deletions Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs
Expand Up @@ -31,7 +31,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 @@ -57,6 +57,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 @@ -90,7 +103,6 @@ private void LooselyMatchAgainst<T>(IList<object> subjects, T expectation, int e
var results = new AssertionResultSet();
int index = 0;
GetTraceMessage getMessage = path => $"Comparing subject at {path}[{index}] with the expectation at {path}[{expectationIndex}]";
int count = subjects.Count;
int indexToBeRemoved = -1;

for (var metaIndex = 0; metaIndex < unmatchedSubjectIndexes.Count; metaIndex++)
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 AssertionScope scope, ICollection<object> subject, ICollection<T> expectation)
{
return scope
Expand Down
Expand Up @@ -184,7 +184,9 @@ private static Type GetIDictionaryInterface(Type expectedType)
.FailWith("but has additional key(s) {0}", keyDifference.AdditionalKeys)
.Then
.ForCondition(!hasMissingKeys || !hasAdditionalKeys)
.FailWith("but it misses key(s) {0} and has additional key(s) {1}", keyDifference.MissingKeys, keyDifference.AdditionalKeys);
.FailWith("but it misses key(s) {0} and has additional key(s) {1}", keyDifference.MissingKeys, keyDifference.AdditionalKeys)
.Then
.ClearExpectation();
}

private static KeyDifference<TSubjectKey, TExpectedKey> CalculateKeyDifference<TSubjectKey, TSubjectValue, TExpectedKey,
Expand Down
12 changes: 11 additions & 1 deletion Src/FluentAssertions/Execution/AssertionScope.cs
Expand Up @@ -79,7 +79,6 @@ internal AssertionScope(AssertionScope sourceScope, bool sourceSucceeded)
reason = sourceScope.reason;
useLineBreaks = sourceScope.useLineBreaks;
parent = sourceScope.parent;
expectation = sourceScope.expectation;
evaluateCondition = sourceSucceeded;
Context = sourceScope.Context;
}
Expand Down Expand Up @@ -171,6 +170,17 @@ public AssertionScope WithExpectation(string message, params object[] args)
return this;
}

/// <summary>
/// Clears the expectation set by <see cref="WithExpectation"/>.
/// </summary>
// SMELL: It would be better to give the expectation an explicit scope, but that would be a breaking change.
public Continuation ClearExpectation()
{
expectation = null;

return new Continuation(this, true);
}

/// <summary>
/// Allows to safely select the subject for successive assertions, even when the prior assertion has failed.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions Src/FluentAssertions/Execution/GivenSelector.cs
Expand Up @@ -131,5 +131,11 @@ public ContinuationOfGiven<T> FailWith(string message, params object[] args)

return new ContinuationOfGiven<T>(this, succeeded);
}

public ContinuationOfGiven<T> ClearExpectation()
{
parentScope.ClearExpectation();
return new ContinuationOfGiven<T>(this, parentScope.Succeeded);
}
}
}

0 comments on commit 93a1972

Please sign in to comment.