Moq: cómo llegar a un parámetro pasado a un método de un servicio simulado

169

Imagina esta clase

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 en una prueba de Foo, ¿cómo podría verificar qué Bar()ha pasado _h.AsyncHandle?

ene
fuente
¿Quiso decir "AsyncHandle" (extra "n")? ¿Y podría publicar el código para Handler o especificar el nombre de tipo completo si es un tipo estándar?
TrueWill
¿Puedes mostrar tu prueba de esqueleto para mostrar lo que estás pensando? Si bien agradezco que de su lado sea obvio, de nuestro lado, parece alguien que no se ha tomado el tiempo de responder la pregunta sin hacer una respuesta especulativa larga.
Ruben Bartelink
1
No hay un Foo ni un Bar () ni nada de esto. Es solo un código de demostración para mostrar la situación en la que me encuentro sin distraerme de los detalles de la aplicación. Y obtuve la respuesta que esperaba obtener.
Jan

Respuestas:

283

Puede usar el método 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 solo desea verificar algo simple en el argumento pasado, también puede hacerlo directamente:

mock.Setup(h => h.AnsyncHandle(It.Is<SomeResponse>(response => response != null)));
Gamlor
fuente
36
Una nota al margen, si tiene múltiples argumentos para su función, debe especificar todos los tipos en el Callback<>()método genérico Moq. Por ejemplo, si tu método tuviera la definición Handler.AnsyncHandle(string, SomeResponse), necesitarías /* ... */.Callback<string, SomeResponse>(r => result = r);. No he encontrado esto explícitamente en muchos lugares, así que pensé que lo agregaría aquí.
Frank Bryce
12
Solo quería corregir @Frank en caso de que no haya visto la respuesta de @JavaJudt. La forma correcta de obtener dos argumentos es:/* ... */.Callback<string, SomeResponse>((s1, s2) => { str1 = s1; result = s2});
renatogbp
2
También puede lograr lo mismo simplemente declarando los tipos de argumentos en la expresión lambda, así:.Callback((string s1, SomeResponse s2) => /* stuff */ )
jb637
1
¿Podría actualizar su respuesta para incluir una referencia al Capture.Inayudante incorporado ?
cao
29

La respuesta de Gamlor funcionó para mí, pero pensé que podría ampliar el comentario de John Carpenter porque estaba buscando una solución que involucrara más de un parámetro. Me imaginé que otras personas que se topan con esta página pueden estar en una situación similar. Encontré esta información en la documentación de Moq .

Usaré el ejemplo de Gamlor, pero supongamos que el método AsyncHandle toma dos argumentos: ay stringun SomeResponseobjeto.

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

Básicamente, solo necesita agregar otro It.IsAny<>()con el tipo apropiado, agregar otro tipo al Callbackmétodo y cambiar la expresión lambda según corresponda.

JavaJudt
fuente
22

El método de devolución de llamada ciertamente funcionará, pero si está haciendo esto en un método con muchos parámetros, puede ser un poco detallado. Aquí hay algo que he usado para eliminar parte de la placa repetitiva.

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

Aquí está la fuente de 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
fuente
21

La respuesta de Gamlor funciona, pero otra forma de hacerlo (y una que considero más expresiva en la prueba) es ...

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 es muy poderoso y vale la pena tomarse el tiempo para acostumbrarse.

Pete martin
fuente
15
Ese enfoque está bien si solo desea verificar si se llamó a un método con un parámetro conocido. En el caso de que el parámetro aún no se haya creado en el momento de escribir la prueba (por ejemplo, la unidad en cuestión crea el parámetro internamente), Callback le permite capturar e interrogar esto, mientras que su enfoque no lo haría.
Michael
1
Necesito almacenar el valor pasado porque necesito verificar que todo un conjunto de objetos pasó.
MrFox
Además, a veces el parámetro es una entidad, y desea afirmaciones separadas para cada campo en la entidad. La mejor manera de hacerlo es capturar el parámetro, en lugar de usar Verify y un comparador.
Kevin Wong
12

La alternativa también es utilizar la Capture.Infunción de moq. Es la moqcaracterística OOTB que permite capturar argumentos en la colección.

//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
fuente
1
¡Gran respuesta! No conocía las clases Moq.Capture y Moq.CaptureMatch hasta que vi esta respuesta. Capture es una mejor alternativa a CallbackIMO. Dado que utiliza Capture directamente en la lista de parámetros, es mucho menos propenso a problemas al refactorizar la lista de parámetros de un método y, por lo tanto, hace que las pruebas sean menos frágiles. Con Callback, debe mantener los parámetros pasados ​​en la Configuración sincronizados con los parámetros de tipo utilizados para Callback, y definitivamente me ha causado problemas en el pasado.
Justin Holzer
7

Podrías usar 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
fuente
2

Esto también funciona:

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

Muchas buenas respuestas aquí! Vaya con el conjunto de características Moq listo para usar hasta que necesite hacer afirmaciones sobre varios parámetros de clase pasados ​​a sus dependencias. Sin embargo, si termina en esa situación, la función de verificación de Moq con It. Sus coincidencias no hace un buen trabajo al aislar la falla de la prueba, y la forma de devolución / devolución de llamada de capturar argumentos agrega líneas de código innecesarias a su prueba (y las pruebas largas son un no-go para mí).

Aquí hay una idea general: https://gist.github.com/Jacob-McKay/8b8d41ebb9565f5fca23654fd944ac6b con una extensión Moq (4.12) que escribí que ofrece una forma más declarativa de hacer afirmaciones sobre los argumentos pasados ​​a los simulacros, sin los inconvenientes mencionados anteriormente. Así es como se ve la sección Verificar ahora:

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

Me entusiasmaría si Moq proporcionara una función que lograra lo mismo mientras fuera declarativa y proporcionara el aislamiento de falla que esto hace. ¡Dedos cruzados!

Jacob McKay
fuente
1
Me gusta esto. El Verify of Moq compite con el Assert de xUnit por la autoridad de realizar aserciones. Eso no se siente bien en la parte Moq de la configuración. La configuración de la característica It.Is también debe admitir expresiones no.
Thomas
"Moq compite con Assert de xUnit por la autoridad de realizar afirmaciones" - Bien dicho @Thomas. Añadiría que perdería la competencia. Moq es bueno para hacerle saber si hubo una llamada coincidente, pero las afirmaciones específicas le brindan mucha mejor información. El principal inconveniente de mi enfoque es perder la seguridad de los tipos y la verificación de los parámetros de los pedidos. He estado buscando una mejora sobre esto durante un tiempo, ¡espero que haya un ninja de C # que pueda hackear un ejemplo juntos! De lo contrario, si encuentro una manera, actualizaré esto.
Jacob McKay