Comment utiliser Moq pour simuler une méthode d'extension?

87

J'écris un test qui dépend des résultats d'une méthode d'extension, mais je ne veux pas qu'un échec futur de cette méthode d'extension brise jamais ce test. Se moquer de ce résultat semblait le choix évident, mais Moq ne semble pas offrir un moyen de remplacer une méthode statique (une exigence pour une méthode d'extension). Il y a une idée similaire avec Moq.Protected et Moq.Stub, mais ils ne semblent rien offrir pour ce scénario. Est-ce que je manque quelque chose ou devrais-je procéder d'une manière différente?

Voici un exemple trivial qui échoue avec l'habituelle "Attente non valide sur un membre non remplaçable" . C'est un mauvais exemple de besoin de se moquer d'une méthode d'extension, mais cela devrait le faire.

public class SomeType {
    int Id { get; set; }
}

var ListMock = new Mock<List<SomeType>>();
ListMock.Expect(l => l.FirstOrDefault(st => st.Id == 5))
        .Returns(new SomeType { Id = 5 });

Comme pour tous les accros de TypeMock qui pourraient suggérer que j'utilise Isolator à la place: j'apprécie l'effort car il semble que TypeMock pourrait faire le travail les yeux bandés et en état d'ébriété, mais notre budget n'augmente pas de si tôt.

Patridge
la source
3
Un duplicata peut être trouvé ici: stackoverflow.com/questions/2295960/… .
Oliver
6
Cette question a une année entière de plus que celle-là. S'il y a duplication, ça va dans l'autre sens.
patridge
2
Toujours pas de solution appropriée en 2019!
TanvirArjel
1
@TanvirArjel En fait, depuis de nombreuses années, vous pouvez utiliser JustMock pour simuler des méthodes d'extension. Et c'est aussi simple que de se moquer de toute autre méthode. Voici un lien vers la documentation: Extension Methods Mocking
Mihail Vladov

Réponses:

69

Les méthodes d'extension ne sont que des méthodes statiques déguisées. Les frameworks moqueurs tels que Moq ou Rhinomocks ne peuvent créer que des instances factices d'objets, cela signifie qu'il n'est pas possible de se moquer des méthodes statiques.

Mendelt
la source
68
@ Mendelt..Ensuite, comment une unité testerait-elle une méthode qui a en interne une méthode d'extension? quelles sont les alternatives possibles?
Sai Avinash
@Mendelt, comment une unité testerait-elle une méthode qui a en interne une méthode d'extension? quelles sont les alternatives possibles?
Alexander
@Alexander J'ai eu la même question et puis ça y a répondu de manière fantastique: agooddayforscience.blogspot.com/2017/08/… -Je vois ça, c'est une bouée de sauvetage !
LTV
30

Si vous pouvez modifier le code des méthodes d'extension, vous pouvez le coder comme ceci pour pouvoir tester:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

public static class MyExtensions
{
    public static IMyImplementation Implementation = new MyImplementation();

    public static string MyMethod(this object obj)
    {
        return Implementation.MyMethod(obj);
    }
}

public interface IMyImplementation
{
    string MyMethod(object obj);
}

public class MyImplementation : IMyImplementation
{
    public string MyMethod(object obj)
    {
        return "Hello World!";
    }
}

Les méthodes d'extension ne sont donc qu'un wrapper autour de l'interface d'implémentation.

(Vous pouvez utiliser uniquement la classe d'implémentation sans méthodes d'extension qui sont en quelque sorte du sucre syntaxique.)

Et vous pouvez vous moquer de l'interface d'implémentation et la définir comme implémentation pour la classe d'extensions.

public class MyClassUsingExtensions
{
    public string ReturnStringForObject(object obj)
    {
        return obj.MyMethod();
    }
}

[TestClass]
public class MyTests
{
    [TestMethod]
    public void MyTest()
    {
        // Given:
        //-------
        var mockMyImplementation = new Mock<IMyImplementation>();

        MyExtensions.Implementation = mockMyImplementation.Object;

        var myClassUsingExtensions = new MyClassUsingExtensions();

        // When:
        //-------
        var myObject = new Object();
        myClassUsingExtensions.ReturnStringForObject(myObject);

        //Then:
        //-------
        // This would fail because you cannot test for the extension method
        //mockMyImplementation.Verify(m => m.MyMethod());

        // This is success because you test for the mocked implementation interface
        mockMyImplementation.Verify(m => m.MyMethod(myObject));
    }
}
informatorius
la source
Merci beaucoup pour cela. Cela a été extrêmement utile et m'a permis de surmonter certains obstacles lors des tests.
DerHaifisch
C'est un commentaire sous-estimé.
Raj
15

J'ai créé une classe wrapper pour les méthodes d'extension dont je devais me moquer.

public static class MyExtensions
{
    public static string MyExtension<T>(this T obj)
    {
        return "Hello World!";
    }
}

public interface IExtensionMethodsWrapper
{
    string MyExtension<T>(T myObj);
}

public class ExtensionMethodsWrapper : IExtensionMethodsWrapper
{
    public string MyExtension<T>(T myObj)
    {
        return myObj.MyExtension();
    }
}

Ensuite, vous pouvez vous moquer des méthodes wrapper dans vos tests et code avec votre conteneur IOC.

ranthonissen
la source
Il s'agit d'une solution de contournement où vous ne pouvez plus utiliser la syntaxe des méthodes d'extension dans votre code. Mais cela aide lorsque vous ne pouvez pas modifier la classe des méthodes d'extension.
informatorius
@informatorius Que voulez-vous dire par là? Dans votre code, vous utilisez MyExtension () de la classe MyExtensions. Dans vos tests, vous utilisez MyExtension () de la classe ExtensionMethodsWrapper () fournissant le paramètre.
ranthonissen
Si ma classe sous test utilise MyExtension () de MyExtensions, je ne peux pas me moquer des MyExtensions. La classe testée doit donc utiliser IExtensionMethodsWrapper pour pouvoir être moquée. Mais alors la classe testée ne peut plus utiliser la syntaxe de la méthode d'extension.
informatorius
1
C'est tout l'intérêt du PO. C'est une solution de contournement pour cela.
ranthonissen
4

Pour les méthodes d'extension, j'utilise normalement l'approche suivante:

public static class MyExtensions
{
    public static Func<int,int, int> _doSumm = (x, y) => x + y;

    public static int Summ(this int x, int y)
    {
        return _doSumm(x, y);
    }
}

Cela permet d'injecter _doSumm assez facilement.

Dmigo
la source
2
J'ai juste essayé cela, mais il y a des problèmes lors de la transmission de l'objet simulé Parent auquel l'extension est destinée lors de sa transmission dans une autre méthode qui se trouve dans un autre assemblage. Dans ce cas, même avec cette technique, l'extension d'origine est sélectionnée lorsque la méthode est appelée dans l'autre assembly, en quelque sorte un problème de portée. Si j'appelle directement dans le projet de test unitaire, c'est bien, mais lorsque vous testez un autre code qui appelle la méthode d'extension, cela n'aide tout simplement pas.
Stephen York
@Stephen Je ne sais pas comment éviter cela. Je n'ai jamais eu ce genre de problème de portée
dmigo
0

La meilleure chose que vous puissiez faire est de fournir une implémentation personnalisée pour le type qui a la méthode d'extension, par exemple:

[Fact]
public class Tests
{
    public void ShouldRunOk()
    {
        var service = new MyService(new FakeWebHostEnvironment());

        // Service.DoStuff() internally calls the SomeExtensionFunction() on IWebHostEnvironment
        // Here it works just fine as we provide a custom implementation of that interface
        service.DoStuff().Should().NotBeNull();
    }
}

public class FakeWebHostEnvironment : IWebHostEnvironment
{
    /* IWebHostEnvironment implementation */

    public bool SomeExtensionFunction()
    {
        return false;
    }
}
Ross
la source