Search code examples
c#unit-testingassertionfluent-assertions

Fluent Assertions: How to assert "single equivalent item in collection"?


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.


Solution

  • 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 build 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>();
        }
    }