2

In a MSTest unit test, I need to assert that a given collection contains exactly a single item which is equivalent with a given item, but fluent assertions seem only to support these methods:

items.Should().ContainEquivalentOf(item);
items.Should().ContainSingle(item);

I need something like a combination of both, something like "ContainSingleEquivalentOf(item)" (which does not seem to exist?).

The problem is that ContainSingle() seem to check equivalence only using the normal Equals() method which is not supported by the objects I need, so I need to use the reflection based equivalence check provided by fluent assertions. But I don't know how I can use it in this scenario.

EDIT: The collection is allowed to contain arbitrary other items which does not match my criteria.

codymanix
  • 28,510
  • 21
  • 92
  • 151
  • 1
    How about `items.Should().ContainSingle().And.ContainEquivalentOf(item);`? – DavidG Sep 29 '21 at 12:00
  • @DavidG: since ContainSingle(item) will always return false for me because of its usage of Equals() instead of equivalency this won't work. – codymanix Sep 29 '21 at 13:50
  • Note how I didn't give it an item in `ContainSingle`? That means it just checks the collection only has a single item, and the next step ensures it is has the correct properties. – DavidG Sep 29 '21 at 13:55
  • @DavidG: sorry, my bad, my description wasn't clear enough, I edited my post. – codymanix Sep 29 '21 at 13:56
  • Then you should do something like this https://stackoverflow.com/questions/37373645/fluentassertions-how-to-specify-that-collection-should-contain-a-certain-number – DavidG Sep 29 '21 at 14:00

2 Answers2

2

There's no built-in way to assert that a collection contains exactly one equivalent. But due to the extensibility of Fluent Assertions, we can built that.

Here's a prototype of a ContainSingleEquivalentOf. It has some limitations though. E.g. the Where(e => !ReferenceEquals(e, match.Which)) asserts that it will only exclude a single item.

public static class Extensions
{
    public static AndWhichConstraint<GenericCollectionAssertions<T>, T> ContainSingleEquivalentOf<T, TExpectation>(this GenericCollectionAssertions<T> parent,
        TExpectation expected, string because = "", params string[] becauseArgs) => parent.ContainSingleEquivalentOf(expected, config => config, because, becauseArgs);

    public static AndWhichConstraint<GenericCollectionAssertions<T>, T> ContainSingleEquivalentOf<T, TExpectation>(this GenericCollectionAssertions<T> parent,
        TExpectation expected, Func<EquivalencyAssertionOptions<TExpectation>, EquivalencyAssertionOptions<TExpectation>> config, string because = "", params string[] becauseArgs)
    {
        var match = parent.ContainEquivalentOf(expected, config, because, becauseArgs);
        var remainingItems = parent.Subject.Where(e => !ReferenceEquals(e, match.Which)).ToList();

        remainingItems.Should().NotContainEquivalentOf(expected, config, because, becauseArgs);

        return match;
    }
}

Note that Fluent Assertions will use Equals when overridden on the expectation to compare instances. To override that behavior you can use ComparingByMembers or provide an anonymous object as the expectation.

class MyClass
{
    public int MyProperty { get; set; }

    public override bool Equals(object obj) => false;
}

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void Force_comparing_by_members()
    {
        var subject = new MyClass[] { new() { MyProperty = 42 } };
        var expected = new MyClass { MyProperty = 42 };
        subject.Should().ContainSingleEquivalentOf(expected, opt => opt.ComparingByMembers<MyClass>());
    }

    [TestMethod]
    public void Use_anonymous_expectation_to_compare_by_members()
    {
        var subject = new MyClass[] { new() { MyProperty = 42 } };
        var expected = new { MyProperty = 42 };

        subject.Should().ContainSingleEquivalentOf(expected);
    }

    [TestMethod]
    public void Multiple_equivalent_items()
    {
        var subject = new MyClass[] { new() { MyProperty = 42 }, new() { MyProperty = 42 } };
        var expected = new { MyProperty = 42 };

        Action act = () => subject.Should().ContainSingleEquivalentOf(expected);

        act.Should().Throw<Exception>();
    }

    [TestMethod]
    public void No_equivalent_item()
    {
        var subject = new MyClass[] { new() { MyProperty = -1 } };
        var expected = new { MyProperty = 42 };

        Action act = () => subject.Should().ContainSingleEquivalentOf(expected);

        act.Should().Throw<Exception>();
    }

    [TestMethod]
    public void Fails_as_Fluent_Assertions_uses_overriden_Equals_method()
    {
        var subject = new MyClass[] { new() { MyProperty = 42 } };
        var expected = new MyClass { MyProperty = 42 };

        Action act = () => subject.Should().ContainSingleEquivalentOf(expected);

        act.Should().Throw<Exception>();
    }
}
Jonas Nyrup
  • 2,376
  • 18
  • 25
  • Sorry if I wasn't clear enought, but my collection is allowed to contain other items, just only one item that matches my criteria. – codymanix Sep 29 '21 at 13:54
1

At the time of writing, there is only one answer to this question. It starts with the statement

There's no built-in way to assert that a collection contains exactly one equivalent. But due to the extensibility of Fluent Assertions, we can built that.

I don't think it's true if you count combining two existing features into a one liner. Fluent Assertions provides a Which property on a number of its assertion overloads which are readable shorthand for the condition being asserted. Examples:

There are dozens of other examples. As per the third example above, you can chain Fluent Assertions statements to achieve the OP's desired result like so:

items.Should().ContainSingle().Which.Should().BeEquivalentTo(item);

They can also be assigned to a local variable to avoid excessive blocks of code like so:

var element = Some.Really.Really.Deep.Hierarchy.Of.Interest.Should().ContainSingle().Which;
element.Id.Should().Be(1);
element.Name.Should().Be("whatever");
...

Building your own extension methods works but seems like overkill to me

user1007074
  • 2,093
  • 1
  • 18
  • 22