Skip to content

Commit

Permalink
Add "not contain equivalent of" assertion
Browse files Browse the repository at this point in the history
  • Loading branch information
ishimko committed Apr 26, 2020
1 parent c34bb3e commit 0f793ee
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 1 deletion.
112 changes: 112 additions & 0 deletions Src/FluentAssertions/Collections/CollectionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,118 @@ 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)
.WithExpectation("Expected {context:collection} not to contain equivalent of {0}{reason}, ", unexpected)
.ForCondition(Subject != null)
.FailWith("but collection is <null>.");

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
111 changes: 111 additions & 0 deletions Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1893,6 +1893,117 @@ 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*");
}

#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
1 change: 1 addition & 0 deletions docs/_pages/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ sidebar:
* Make `DefaultValueFormatter` and `EnumerableValueFormatter` suitable for inheritance - [#1295](https://github.com/fluentassertions/fluentassertions/pull/1295).
* Added support for dictionary assertions on `IReadOnlyDictionary<TKey, TValue>` - [#1298](https://github.com/fluentassertions/fluentassertions/pull/1298).
* `GenericAsyncFunctionAssertions` now has `AndWhichConstraint` overloads for `NotThrow[Async]` and `NotThrowAfter[Async]` - [#1289](https://github.com/fluentassertions/fluentassertions/pull/1289).
* Added `collection.Should().NotContainEquivalentTo` to use object graph comparison rules to assert absence of an element in the collection - [#1318](https://github.com/fluentassertions/fluentassertions/pull/1318).

**Fixes**
* Reported actual value when it contained `{{{{` or `}}}}` - [#1234](https://github.com/fluentassertions/fluentassertions/pull/1234).
Expand Down

0 comments on commit 0f793ee

Please sign in to comment.