Weryfikacja określonego parametru za pomocą Moq

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

Zaczynam używać Moq i trochę się zmagam. Próbuję sprawdzić, czy messageServiceClient odbiera właściwy parametr, którym jest XmlElement, ale nie mogę znaleźć żadnego sposobu, aby to zadziałało. Działa tylko wtedy, gdy nie sprawdzam określonej wartości.

Jakieś pomysły?

Częściowa odpowiedź: znalazłem sposób, aby sprawdzić, czy plik XML wysłany do serwera proxy jest poprawny, ale nadal uważam, że nie jest to właściwy sposób.

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

Przy okazji, jak mogę wyodrębnić wyrażenie z wywołania Verify?

Luis Mirabal
źródło

Odpowiedzi:

251

Jeśli logika weryfikacji jest nietrywialna, napisanie dużej metody lambda będzie trudne (jak pokazuje przykład). Mógłbyś umieścić wszystkie instrukcje testowe w oddzielnej metodzie, ale nie lubię tego robić, ponieważ zakłóca to przepływ odczytu kodu testowego.

Inną opcją jest użycie wywołania zwrotnego w wywołaniu Instalatora w celu zapisania wartości, która została przekazana do metody symulowanej, a następnie napisanie standardowych Assertmetod w celu jej zweryfikowania. Na przykład:

// 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
źródło
6
Jedną dużą zaletą tego podejścia jest to, że da ci ono określone niepowodzenia testów dla tego, jak obiekt jest nieprawidłowy (ponieważ testujesz każdy z osobna).
Rob Church
1
Myślałem, że byłem jedynym, który to zrobił, ciesząc się, że to rozsądne podejście!
Will Appleby,
3
Myślę, że użycie It.Is <MyObject> (walidator) według Mayo jest lepsze, ponieważ pozwala uniknąć nieco niezręcznego sposobu zapisywania wartości parametru jako części lambda
stevec
czy ten wątek jest bezpieczny, na przykład podczas równoległego uruchamiania testów?
Anton Tolken
@AntonTolken Nie próbowałem tego, ale w moim przykładzie aktualizowany obiekt jest zmienną lokalną (saveObject), więc powinien być bezpieczny dla wątków.
Rich Tebb
113

Weryfikowałem połączenia w ten sam sposób - uważam, że to właściwy sposób.

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

Jeśli twoje wyrażenie lambda stanie się nieporęczne, możesz utworzyć funkcję, która przyjmuje MyObjectjako dane wejściowe i wyjściowe 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";
}

Należy również pamiętać o błędzie w Mock, w którym komunikat o błędzie informuje, że metoda została wywołana wiele razy, gdy w ogóle nie została wywołana. Być może już to naprawili - ale jeśli zobaczysz ten komunikat, możesz rozważyć sprawdzenie, czy metoda została faktycznie wywołana.

EDYCJA: Oto przykład wielokrotnego wywoływania weryfikacji dla tych scenariuszy, w których chcesz sprawdzić, czy wywołujesz funkcję dla każdego obiektu na liście (na przykład).

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

To samo podejście do konfiguracji ...

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

Więc za każdym razem, gdy GetStuff zostanie wywołany dla tego itemId, zwróci rzeczy specyficzne dla tego elementu. Alternatywnie, możesz użyć funkcji, która pobiera itemId jako dane wejściowe i zwraca rzeczy.

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

Inna metoda, którą widziałem na blogu jakiś czas temu (być może Phil Haack?), Polegała na konfiguracji zwracania z jakiegoś rodzaju obiektu usuwanego z kolejki - za każdym razem, gdy funkcja była wywoływana, ściągał element z kolejki.

Mayo
źródło
1
Dzięki, dla mnie to ma sens. Nadal nie mogę zrozumieć, kiedy określić szczegóły w konfiguracji lub weryfikacji. To dość zagmatwane. W tej chwili po prostu zezwalam na wszystko w Instalatorze i określam wartości w Verify.
Luis Mirabal
Jak myślisz, jak mogę sprawdzić wiadomości, gdy jest wiele połączeń? Klient przyjmuje wiadomości i może tworzyć wiele queueableMessages, które kończą się wieloma wywołaniami iw każdym z tych połączeń muszę sprawdzić różne wiadomości. Generalnie wciąż zmagam się z testami jednostkowymi, jeszcze nie znam ich zbyt dobrze.
Luis Mirabal
Nie sądzę, aby istniała magiczna srebrna kula, jeśli chodzi o to, jak powinieneś to zrobić. Potrzeba praktyki i zaczynasz się poprawiać. U mnie określam parametry tylko wtedy, gdy mam coś do porównania i gdy nie testuję już tego parametru w innym teście. Jeśli chodzi o wiele połączeń, istnieje kilka podejść. Aby skonfigurować i zweryfikować funkcję, która jest wywoływana wiele razy, zwykle wywołuję konfigurację lub weryfikację (Times.Once ()) dla każdego oczekiwanego wywołania - często z pętlą for. Możesz użyć określonych parametrów, aby odizolować każde połączenie.
Mayo,
Dodałem kilka przykładów wielu połączeń - patrz odpowiedź powyżej.
Mayo,
1
„Pamiętaj też o błędzie w Mock, w którym komunikat o błędzie informuje, że metoda była wywoływana wiele razy, gdy w ogóle nie była wywoływana. Być może już go naprawili - ale jeśli zobaczysz ten komunikat, możesz rozważyć weryfikację tego metoda została faktycznie wywołana. " - Taki błąd całkowicie unieważnia fałszywą bibliotekę IMHO. Jak na ironię nie mieli odpowiedniego kodu testowego :-)
Gianluca Ghettini
20

Prostszym sposobem byłoby:

ObjectA.Verify(
    a => a.Execute(
        It.Is<Params>(p => p.Id == 7)
    )
);
dmitry.sergeyev
źródło
Nie wydaje mi się, aby to działało, próbuję sprawdzić, czy moja metoda została wywołana z parametrem Code.WRCC. Ale mój test zawsze kończy się pomyślnie, mimo że przekazany parametr to WRDD .. 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

Uważam, że problem polega na tym, że Moq sprawdzi równość. A ponieważ XmlElement nie zastępuje Equals, jego implementacja sprawdzi równość odwołań.

Nie możesz użyć niestandardowego obiektu, aby zastąpić równa się?

Fernando
źródło
Tak, skończyłem na tym. Zdałem sobie sprawę, że problem polega na sprawdzeniu pliku Xml. W drugiej części pytania dodałem możliwą odpowiedź deserializującą xml do obiektu
Luis Mirabal
1

Miał też jeden z nich, ale parametrem akcji był interfejs bez właściwości publicznych. Skończyło się na użyciu It.Is () z oddzielną metodą iw ramach tej metody musiałem zrobić trochę kpiny z interfejsu

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
źródło