Search code examples
c#dictionaryfluent-assertions

How to use Excluding in FluentAssertions to exclude specific KeyValue pair in Dictionary


I'm using FluentAssertions with ShouldBeEquivalentTo to compare two dictionaries of type Dictionary<string, string> but want to exclude one or more specific KeyValue pairs (because they contain timestamps in this case). How to do this?

I tried things like: opt => opt.Excluding(x => x.Single(kv => kv.Key == "MySearchKey")) but this results in errors like: Message: System.ArgumentException : Expression <Convert(x.Single(kv => (kv.Key == "MySearchKey")))> cannot be used to select a member.

Is what I want possible? Or should I maybe exclude the value only and not the pair (that's maybe even better because the existence of the key will be checked then)? Thanks!


Solution

  • Excluding() is meant to exclude members of a type, not excluding a member of a collection, see documentation for more info.

    Note: the code below is for the current stable version 4.19.4 of Fluent Assertions.

    Example: You want to compare instances of Person and PersonDTO, but Person contains the AnotherProperty which you want to exclude from the object comparison.

    var person = new Person
    {
        FirstName = "John",
        LastName = "McClane",
        AnotherProperty = 42
    };
    
    var personDTO = new PersonDTO
    {
        FirstName = "John",
        LastName = "McClane"
    };
    

    This is where you would use Exclude to exclude a member of a type.

    person.ShouldBeEquivalentTo(personDTO, options => options.Excluding(e => e.AnotherProperty));
    

    In your concrete case I would not use ShouldBeEquivalentTo. Consider these two dictionary instances, where you want to omit a member of a collection, here the member with Key == "unknown".

    var actual = new Dictionary<string, int>
    {
        ["one"] = 1,
        ["two"] = 2,
        ["three"] = 3,
        ["unknown"] = -1,
        ["fail"] = -2
    };
    
    var expected = new Dictionary<string, int>
    {
        ["one"] = 1,
        ["two"] = 2,
        ["three"] = 3
    };
    

    You could either just filter out the unwanted key-value pairs:

    IEnumerable<KeyValuePair<string, int>> filtered = actual.Where(e => e.Key != "unknown");
    

    Now the assertion will be between two IEnumerable<KeyValuePair<string, int>>s

    filtered.Should().Equal(expected);
    

    which will give the following assertion failure message:

    FluentAssertions.Execution.AssertionFailedException: 'Expected collection to be equal to {[one, 1], [two, 2], [three, 3]}, but {[one, 1], [two, 2], [three, 3], [fail, -2]} contains 1 item(s) too many.'
    

    Otherwise turn the filtered enumerable back into a dictionary:

    Dictionary<string, int> filteredDict = actual.Where(e => e.Key != "unknown")
        .ToDictionary(e => e.Key, e => e.Value);
    

    You will now be comparing Dictionary<string, int>s again:

    filteredDict.Should().Equal(expected);
    

    which will give the following assertion failure message:

    FluentAssertions.Execution.AssertionFailedException: 'Expected dictionary to be equal to {[one, 1], [two, 2], [three, 3]}, but found additional keys {"fail"}.'
    

    If want to use the second approach and you do this often, you could create extension methods to extract the logic of removing a member from the test method.

    public static class DictionaryExtensions
    {
        public static IDictionary<TKey, TValue> ExceptKeys<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, params TKey[] keys)
        {
            if (dictionary == null) throw new ArgumentNullException(nameof(dictionary));
            if (keys == null) throw new ArgumentNullException(nameof(keys));
    
            return dictionary.Where(e => !keys.Contains(e.Key)).ToDictionary(e => e.Key, e => e.Value);
        }
    
        public static IDictionary<TKey, TValue> ExceptValues<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, params TValue[] values)
        {
            if (dictionary == null) throw new ArgumentNullException(nameof(dictionary));
            if (values == null) throw new ArgumentNullException(nameof(values));
    
            return dictionary.Where(e => !values.Contains(e.Value)).ToDictionary(e => e.Key, e => e.Value);
        }
    }
    

    You can now write an in my opinion more clear and concise test:

    actual.ExceptKeys("unknown").Should().Equal(expected);