Überprüfen eines bestimmten Parameters mit Moq

168
public void SubmitMessagesToQueue_OneMessage_SubmitSuccessfully()
{
    var messageServiceClientMock = new Mock<IMessageServiceClient>();
    var queueableMessage = CreateSingleQueueableMessage();
    var message = queueableMessage[0];
    var xml = QueueableMessageAsXml(queueableMessage);
    messageServiceClientMock.Setup(proxy => proxy.SubmitMessage(xml)).Verifiable();
    //messageServiceClientMock.Setup(proxy => proxy.SubmitMessage(It.IsAny<XmlElement>())).Verifiable();

    var serviceProxyFactoryStub = new Mock<IMessageServiceClientFactory>();
    serviceProxyFactoryStub.Setup(proxyFactory => proxyFactory.CreateProxy()).Returns(essageServiceClientMock.Object);
    var loggerStub = new Mock<ILogger>();

    var client = new MessageClient(serviceProxyFactoryStub.Object, loggerStub.Object);
    client.SubmitMessagesToQueue(new List<IMessageRequestDTO> {message});

    //messageServiceClientMock.Verify(proxy => proxy.SubmitMessage(xml), Times.Once());
    messageServiceClientMock.Verify();
}

Ich fange an, Moq zu benutzen und kämpfe ein bisschen. Ich versuche zu überprüfen, ob messageServiceClient den richtigen Parameter empfängt, nämlich ein XmlElement, aber ich kann keine Möglichkeit finden, dies zum Laufen zu bringen. Es funktioniert nur, wenn ich einen bestimmten Wert nicht überprüfe.

Irgendwelche Ideen?

Teilantwort: Ich habe einen Weg gefunden, um zu testen, ob die an den Proxy gesendete XML korrekt ist, aber ich denke immer noch nicht, dass dies der richtige Weg ist.

public void SubmitMessagesToQueue_OneMessage_SubmitSuccessfully()
{
    var messageServiceClientMock = new Mock<IMessageServiceClient>();
    messageServiceClientMock.Setup(proxy => proxy.SubmitMessage(It.IsAny<XmlElement>())).Verifiable();
    var serviceProxyFactoryStub = new Mock<IMessageServiceClientFactory>();
    serviceProxyFactoryStub.Setup(proxyFactory => proxyFactory.CreateProxy()).Returns(messageServiceClientMock.Object);
    var loggerStub = new Mock<ILogger>();

    var client = new MessageClient(serviceProxyFactoryStub.Object, loggerStub.Object);
    var message = CreateMessage();
    client.SubmitMessagesToQueue(new List<IMessageRequestDTO> {message});

    messageServiceClientMock.Verify(proxy => proxy.SubmitMessage(It.Is<XmlElement>(xmlElement => XMLDeserializer<QueueableMessage>.Deserialize(xmlElement).Messages.Contains(message))), Times.Once());
}

Wie kann ich übrigens den Ausdruck aus dem Verify-Aufruf extrahieren?

Luis Mirabal
quelle

Antworten:

248

Wenn die Überprüfungslogik nicht trivial ist, ist es schwierig, eine große Lambda-Methode zu schreiben (wie Ihr Beispiel zeigt). Sie könnten alle Testanweisungen in eine separate Methode einfügen, aber ich mache das nicht gerne, weil es den Fluss des Lesens des Testcodes stört.

Eine andere Möglichkeit besteht darin, einen Rückruf beim Setup-Aufruf zu verwenden, um den Wert zu speichern, der an die verspottete Methode übergeben wurde, und dann Standardmethoden Assertzu schreiben , um ihn zu validieren. Beispielsweise:

// Arrange
MyObject saveObject;
mock.Setup(c => c.Method(It.IsAny<int>(), It.IsAny<MyObject>()))
        .Callback<int, MyObject>((i, obj) => saveObject = obj)
        .Returns("xyzzy");

// Act
// ...

// Assert
// Verify Method was called once only
mock.Verify(c => c.Method(It.IsAny<int>(), It.IsAny<MyObject>()), Times.Once());
// Assert about saveObject
Assert.That(saveObject.TheProperty, Is.EqualTo(2));
Rich Tebb
quelle
6
Ein großer Vorteil dieses Ansatzes besteht darin, dass Sie spezifische Testfehler erhalten, wenn das Objekt falsch ist (da Sie jedes einzeln testen).
Rob Church
1
Ich dachte, ich wäre der einzige, der dies tat, froh zu sehen, dass es ein vernünftiger Ansatz ist!
Will Appleby
3
Ich denke, die Verwendung von It.Is <MyObject> (Validator) gemäß Mayo ist besser, da es die etwas umständliche Art vermeidet, den Parameterwert als Teil des Lambda zu
speichern
Ist dieser Thread sicher, wenn beispielsweise Tests parallel ausgeführt werden?
Anton Tolken
@AntonTolken Ich habe es nicht ausprobiert, aber in meinem Beispiel ist das aktualisierte Objekt eine lokale Variable (saveObject), daher sollte es threadsicher sein.
Rich Tebb
111

Ich habe Anrufe auf die gleiche Weise überprüft - ich glaube, es ist der richtige Weg, dies zu tun.

mockSomething.Verify(ms => ms.Method(
    It.IsAny<int>(), 
    It.Is<MyObject>(mo => mo.Id == 5 && mo.description == "test")
  ), Times.Once());

Wenn Ihr Lambda-Ausdruck unhandlich wird, können Sie eine Funktion erstellen, die MyObjectals Eingabe und Ausgabe true/ false...

mockSomething.Verify(ms => ms.Method(
    It.IsAny<int>(), 
    It.Is<MyObject>(mo => MyObjectFunc(mo))
  ), Times.Once());

private bool MyObjectFunc(MyObject myObject)
{
  return myObject.Id == 5 && myObject.description == "test";
}

Beachten Sie auch einen Fehler bei Mock, bei dem in der Fehlermeldung angegeben wird, dass die Methode mehrmals aufgerufen wurde, wenn sie überhaupt nicht aufgerufen wurde. Möglicherweise haben sie das Problem inzwischen behoben. Wenn Sie diese Meldung sehen, sollten Sie möglicherweise überprüfen, ob die Methode tatsächlich aufgerufen wurde.

BEARBEITEN: Hier ist ein Beispiel für den mehrmaligen Aufruf von verify für Szenarien, in denen Sie überprüfen möchten, ob Sie beispielsweise für jedes Objekt in einer Liste eine Funktion aufrufen.

foreach (var item in myList)
  mockRepository.Verify(mr => mr.Update(
    It.Is<MyObject>(i => i.Id == item.Id && i.LastUpdated == item.LastUpdated),
    Times.Once());

Gleicher Ansatz für die Einrichtung ...

foreach (var item in myList) {
  var stuff = ... // some result specific to the item
  this.mockRepository
    .Setup(mr => mr.GetStuff(item.itemId))
    .Returns(stuff);
}

Jedes Mal, wenn GetStuff für diese itemId aufgerufen wird, werden für dieses Item spezifische Daten zurückgegeben. Alternativ können Sie eine Funktion verwenden, die itemId als Eingabe verwendet und Daten zurückgibt.

this.mockRepository
    .Setup(mr => mr.GetStuff(It.IsAny<int>()))
    .Returns((int id) => SomeFunctionThatReturnsStuff(id));

Bei einer anderen Methode, die ich vor einiger Zeit in einem Blog gesehen habe (Phil Haack vielleicht?), Wurde das Setup von einem Dequeue-Objekt zurückgegeben. Jedes Mal, wenn die Funktion aufgerufen wurde, wurde ein Element aus einer Warteschlange gezogen.

Mayo
quelle
1
Danke, das macht für mich Sinn. Was ich immer noch nicht verstehen kann, ist, wann Details in Setup oder Verify angegeben werden müssen. Es ist ziemlich verwirrend. Im Moment erlaube ich nur alles in Setup und gebe die Werte in Verify an.
Luis Mirabal
Wie kann ich die Nachrichten bei mehreren Anrufen überprüfen? Der Client nimmt Nachrichten entgegen und kann mehrere queue-fähige Nachrichten erstellen, die in mehreren Anrufen enden. Bei jedem dieser Anrufe muss ich verschiedene Nachrichten überprüfen. Ich habe immer noch Probleme mit Unit-Tests im Allgemeinen, ich bin noch nicht sehr vertraut damit.
Luis Mirabal
Ich glaube nicht, dass es eine magische Silberkugel gibt, wie Sie das tun sollten. Es braucht Übung und du wirst besser. Für mich gebe ich Parameter nur an, wenn ich etwas zum Vergleichen habe und wenn ich diesen Parameter noch nicht in einem anderen Test teste. Für mehrere Anrufe gibt es mehrere Ansätze. Zum Einrichten und Überprüfen einer Funktion, die mehrmals aufgerufen wird, rufe ich normalerweise setup oder verify (Times.Once ()) für jeden erwarteten Aufruf auf - häufig mit einer for-Schleife. Sie können die spezifischen Parameter verwenden, um jeden Anruf zu isolieren.
Mayo
Ich habe einige Beispiele für mehrere Anrufe hinzugefügt - siehe Antwort oben.
Mayo
1
"Beachten Sie auch einen Fehler bei Mock, bei dem in der Fehlermeldung angegeben wird, dass die Methode mehrmals aufgerufen wurde, als sie überhaupt nicht aufgerufen wurde. Möglicherweise wurde sie bereits behoben. Wenn Sie diese Meldung jedoch sehen, sollten Sie dies möglicherweise überprüfen Die Methode wurde tatsächlich aufgerufen. " - Ein Fehler wie dieser macht eine verspottete Bibliothek meiner Meinung nach vollständig ungültig. Wie ironisch, dass sie keinen richtigen
Testcode
20

Ein einfacherer Weg wäre:

ObjectA.Verify(
    a => a.Execute(
        It.Is<Params>(p => p.Id == 7)
    )
);
dmitry.sergeyev
quelle
Ich kann das scheinbar nicht zum Laufen bringen. Ich versuche zu überprüfen, ob meine Methode mit dem Code.WRCC als Parameter aufgerufen wurde. Aber mein Test besteht immer, obwohl der übergebene Parameter WRDD ist. m.Setup(x => x.CreateSR(Code.WRDD)).ReturnsAsync(0); await ClassUnderTest.CompleteCC(accountKey, It.IsAny<int>(), mockRequest); m.Verify(x => x.CreateSR(It.Is<Code>(p => p == Code.WRCC)), Times.Once());
Greg Quinn
1

Ich glaube, dass das Problem in der Tatsache liegt, dass Moq auf Gleichheit prüfen wird. Da XmlElement Equals nicht überschreibt, wird bei seiner Implementierung die Referenzgleichheit überprüft.

Können Sie kein benutzerdefiniertes Objekt verwenden, um Gleiches zu überschreiben?

Fernando
quelle
Ja, das habe ich letztendlich gemacht. Mir wurde klar, dass das Problem darin bestand, die XML zu überprüfen. Im zweiten Teil der Frage habe ich eine mögliche Antwort hinzugefügt, die die XML zu einem Objekt deserialisiert
Luis Mirabal
1

Hatte auch eine davon, aber der Parameter der Aktion war eine Schnittstelle ohne öffentliche Eigenschaften. Endete mit It.Is () mit einer separaten Methode und musste innerhalb dieser Methode einige Verspottungen der Schnittstelle durchführen

public interface IQuery
{
    IQuery SetSomeFields(string info);
}

void DoSomeQuerying(Action<IQuery> queryThing);

mockedObject.Setup(m => m.DoSomeQuerying(It.Is<Action<IQuery>>(q => MyCheckingMethod(q)));

private bool MyCheckingMethod(Action<IQuery> queryAction)
{
    var mockQuery = new Mock<IQuery>();
    mockQuery.Setup(m => m.SetSomeFields(It.Is<string>(s => s.MeetsSomeCondition())
    queryAction.Invoke(mockQuery.Object);
    mockQuery.Verify(m => m.SetSomeFields(It.Is<string>(s => s.MeetsSomeCondition(), Times.Once)
    return true
}
ds4940
quelle