Verificación de un parámetro específico con Moq

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

Estoy empezando a usar Moq y estoy luchando un poco. Estoy tratando de verificar que messageServiceClient esté recibiendo el parámetro correcto, que es un XmlElement, pero no encuentro ninguna manera de hacerlo funcionar. Funciona solo cuando no verifico un valor en particular.

¿Algunas ideas?

Respuesta parcial: He encontrado una forma de probar que el xml enviado al proxy es correcto, pero todavía no creo que sea la forma correcta de hacerlo.

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

Por cierto, ¿cómo podría extraer la expresión de la llamada Verificar?

Luis Mirabal
fuente

Respuestas:

250

Si la lógica de verificación no es trivial, será complicado escribir un método lambda grande (como muestra su ejemplo). Podrías poner todas las declaraciones de prueba en un método separado, pero no me gusta hacer esto porque interrumpe el flujo de lectura del código de prueba.

Otra opción es usar una devolución de llamada en la llamada de Configuración para almacenar el valor que se pasó al método simulado y luego escribir Assertmétodos estándar para validarlo. Por ejemplo:

// 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
fuente
66
Un gran beneficio de este enfoque es que le dará fallas de prueba específicas de cómo el objeto es incorrecto (ya que está probando cada una individualmente).
Rob Church
1
Pensé que era el único que hizo esto, ¡me alegra ver que es un enfoque razonable!
Will Appleby
3
Creo que usar It.Is <MyObject> (validador) según Mayo es mejor ya que evita la forma un poco incómoda de guardar el valor del parámetro como parte del lambda
stevec
¿Es seguro este hilo, por ejemplo, cuando se ejecutan pruebas en paralelo?
Anton Tolken
@AntonTolken No lo he probado, pero en mi ejemplo, el objeto que se actualiza es una variable local (saveObject), por lo que debería ser seguro para subprocesos.
Rich Tebb
112

He estado verificando llamadas de la misma manera: creo que es la forma correcta de hacerlo.

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

Si su expresión lambda se vuelve difícil de manejar, puede crear una función que tome MyObjectcomo entrada y salida 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";
}

Además, tenga en cuenta un error con Mock donde el mensaje de error indica que el método se llamó varias veces cuando no se llamó en absoluto. Es posible que ya lo hayan solucionado, pero si ve ese mensaje, puede considerar verificar que realmente se haya llamado al método.

EDITAR: Aquí hay un ejemplo de verificación de llamadas múltiples veces para aquellos escenarios en los que desea verificar que llama a una función para cada objeto en una lista (por ejemplo).

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

El mismo enfoque para la configuración ...

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

Por lo tanto, cada vez que se llama a GetStuff para ese itemId, devolverá cosas específicas de ese elemento. Alternativamente, podría usar una función que tome itemId como entrada y devuelva cosas.

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

Otro método que vi en un blog hace algún tiempo (¿Phil Haack, tal vez?) Había configurado el regreso de algún tipo de objeto de cola: cada vez que se llamaba a la función, sacaba un elemento de una cola.

Mayonesa
fuente
1
Gracias, tiene sentido para mi. Lo que aún no puedo entender es cuándo especificar detalles en Configuración o Verificar. Es bastante confuso. Por el momento, solo estoy permitiendo cualquier cosa en la Configuración y especificando los valores en Verificar.
Luis Mirabal
¿Cómo crees que puedo verificar los mensajes cuando hay varias llamadas? El cliente toma mensajes y puede crear múltiples mensajes queueableable, que terminarán en múltiples llamadas y en cada una de esas llamadas, tengo que verificar diferentes mensajes. Todavía estoy luchando con las pruebas unitarias en general, todavía no estoy muy familiarizado con ellas.
Luis Mirabal
No creo que haya una bala de plata mágica en términos de cómo debes hacer esto. Se necesita práctica y comienzas a mejorar. Para mí, solo especifico parámetros cuando tengo algo con qué compararlos y cuando aún no estoy probando ese parámetro en otra prueba. En cuanto a las llamadas múltiples, hay varios enfoques. Para configurar y verificar una función que se llama varias veces, generalmente llamo a setup o verifique (Times.Once ()) para cada llamada que espero, a menudo con un bucle for. Puede usar los parámetros específicos para aislar cada llamada.
Mayo
Agregué algunos ejemplos para llamadas múltiples: vea la respuesta anterior.
Mayo
1
"Además, tenga en cuenta un error con Mock en el que el mensaje de error indica que el método se llamó varias veces cuando no se llamó en absoluto. Es posible que ya lo hayan solucionado, pero si ve ese mensaje, puede considerar verificar que el método fue realmente llamado ". - Un error como este invalida por completo una biblioteca burlona en mi humilde opinión. Qué irónico que no tuvieran el código de prueba adecuado :-)
Gianluca Ghettini
20

Una forma más simple sería hacer:

ObjectA.Verify(
    a => a.Execute(
        It.Is<Params>(p => p.Id == 7)
    )
);
dmitry.sergeyev
fuente
Parece que no puedo hacer que esto funcione, estoy tratando de verificar que mi método fue llamado con el Code.WRCC como parámetro. Pero mi prueba siempre pasa, a pesar de que el parámetro aprobado es 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

Creo que el problema está en el hecho de que Moq verificará la igualdad. Y, dado que XmlElement no anula Equals, su implementación verificará la igualdad de referencia.

¿No puedes usar un objeto personalizado para anular iguales?

Fernando
fuente
Sí, terminé haciendo eso. Me di cuenta de que el problema era verificar el Xml. En la segunda parte de la pregunta, agregué una posible respuesta deserializando el xml a un objeto
Luis Mirabal
1

Tenía uno de estos también, pero el parámetro de la acción era una interfaz sin propiedades públicas. Terminé usando It.Is () con un método separado y dentro de este método tuve que hacer algunas burlas de la interfaz

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
fuente