Skip to content

Add Support for Indexers With init #197

@JasonBock

Description

@JasonBock

Originally, I thought I couldn't do this. But....I just realized it's possible. Here's a (long) snippet of code that demonstrates how this will work - I'll describe the details after:

using System;
using System.Collections;
using System.Collections.Generic;

var guid = Guid.NewGuid();

var properties = new ConstructorProperties
{
    ["1", 2, guid] = 22,
    [2, "b"] = "33",
};

var mock = new Mock(properties);
Console.WriteLine(mock["1", 2, guid]); // prints 22
Console.WriteLine(mock[2, "b"]); // prints 33

public sealed class ConstructorProperties
    : IEnumerable<(string, int, Guid)>, IEnumerable<(int, string)>
{
    private readonly Dictionary<(string, int, Guid), int> i0 = new();
    private readonly Dictionary<(int, string), string> i1 = new();
    
    IEnumerator<(string, int, Guid)> IEnumerable<(string, int, Guid)>.GetEnumerator()
    {
        foreach(var key in this.i0.Keys)
        {
            yield return key;
        }
    }

    IEnumerator<(int, string)> IEnumerable<(int, string)>.GetEnumerator()
    {
        foreach(var key in this.i1.Keys)
        {
            yield return key;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();

    public int this[string a, int b, Guid c]
    {
        get => this.i0[(a, b, c)];
        init => this.i0[(a, b, c)] = value;
    }

    public string this[int a, string b]
    {
        get => this.i1[(a, b)];
        init => this.i1[(a, b)] = value;
    }
}

public sealed class Mock
{
    private readonly Dictionary<(string, int, Guid), int> i0 = new();
    private readonly Dictionary<(int, string), string> i1 = new();

    public Mock(ConstructorProperties constructorProperties)
    {
        foreach((string a, int b, Guid c) in (IEnumerable<(string, int, Guid)>)constructorProperties)
        {
            this[a, b, c] = constructorProperties[a, b, c];
        }
        
        foreach((int a, string b) in (IEnumerable<(int, string)>)constructorProperties)
        {
            this[a, b] = constructorProperties[a, b];
        }
    }
    
    public int this[string a, int b, Guid c]
    {
        get => this.i0[(a, b, c)];
        init => this.i0[(a, b, c)] = value;
    }

    public string this[int a, string b]
    {
        get => this.i1[(a, b)];
        init => this.i1[(a, b)] = value;
    }
}

Basically, I look at the target mock type, and for all of the indexers that have an init (or are required), create a Dictionary<> as a readonly field, and use that to back the indexer. We don't care how this is handled in the mock, we just capture the index values and the related value. Then we turn around and set those in the mock. We need to explicitly implement these methods because we may have more than one.

Note that if the indexer has just one index value, we won't use a tuple, just that type.

Tasks:

  • Ensure that all properties and indexers on ConstructorProperties have a get, even if the target property/indexer doesn't.
  • Pass ConstructorProperties to the mock as the 2nd argument after handlers and before any of the mock's constructor arguments, similar to what is done in .Instance(), and set the properties in the constructor. Remove the object initialization code in .Instance()
  • For each indexer with an init
    • Add a Dictionary<> to ConstructorProperties
    • Add IEnumerable<> to ConstructorProperties
    • Implement IEnumerable<> explicitly
    • Implement the indexer to use that dictionary.
    • In the mock's constructor, foreach over all the keys (casting ConstructorProperties to that specific IEnumerable<> type) and set the indexer to the values from the indexer in ConstructorProperties
  • Add an explicit implementation of the non-generic GetEnumerator() that throws an exception, maybe the new UnreachableException (it should never be called)
  • I think I should allow init properties and indexers to be mocked. Currently they do nothing, but it's possible a constructor could call them, so...it shouldn't be hard to do this, as I already allow a set to be mocked. I'd just have to include init for that as well.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions