Moq: Comment accéder à un paramètre passé à une méthode d'un service simulé

169

Imaginez cette classe

public class Foo {

    private Handler _h;

    public Foo(Handler h)
    {
        _h = h;
    }

    public void Bar(int i)
    {
        _h.AsyncHandle(CalcOn(i));
    }

    private SomeResponse CalcOn(int i)
    {
        ...;
    }
}

Mo (q) cking Handler dans un test de Foo, comment pourrais-je vérifier ce qui Bar()est passé _h.AsyncHandle?

Jan
la source
Vouliez-vous dire «AsyncHandle» (extra «n»)? Et pourriez-vous publier le code pour Handler, ou spécifier le nom de type complet s'il s'agit d'un type standard?
TrueWill
Pouvez-vous montrer votre test de squelette pour montrer ce que vous pensez? Bien que j'apprécie que de votre côté, c'est évident, de notre côté, cela ressemble à quelqu'un qui n'a pas pris le temps de répondre à la question sans faire une longue réponse spéculative.
Ruben Bartelink
1
Il n'y a ni Foo ni Bar () ni rien de ce genre. C'est juste un code de démonstration pour montrer la situation dans laquelle je me trouve sans me distraire des spécificités de l'application. Et j'ai juste la réponse, j'espérais avoir.
Jan

Réponses:

283

Vous pouvez utiliser la méthode Mock.Callback:

var mock = new Mock<Handler>();
SomeResponse result = null;
mock.Setup(h => h.AnsyncHandle(It.IsAny<SomeResponse>()))
    .Callback<SomeResponse>(r => result = r);

// do your test
new Foo(mock.Object).Bar(22);
Assert.NotNull(result);

Si vous voulez seulement vérifier quelque chose de simple sur l'argument passé, vous pouvez également le faire directement:

mock.Setup(h => h.AnsyncHandle(It.Is<SomeResponse>(response => response != null)));
Gamlor
la source
36
Remarque: si vous avez plusieurs arguments pour votre fonction, vous devez spécifier tous les types dans la Callback<>()méthode Moq générique . Par exemple, si votre méthode avait la définition Handler.AnsyncHandle(string, SomeResponse), vous en auriez besoin /* ... */.Callback<string, SomeResponse>(r => result = r);. Je n'ai pas trouvé cela explicitement indiqué dans de nombreux endroits, alors j'ai pensé l'ajouter ici.
Frank Bryce
12
Je voulais juste corriger @Frank au cas où vous n'auriez pas vu la réponse de @JavaJudt. La bonne façon d'obtenir deux arguments est:/* ... */.Callback<string, SomeResponse>((s1, s2) => { str1 = s1; result = s2});
renatogbp
2
Vous pouvez également obtenir la même chose en déclarant simplement les types d'arguments dans l'expression lambda, comme ceci:.Callback((string s1, SomeResponse s2) => /* stuff */ )
jb637
1
Pourriez-vous mettre à jour votre réponse pour inclure une référence à l' Capture.Inassistant intégré ?
cao
29

La réponse de Gamlor a fonctionné pour moi, mais j'ai pensé développer le commentaire de John Carpenter car je cherchais une solution impliquant plus d'un paramètre. J'ai pensé que d'autres personnes qui tombaient sur cette page pourraient être dans une situation similaire. J'ai trouvé cette information dans la documentation Moq .

Je vais utiliser l'exemple de Gamlor, mais faisons comme si la méthode AsyncHandle prend deux arguments: un stringet un SomeResponseobjet.

var mock = new Mock<Handler>();
string stringResult = string.Empty;
SomeResponse someResponse = null;
mock.Setup(h => h.AsyncHandle(It.IsAny<string>(), It.IsAny<SomeResponse>()))
    .Callback<string, SomeResponse>((s, r) => 
    {
        stringResult = s;
        someResponse = r;
    });

// do your test
new Foo(mock.Object).Bar(22);
Assert.AreEqual("expected string", stringResult);
Assert.IsNotNull(someResponse);

Fondamentalement, il vous suffit d'en ajouter un autre It.IsAny<>()avec le type approprié, d'ajouter un autre type à la Callbackméthode et de modifier l'expression lambda le cas échéant.

JavaJudt
la source
22

La méthode Callback fonctionnera certainement, mais si vous faites cela sur une méthode avec beaucoup de paramètres, cela peut être un peu verbeux. Voici quelque chose que j'ai utilisé pour retirer une partie du passe-partout.

var mock = new Mock<Handler>();

// do your test   
new Foo(mock.Object).Bar(22);

var arg = new ArgumentCaptor<SomeResponse>();
mock.Verify(h => h.AsyncHandle(arg.Capture()));
Assert.NotNull(arg.Value);

Voici la source d'ArgumentCaptor:

public class ArgumentCaptor<T>
{
    public T Capture()
    {
        return It.Is<T>(t => SaveValue(t));
    }

    private bool SaveValue(T t)
    {
        Value = t;
        return true;
    }

    public T Value { get; private set; }
}
Andrew Radford
la source
21

La réponse de Gamlor fonctionne, mais une autre façon de le faire (et que je considère plus expressive dans le test) est ...

var mock = new Mock<Handler>();
var desiredParam = 47; // this is what you want to be passed to AsyncHandle
new Foo(mock.Object).Bar(22);
mock.Verify(h => h.AsyncHandle(desiredParam), Times.Once());

Vérifier est très puissant et vaut la peine de prendre le temps de s'y habituer.

Pete Martin
la source
15
Cette approche convient si vous souhaitez simplement vérifier si une méthode a été appelée avec un paramètre connu. Dans le cas où le paramètre n'est pas encore créé au moment de l'écriture du test (par exemple, l'unité en question crée le paramètre en interne), alors Callback vous permet de capturer et d'interroger cela, contrairement à votre approche.
Michael
1
J'ai besoin de stocker la valeur passée car je dois vérifier que l'ensemble d'un ensemble d'objets a été transmis.
MrFox du
De plus, parfois, le paramètre est une entité et vous souhaitez des assertions distinctes pour chaque champ de l'entité. La meilleure façon de faire est de capturer le paramètre, plutôt que d'utiliser Verify et un matcher.
Kevin Wong le
12

L'alternative consiste également à utiliser la Capture.Infonctionnalité du moq. C'est la moqfonction OOTB qui permet la capture d'arguments dans la collection.

//Arrange
var args = new List<SomeResponse>();
mock.Setup(h => h.AnsyncHandle(Capture.In(args)));

//Act
new Foo(mock.Object).Bar(22);

//Assert
//... assert args.Single() or args.First()
Johnny
la source
1
Très bonne réponse! Je n'étais pas au courant des classes Moq.Capture et Moq.CaptureMatch avant de voir cette réponse. Capture est une meilleure alternative à l' CallbackOMI. Étant donné que vous utilisez Capture directement dans la liste de paramètres, il est beaucoup moins sujet aux problèmes lors de la refactorisation de la liste de paramètres d'une méthode et rend donc les tests moins fragiles. Avec Callback, vous devez garder les paramètres passés dans Setup en synchronisation avec les paramètres de type utilisés pour Callback, et cela m'a certainement causé des problèmes dans le passé.
Justin Holzer
7

Vous pourriez utiliser It.Is<TValue>() matcher.

var mock = new Mock<Handler>();
new Foo(mock.Object).Bar(22);
mock.Verify(h => h.AsyncHandle(It.Is<SomeResponse>(r => r != null )));
Ed Yablonsky
la source
2

Cela fonctionne également:

Mock<InterfaceThing> mockedObject = new Mock<InterfaceThing>();
var objectParameter = mockedObject.Invocations[1].Arguments[0] as ObjectParameter;
Jeff Smith
la source
2

Beaucoup de bonnes réponses ici! Optez pour l'ensemble de fonctionnalités Moq prêt à l'emploi jusqu'à ce que vous ayez besoin de faire des affirmations sur plusieurs paramètres de classe passés à vos dépendances. Si vous vous retrouvez dans cette situation, la fonction de vérification Moq avec les correspondants It.Is ne fait pas un bon travail pour isoler l'échec du test, et la méthode de retour / rappel des arguments ajoute des lignes de code inutiles à votre test (et les longs tests sont interdits pour moi).

Voici l'essentiel: https://gist.github.com/Jacob-McKay/8b8d41ebb9565f5fca23654fd944ac6b avec une extension Moq (4.12) que j'ai écrite qui donne une manière plus déclarative de faire des affirmations sur les arguments passés aux mocks, sans les inconvénients mentionnés ci-dessus. Voici à quoi ressemble la section Vérifier maintenant:

        mockDependency
            .CheckMethodWasCalledOnce(nameof(IExampleDependency.PersistThings))
            .WithArg<InThing2>(inThing2 =>
            {
                Assert.Equal("Input Data with Important additional data", inThing2.Prop1);
                Assert.Equal("I need a trim", inThing2.Prop2);
            })
            .AndArg<InThing3>(inThing3 =>
            {
                Assert.Equal("Important Default Value", inThing3.Prop1);
                Assert.Equal("I NEED TO BE UPPER CASED", inThing3.Prop2);
            });

Je serais ravi si Moq fournissait une fonctionnalité qui accomplissait la même chose tout en étant aussi déclarative et en fournissant l'isolement d'échec que cela permet. Doigts croisés!

Jacob McKay
la source
1
J'aime ça. La vérification de Moq est en concurrence avec l'assert de xUnit pour l'autorité d'effectuer des assertions. Cela ne semble pas juste sur la partie Moq de la configuration. La configuration de la fonctionnalité It.Is doit également prendre en charge les expressions non.
Thomas
"Moq est en concurrence avec l'affirmation de xUnit pour l'autorité d'effectuer des assertions" - Bien dit @Thomas. J'ajouterais qu'il perdrait la concurrence. Moq est bon pour vous faire savoir s'il y a eu un appel correspondant, mais des affirmations spécifiques vous donnent de bien meilleures informations. Le principal inconvénient de mon approche est de perdre la sécurité de type et la vérification de l'ordre des paramètres. Je cherchais une amélioration à ce sujet depuis un moment, j'espère qu'il y a un ninja C # qui peut pirater un exemple ensemble! Sinon, si je trouve un moyen, je mettrai à jour cela.
Jacob McKay