setup - pruebas unitarias c# con mock
Verificando que un método fue llamado (3)
La prueba falla cuando Hello()
es interno porque Moq no puede proporcionar una anulación del método en este caso. Esto significa que se ejecutará la implementación interna de Hello()
, en lugar de la versión de prueba, lo que hará que la Verify()
falle.
Por cierto, lo que estás haciendo aquí no tiene sentido en el contexto de una prueba unitaria. A una prueba unitaria no debería importarle que Say()
llame a un método interno Hello()
. Esta es la implementación interna de su ensamblado y no una preocupación de consumir código.
Al usar Moq, tengo un problema muy extraño en el que la configuración en un simulacro solo parece funcionar si el método que estoy configurando es público. No sé si esto es un error Moq o si simplemente tengo esto mal (novato para Moq). Aquí está el caso de prueba:
public class TestClass
{
public string Say()
{
return Hello();
}
internal virtual string Hello()
{
return "";
}
}
[TestMethod]
public void Say_WhenPublic_CallsHello()
{
Mock<TestClass> mock = new Mock<TestClass>();
mock.Setup(x => x.Hello()).Returns("Hello World");
string result = mock.Object.Say();
mock.Verify(x => x.Hello(), Times.Exactly(1));
Assert.AreEqual("Hello World", result);
}
Lo cual falla con este mensaje:
Say_WhenPublic_CallsHello failed: Moq.MockException: Invocation no se realizó en el simulacro 1 veces: x => x.Hola () en Moq.Mock.ThrowVerifyException (IProxyCall esperado, Expresión de expresión, Times veces) ...
Si hago que el método Hello sea público como este, la prueba pasa. Cuál es el problema aquí?
public virtual string Hello()
{
return "";
}
¡Gracias por adelantado!
Moq no hace burlas parciales y solo puede simular métodos o interfaces virtuales públicos. Cuando creas un Mock estás creando un T completamente nuevo y eliminando toda implementación de cualquier método virtual público.
Mi sugerencia sería concentrarse en probar el área de superficie pública de sus objetos y no sus partes internas. Sus internos recibirán cobertura. Solo asegúrate de tener una idea clara de cuál es tu clase objetivo y no te burles de eso (en la mayoría de los casos).
La burla parcial solo es útil cuando quiere probar la funcionalidad de una clase abstracta con implementaciones simuladas de métodos abstractos. Si no estás haciendo esto, es probable que no te veas muy beneficiado haciendo un simulacro parcial .
Si esta no es una clase abstracta, querrás enfocarte más en el enfoque que sugiere Modan, lo que quiere decir que debes burlar las dependencias en lugar de la clase objetivo. Todo esto se reduce a la regla, no te burles de lo que estás probando .
No sé lo suficiente acerca de cómo funciona esto debajo de las coberturas para darle una respuesta técnica sobre por qué ese es el comportamiento, pero creo que puedo ayudarlo con su confusión.
En su ejemplo, está llamando a un método Say () y está devolviendo el texto esperado. Su expectativa no debería hacer cumplir una implementación particular de Say (), es decir, que llama a un método interno llamado Hello () para devolver esa cadena. Es por eso que no pasa la verificación, y también por qué la cadena devuelta es "", es decir, se ha llamado a la implementación real de Hello ().
Al hacer que el método Hello sea público, parece que esto ha habilitado a Moq para interceptar la llamada y usar su implementación en su lugar. Por lo tanto, en este caso la prueba parece pasar. Sin embargo, en este escenario no ha logrado realmente nada útil, porque su prueba dice que cuando llama a Say () el resultado es "Hello World" cuando en realidad el resultado es "".
He reescrito tu ejemplo para mostrar cómo esperaría que se usara Moq (no necesariamente definitivo, pero con suerte claro).
public interface IHelloProvider
{
string Hello();
}
public class TestClass
{
private readonly IHelloProvider _provider;
public TestClass(IHelloProvider provider)
{
_provider = provider;
}
public string Say()
{
return _provider.Hello();
}
}
[TestMethod]
public void WhenSayCallsHelloProviderAndReturnsResult()
{
//Given
Mock<IHelloProvider> mock = new Mock<IHelloProvider>();
TestClass concrete = new TestClass(mock.Object);
//Expect
mock.Setup(x => x.Hello()).Returns("Hello World");
//When
string result = concrete.Say();
//Then
mock.Verify(x => x.Hello(), Times.Exactly(1));
Assert.AreEqual("Hello World", result);
}
En mi ejemplo, he introducido una interfaz para un proveedor de IHello. Notará que no hay implementación de IHelloProvider. Esto está en el corazón de lo que estamos tratando de lograr mediante el uso de una solución de burla.
Estamos intentando probar una clase (TestClass), que depende de algo externo (IHelloProvider). Si está utilizando Test Driven Development, es probable que todavía no haya escrito un IHelloProvider, pero sabe que necesitará uno en algún momento. Sin embargo, en primer lugar, desea que TestClass funcione, en lugar de distraerse. O tal vez IHelloProvider usa una base de datos, o un archivo plano, o es difícil de configurar.
Incluso si tiene un IHelloProvider totalmente funcional, todavía está tratando de probar el comportamiento de TestClass, por lo que es probable que usar HelloProvider concreto haga que su prueba sea más propensa a fallar, por ejemplo, si hay un cambio en el comportamiento de HelloProvider, no quiere tener que cambiar las pruebas de cada clase que lo usa, solo quiere cambiar las pruebas de HelloProvider.
Volviendo al código, ahora tenemos una clase TestClass, que depende de una interfaz IHelloProvider, cuya implementación se proporciona en tiempo de construcción (esto es inyección de dependencia).
El comportamiento de Say () es que llama al método Hello () en IHelloProvider.
Si mira hacia atrás en la prueba, hemos creado un objeto TestClass real, ya que en realidad queremos probar el código que hemos escrito. Creamos un Mock IHelloProvider, y dijimos que esperamos que tenga su método Hello () llamado, y cuando lo haga, devuelva la cadena "Hello World".
Luego llamamos a Say () y verificamos los resultados como antes.
Lo importante es darse cuenta de que no estamos interesados en el comportamiento de IHelloProvider, por lo que podemos simularlo para facilitar las pruebas. Estamos interesados en el comportamiento de TestClass, por lo que hemos creado un TestClass real, no un simulacro, para que podamos probar su comportamiento real.
Espero que esto haya ayudado a aclarar lo que está pasando.