Moq: So gelangen Sie zu einem Parameter, der an eine Methode eines verspotteten Dienstes übergeben wird

168

Stellen Sie sich diese Klasse vor

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 in einem Test von Foo, wie könnte ich überprüfen, was Bar()passiert ist _h.AsyncHandle?

Jan.
quelle
Meinten Sie "AsyncHandle" (extra "n")? Und könnten Sie den Code für Handler veröffentlichen oder den vollständig qualifizierten Typnamen angeben, wenn es sich um einen Standardtyp handelt?
TrueWill
Können Sie Ihren Skeletttest zeigen, um zu zeigen, was Sie denken? Obwohl ich es zu schätzen weiß, dass es von Ihrer Seite offensichtlich ist, sieht es von unserer Seite aus wie jemand, der sich nicht die Zeit genommen hat, die Frage ohne eine lange spekulative Antwort beantwortbar zu machen.
Ruben Bartelink
1
Es gibt weder ein Foo noch eine Bar () oder so etwas. Es ist nur ein Demo-Code, der die Situation zeigt, in der ich mich befinde, ohne von den Anwendungsspezifikationen abgelenkt zu werden. Und ich bekam genau die Antwort, auf die ich gehofft hatte.
Januar

Antworten:

282

Sie können die Mock.Callback-Methode verwenden:

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

Wenn Sie nur etwas Einfaches an dem übergebenen Argument überprüfen möchten, können Sie dies auch direkt tun:

mock.Setup(h => h.AnsyncHandle(It.Is<SomeResponse>(response => response != null)));
Gamlor
quelle
36
Eine Randnotiz: Wenn Sie mehrere Argumente für Ihre Funktion haben, müssen Sie alle Typen in der generischen Callback<>()Moq-Methode angeben . Wenn Ihre Methode beispielsweise die Definition hätte Handler.AnsyncHandle(string, SomeResponse), würden Sie benötigen /* ... */.Callback<string, SomeResponse>(r => result = r);. Ich habe dies an vielen Stellen nicht explizit angegeben, daher dachte ich, ich würde es hier hinzufügen.
Frank Bryce
12
Ich wollte nur @Frank korrigieren, falls Sie die Antwort von @JavaJudt nicht gesehen haben. Der richtige Weg, um zwei Argumente zu erhalten, ist:/* ... */.Callback<string, SomeResponse>((s1, s2) => { str1 = s1; result = s2});
Renatogbp
2
Sie können dasselbe auch erreichen, indem Sie einfach die Arten der Argumente im Lambda-Ausdruck deklarieren, wie .Callback((string s1, SomeResponse s2) => /* stuff */ )
folgt
1
Könnten Sie bitte Ihre Antwort aktualisieren, um einen Verweis auf den eingebauten Capture.InHelfer aufzunehmen?
Cao
29

Gamlors Antwort funktionierte für mich, aber ich dachte, ich würde John Carpenters Kommentar erweitern, weil ich nach einer Lösung suchte, die mehr als einen Parameter umfasste. Ich nahm an, dass andere Leute, die auf diese Seite stolpern, sich in einer ähnlichen Situation befinden könnten. Ich habe diese Informationen in der Moq-Dokumentation gefunden .

Ich werde Gamlors Beispiel verwenden, aber tun wir so, als ob die AsyncHandle-Methode zwei Argumente akzeptiert: a stringund ein SomeResponseObjekt.

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

Grundsätzlich müssen Sie nur It.IsAny<>()einen weiteren Typ mit dem entsprechenden Typ hinzufügen, der CallbackMethode einen weiteren Typ hinzufügen und den Lambda-Ausdruck entsprechend ändern.

JavaJudt
quelle
22

Die Callback-Methode wird sicherlich funktionieren, aber wenn Sie dies mit einer Methode mit vielen Parametern tun, kann sie etwas ausführlich sein. Hier ist etwas, mit dem ich einen Teil der Kesselplatte entfernt habe.

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

Hier ist die Quelle für 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
quelle
21

Gamlors Antwort funktioniert, aber eine andere Möglichkeit (und eine, die ich im Test als aussagekräftiger betrachte) ist ...

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());

Verify ist sehr leistungsfähig und es lohnt sich, sich daran zu gewöhnen.

Pete Martin
quelle
15
Dieser Ansatz ist in Ordnung, wenn Sie nur überprüfen möchten, ob eine Methode mit einem bekannten Parameter aufgerufen wurde. Wenn der Parameter zum Zeitpunkt des Testschreibens noch nicht erstellt wurde (z. B. wenn das betreffende Gerät den Parameter intern erstellt), können Sie ihn mit Callback erfassen und abfragen, während dies bei Ihrem Ansatz nicht der Fall ist.
Michael
1
Ich muss den übergebenen Wert speichern, da ich überprüfen muss, ob alle Objekte übergeben wurden.
MrFox
Manchmal ist der Parameter auch eine Entität, und Sie möchten separate Zusicherungen für jedes Feld in der Entität. Der beste Weg, dies zu tun, besteht darin, den Parameter zu erfassen, anstatt Verify und einen Matcher zu verwenden.
Kevin Wong
11

Die Alternative ist auch die Verwendung der Capture.InFunktion des moq. Es ist die OOTB- moqFunktion, die die Erfassung von Argumenten in der Sammlung ermöglicht.

//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
quelle
1
Gute Antwort! Die Klassen Moq.Capture und Moq.CaptureMatch waren mir nicht bekannt, bis ich diese Antwort sah. Capture ist eine bessere Alternative zu CallbackIMO. Da Sie Capture direkt in der Parameterliste verwenden, ist es weitaus weniger anfällig für Probleme beim Umgestalten der Parameterliste einer Methode und macht Tests daher weniger spröde. Bei Callback müssen Sie die im Setup übergebenen Parameter mit den für Callback verwendeten Typparametern synchron halten, und dies hat mir in der Vergangenheit definitiv Probleme bereitet.
Justin Holzer
7

Sie könnten It.Is<TValue>()Matcher verwenden.

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

Das funktioniert auch:

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

Viele gute Antworten hier! Verwenden Sie den sofort einsatzbereiten Moq-Funktionsumfang, bis Sie Aussagen zu mehreren Klassenparametern treffen müssen, die an Ihre Abhängigkeiten übergeben werden. Wenn Sie jedoch in dieser Situation enden, kann die Moq Verify-Funktion mit It.Is-Matchern den Testfehler nicht gut isolieren, und die Methode Returns / Callback zum Erfassen von Argumenten fügt Ihrem Test unnötige Codezeilen hinzu (und lange Tests sind für mich ein No-Go).

Hier ist ein Kern: https://gist.github.com/Jacob-McKay/8b8d41ebb9565f5fca23654fd944ac6b mit einer Moq (4.12) -Erweiterung, die ich geschrieben habe und die eine aussagekräftigere Möglichkeit bietet, Aussagen über Argumente zu machen, die an Mocks weitergegeben wurden, ohne die oben genannten Nachteile. So sieht der Abschnitt "Überprüfen" jetzt aus:

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

Ich wäre begeistert, wenn Moq eine Funktion bereitstellen würde, die dasselbe erreicht, gleichzeitig deklarativ ist und die Fehlerisolation bietet, die dies bewirkt. Daumen drücken!

Jacob McKay
quelle
1
Ich mag das. Das Verify of Moq konkurriert mit dem Assert von xUnit um die Berechtigung zur Durchführung von Asserts. Das fühlt sich im Moq-Teil des Setups nicht richtig an. Das It.Is-Feature-Setup sollte auch Nicht-Ausdrücke unterstützen.
Thomas
"Moq konkurriert mit dem Assert von xUnit um die Autorität der Durchführung von Asserts" - Gut gesagt, @Thomas. Ich würde hinzufügen, dass es die Konkurrenz verlieren würde. Moq ist gut darin, Sie wissen zu lassen, ob es einen passenden Anruf gab, aber bestimmte Behauptungen geben Ihnen viel bessere Informationen. Der Hauptnachteil meines Ansatzes besteht darin, die Typensicherheit und die Ordnungsprüfung von Parametern zu verlieren. Ich habe eine Weile nach einer Verbesserung gesucht, hoffentlich gibt es da draußen einen C # -Ninja, der zusammen ein Beispiel hacken kann! Andernfalls werde ich dies aktualisieren, wenn ich einen Weg finde.
Jacob McKay