Skip to content

Commit

Permalink
Add NotContainEquivalentOf
Browse files Browse the repository at this point in the history
  • Loading branch information
ishimko committed Apr 26, 2020
1 parent c34bb3e commit 0b37e37
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 1 deletion.
111 changes: 111 additions & 0 deletions Src/FluentAssertions/Collections/CollectionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,117 @@ public AndConstraint<TAssertions> BeEquivalentTo(IEnumerable expectation, string
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that a collection of objects does not contain any object equivalent to another object.
/// </summary>
/// <remarks>
/// Objects within the collection are equivalent to the expected object 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.
/// 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 <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, string because = "",
params object[] becauseArgs)
{
return NotContainEquivalentOf(unexpected, config => config, because, becauseArgs);
}

/// <summary>
/// Asserts that a collection of objects does not contain any object equivalent to another object.
/// </summary>
/// <remarks>
/// Objects within the collection are equivalent to the expected object 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.
/// 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 <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, Func<EquivalencyAssertionOptions<TExpectation>,
EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(config, nameof(config));

Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(Subject != null)
.FailWith("Expected {context:collection} not to contain equivalent of {0}{reason}, but collection is <null>.", unexpected);

EquivalencyAssertionOptions<TExpectation> options = config(AssertionOptions.CloneDefaults<TExpectation>());

var foundIndices = new List<int>();
using (var scope = new AssertionScope())
{
int index = 0;
foreach (object actualItem in Subject)
{
var context = new EquivalencyValidationContext
{
Subject = actualItem,
Expectation = unexpected,
CompileTimeType = typeof(TExpectation),
Because = because,
BecauseArgs = becauseArgs,
Tracer = options.TraceWriter,
};

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

string[] failures = scope.Discard();

if (!failures.Any())
{
foundIndices.Add(index);
}

index++;
}
}

if (foundIndices.Count > 0)
{
using (new AssertionScope())
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:collection} {0} not to contain equivalent of {1}{reason}, ", Subject, unexpected)
.AddReportable("configuration", options.ToString());

if (foundIndices.Count == 1)
{
Execute.Assertion
.FailWith("but found one at index {0}.", foundIndices[0]);
}
else
{
Execute.Assertion
.FailWith("but found several at indices {0}.", foundIndices);
}
}
}

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

/// <summary>
/// Asserts that the current collection only contains items that are assignable to the type <typeparamref name="T" />.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeSubsetOf(System.Collections.IEnumerable unexpectedSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContain(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainNulls(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotEqual(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotHaveSameCount(System.Collections.IEnumerable otherCollection, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeSubsetOf(System.Collections.IEnumerable unexpectedSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContain(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainNulls(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotEqual(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotHaveSameCount(System.Collections.IEnumerable otherCollection, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeSubsetOf(System.Collections.IEnumerable unexpectedSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContain(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainNulls(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotEqual(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotHaveSameCount(System.Collections.IEnumerable otherCollection, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeSubsetOf(System.Collections.IEnumerable unexpectedSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContain(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainNulls(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotEqual(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotHaveSameCount(System.Collections.IEnumerable otherCollection, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeSubsetOf(System.Collections.IEnumerable unexpectedSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContain(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainEquivalentOf<TExpectation>(TExpectation unexpected, System.Func<FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotContainNulls(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotEqual(System.Collections.IEnumerable unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotHaveSameCount(System.Collections.IEnumerable otherCollection, string because = "", params object[] becauseArgs) { }
Expand Down
135 changes: 135 additions & 0 deletions Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using FluentAssertions.Execution;
using Xunit;
using Xunit.Sdk;

Expand Down Expand Up @@ -1893,6 +1894,140 @@ public void When_collection_contains_object_equivalent_of_boxed_object_it_should

#endregion

#region Not Contain Equivalent Of

[Fact]
public void When_collection_contains_object_equal_to_another_it_should_throw()
{
// Arrange
var item = 1;
IEnumerable collection = new[] { 0, 1 };

// Act
Action act = () => collection.Should().NotContainEquivalentOf(item, "because we want to test the failure {0}", "message");

// Assert
act.Should().Throw<XunitException>().WithMessage("Expected collection {0, 1} not to contain*because we want to test the failure message, " +
"but found one at index 1.*With configuration*");
}

[Fact]
public void When_collection_contains_several_objects_equal_to_another_it_should_throw()
{
// Arrange
var item = 1;
IEnumerable collection = new[] { 0, 1, 1 };

// Act
Action act = () => collection.Should().NotContainEquivalentOf(item, "because we want to test the failure {0}", "message");

// Assert
act.Should().Throw<XunitException>().WithMessage("Expected collection {0, 1, 1} not to contain*because we want to test the failure message, " +
"but found several at indices {1, 2}.*With configuration*");
}

[Fact]
public void When_asserting_collection_to_not_to_contain_equivalent_but_collection_is_null_it_should_throw()
{
// Arrange
var item = 1;
IEnumerable collection = null;

// Act
Action act = () => collection.Should().NotContainEquivalentOf(item);

// Assert
act.Should().Throw<XunitException>().WithMessage("Expected collection*not to contain*but collection is <null>.");
}

[Fact]
public void When_injecting_a_null_config_to_NotContainEquivalentOf_it_should_throw()
{
// Arrange
IEnumerable collection = null;
object item = null;

// Act
Action act = () => collection.Should().NotContainEquivalentOf(item, config: null);

// Assert
act.Should().ThrowExactly<ArgumentNullException>()
.Which.ParamName.Should().Be("config");
}

[Fact]
public void When_asserting_empty_collection_to_not_contain_equivalent_it_should_succeed()
{
// Arrange
IEnumerable collection = new int[0];
int item = 4;

// Act / Assert
collection.Should().NotContainEquivalentOf(item);
}

[Fact]
public void When_collection_does_not_contain_object_equivalent_of_unexpected_it_should_succeed()
{
// Arrange
IEnumerable collection = new[] { 1, 2, 3 };
int item = 4;

// Act / Assert
collection.Should().NotContainEquivalentOf(item);
}

[Fact]
public void When_asserting_collection_to_not_contain_equivalent_it_should_respect_config()
{
// Arrange
IEnumerable collection = new[]
{
new Customer
{
Name = "John",
Age = 18
},
new Customer
{
Name = "Jane",
Age = 18
}
};
var item = new Customer { Name = "John", Age = 20 };

// Act
Action act = () => collection.Should().NotContainEquivalentOf(item, options => options.Excluding(x => x.Age));

// Assert
act.Should().Throw<XunitException>().WithMessage("*Exclude member root.Age*");
}

[Fact]
public void When_asserting_collection_to_not_contain_equivalent_it_should_allow_combining_inside_assertion_scope()
{
// Arrange
IEnumerable collection = new[] { 1, 2, 3 };
int another = 3;

// Act
Action act = () =>
{
using (new AssertionScope())
{
collection.Should().NotContainEquivalentOf(another, "because we want to test {0}", "first message")
.And
.HaveCount(4, "because we want to test {0}", "second message");
}
};

// Assert
act.Should().Throw<XunitException>().WithMessage("Expected collection*not to contain*first message*but*.\n" +
"Expected*4 item(s)*because*second message*but*.");
}

#endregion

#region Be Subset Of

[Fact]
Expand Down
9 changes: 8 additions & 1 deletion docs/_pages/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ collection.Should().NotContain(x => x > 10);

object boxedValue = 2;
collection.Should().ContainEquivalentOf(boxedValue); // Compared by object equivalence
object unexpectedBoxedValue = 82;
collection.Should().NotContainEquivalentOf(unexpectedBoxedValue); // Compared by object equivalence
const int successor = 5;
const int predecessor = 5;
Expand All @@ -87,7 +89,12 @@ IEnumerable<string> stringCollection = new[] { "build succeded", "test failed" }
stringCollection.Should().ContainMatch("* failed");
```

The `collection.Should().ContainEquivalentOf(boxedValue)` asserts that a collection contains at least one object that is equivalent to the expected object. The comparison is governed by the same rules and options as the [Object graph comparison](/objectgraphs).
In order to assert presence of an equivalent item in a collection applying [Object graph comparison](/objectgraphs) rules, use this:

```csharp
collection.Should().ContainEquivalentOf(boxedValue);
collection.Should().NotContainEquivalentOf(unexpectedBoxedValue)
```

Those last two methods can be used to assert a collection contains items in ascending or descending order.
For simple types that might be fine, but for more complex types, it requires you to implement `IComparable`, something that doesn't make a whole lot of sense in all cases.
Expand Down

0 comments on commit 0b37e37

Please sign in to comment.