web-dev-qa-db-fra.com

Se moquer des méthodes génériques dans Moq sans spécifier T

J'ai une interface avec une méthode comme suit: 

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

Je voudrais me moquer de la classe qui contient cette méthode sans avoir à spécifier des méthodes de configuration pour chaque type pour lequel elle pourrait être utilisée. Idéalement, j'aimerais simplement qu'il renvoie un new mock<T>.Object.

Comment est-ce que je réalise ceci?

Il semble que mon explication n'était pas claire. Voici un exemple - c'est possible maintenant, quand je spécifie le T (ici, chaîne):

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

Ce que j'aimerais réaliser est quelque chose comme ceci:

[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.
}

Certaines méthodes de l'objet à tester peuvent appeler une réserve pour trois ou quatre types différents. Si je dois configurer tous les types, je dois écrire beaucoup de code de configuration pour chaque test. Mais dans un seul test, je ne m'occupe pas de tous, j'ai simplement besoin d'objets fictifs non nuls, à l'exception de celui que je teste actuellement (et pour lequel j'écris volontiers une configuration plus complexe).

37
Wilbert

Sauf si je comprends mal ce dont vous avez besoin, vous pouvez créer une méthode comme celle-ci:

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

Faites simplement ceci:

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

  ...
}

Étant donné que votre maquette n'a pas de comportement Strict, elle sera satisfaite des appels que vous n'avez même pas configurés. Dans ce cas, un "défaut" est simplement renvoyé. ensuite

DefaultValue.Mock

garantit que cette "valeur par défaut" est un nouveau Mock<> de type approprié, au lieu d'une simple référence null.

La limitation ici est que vous ne pouvez pas contrôler (par exemple, effectuer des configurations spéciales) les "sous-mocks" individuels qui sont renvoyés.

17
Jeppe Stig Nielsen

J'ai trouvé une alternative qui, je pense, se rapproche de ce que vous voulez. Quoi qu'il en soit, cela m'a été utile, alors voilà. L'idée est de créer une classe intermédiaire presque purement abstraite et qui implémente votre interface. La partie non abstraite est la partie que Moq ne peut pas gérer. Par exemple.

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
}

Vous pouvez maintenant vous moquer de RepoFake au lieu de IRepo. Tout fonctionne de la même manière sauf que vous écrivez vos configurations sur ReserveProxy au lieu de Reserve. Vous pouvez gérer le rappel si vous souhaitez effectuer des assertions en fonction du type, bien que le paramètre Type à ReserveProxy soit totalement facultatif.

5
OlduwanSteve

Voici une façon de le faire qui semble fonctionner. Si toutes les classes que vous utilisez dans IRepo héritent d'une seule classe de base, vous pouvez utiliser ceci tel quel et ne jamais avoir à le mettre à jour.

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;
}

Sinon, si vous n'avez pas de classe de base, vous pouvez modifier SetupGenericReserve afin de ne pas utiliser le paramètre de type TBase et créer simplement une liste de tous les types que vous souhaitez configurer, comme suit:

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

Remarque: Ceci est écrit pour ASP.NET Core, mais devrait fonctionner dans d'autres versions, à l'exception de la méthode GetDerivedTypes.

0
Sam