wake-up-neo.net

Verspottung generischer Methoden in Moq ohne Angabe von T

Ich habe eine Schnittstelle mit einer Methode wie folgt: 

public interface IRepo
{
    IA<T> Reserve<T>();
}

Ich möchte die Klasse, die diese Methode enthält, verspotten, ohne für jeden Typ, für den sie verwendet werden kann, Setup-Methoden angeben zu müssen. Im Idealfall möchte ich einfach einen new mock<T>.Object zurückgeben.

Wie kann ich das erreichen?

Es scheint, dass meine Erklärung unklar war. Hier ein Beispiel - das ist jetzt möglich, wenn ich das T (hier String) vorgeben:

[TestMethod]
public void ExampleTest()
{
    var mock = new Mock<IRepo>();
    mock.Setup(pa => pa.Reserve<string>()).Returns(new Mock<IA<string>>().Object);
}

Was ich erreichen möchte, ist etwa so:

[TestMethod]
public void ExampleTest()
{
    var mock = new Mock<IRepo>();
    mock.Setup(pa => pa.Reserve<T>()).Returns(new Mock<IA<T>>().Object);
    // of course T doesn't exist here. But I would like to specify all types
    // without having to repeat the .Setup(...) line for each of them.
}

Einige Methoden des zu testenden Objekts können für drei oder vier verschiedene Typen reserviert sein. Wenn ich alle Typen einrichten muss, muss ich für jeden Test viel Setup-Code schreiben. Aber in einem einzigen Test befasse ich mich nicht mit allen, ich brauche einfach Objekte, die nicht null sind, außer denjenigen, die ich gerade teste (und für die ich gerne ein komplexeres Setup schreibe).

37
Wilbert

Wenn ich nicht missverstehe, was Sie brauchen, könnten Sie eine Methode wie diese bauen:

private Mock<IRepo> MockObject<T>()
{
    var mock = new Mock<IRepo>();
    return mock.Setup(pa => pa.Reserve<T>())
        .Returns(new Mock<IA<T>>().Object).Object;
}
18
Mike Perrenoud

Einfach das tun:

[TestMethod]
public void ExampleTest()
{
  var mock = new Mock<IRepo> { DefaultValue = DefaultValue.Mock, };
  // no setups needed!

  ...
}

Da sich Ihr Modell nicht Strict verhält, ist es mit Anrufen zufrieden, die Sie noch nicht einmal eingerichtet haben. In diesem Fall wird einfach ein "Standardwert" zurückgegeben. Dann

DefaultValue.Mock

stellt sicher, dass es sich bei diesem "Standard" um einen neuen Mock<> des entsprechenden Typs handelt und nicht nur eine Nullreferenz.

Die Einschränkung hier ist, dass Sie die zurückgegebenen einzelnen "Sub-Mocks" nicht steuern können (z. B. spezielle Einstellungen vornehmen können).

17

Ich habe eine Alternative gefunden, die meiner Meinung nach näher kommt, was Sie wollen. Jedenfalls war es nützlich für mich, also hier. Die Idee ist, eine Zwischenklasse zu erstellen, die fast rein abstrakt ist und Ihre Schnittstelle implementiert. Der nicht abstrakte Teil ist der Teil, mit dem Moq nicht umgehen kann. Z.B.

public abstract class RepoFake : IRepo
{
    public IA<T> Reserve<T>()
    {
        return (IA<T>)ReserveProxy(typeof(T));
    }

    // This will be mocked, you can call Setup with it
    public abstract object ReserveProxy(Type t);

    // TODO: add abstract implementations of any other interface members so they can be mocked
}

Jetzt können Sie RepoFake anstelle von IRepo nachahmen. Alles funktioniert gleich, außer dass Sie Ihre Setups auf ReserveProxy statt auf Reserve schreiben. Sie können den Rückruf bearbeiten, wenn Sie Assertions basierend auf dem Typ durchführen möchten, obwohl der Parameter Type für ReserveProxy vollständig optional ist.

5
OlduwanSteve

Hier ist eine Möglichkeit, die zu funktionieren scheint. Wenn alle Klassen, die Sie in IRepo verwenden, von einer einzigen Basisklasse erben, können Sie dies wie gehabt verwenden und müssen es niemals aktualisieren.

public Mock<IRepo> SetupGenericReserve<TBase>() where TBase : class
{
    var mock = new Mock<IRepo>();
    var types = GetDerivedTypes<TBase>();
    var setupMethod = this.GetType().GetMethod("Setup");

    foreach (var type in types)
    {
        var genericMethod = setupMethod.MakeGenericMethod(type)
            .Invoke(null,new[] { mock });
    }

    return mock;
}

public void Setup<TDerived>(Mock<IRepo> mock) where TDerived : class
{
    // Make this return whatever you want. Can also return another mock
    mock.Setup(x => x.Reserve<TDerived>())
        .Returns(new IA<TDerived>());
}

public IEnumerable<Type> GetDerivedTypes<T>() where T : class
{
    var types = new List<Type>();
    var myType = typeof(T);

    var assemblyTypes = myType.GetTypeInfo().Assembly.GetTypes();

    var applicableTypes = assemblyTypes
        .Where(x => x.GetTypeInfo().IsClass 
                && !x.GetTypeInfo().IsAbstract 
                 && x.GetTypeInfo().IsSubclassOf(myType));

    foreach (var type in applicableTypes)
    {
        types.Add(type);
    }

    return types;
}

Wenn Sie über keine Basisklasse verfügen, können Sie andernfalls SetupGenericReserve so ändern, dass der Parameter TBase type nicht verwendet wird. Stattdessen erstellen Sie einfach eine Liste aller Typen, die Sie einrichten möchten.

public IEnumerable<Type> Alternate()
{
    return new [] 
    {
        MyClassA.GetType(),
        MyClassB.GetType()
    }
}

Hinweis: Dies ist für ASP.NET Core geschrieben, sollte aber in anderen Versionen außer der GetDerivedTypes-Methode funktionieren.

0
Sam