Pruebas unitarias con Moq

sábado, 16 de noviembre de 2013

Moq es un framework para crear mocks en .Net, se puede descargar mediante NuGet y actualmente tiene unas 700.000 descargas lo que indica la popularidad de este framework frente a otros que se pueden descargar también mediante NuGet.
Utiliza linq con expresiones lambda lo que lo hace muy flexible y productivo.

Antes de empezar con un ejemplo utilizando moq, voy a repasar terminología de TDD.




El elemento que queremos probar se denomina SUT (subject under test).

Existen diferentes tipo de objetos doble que reemplazan dependencias reales del SUT.

Dummy: Es un objeto que no se utiliza en el test, su único objetivo es rellenar un parámetro requerido en por el SUT.

Fake: es un objeto que tiene una implementación casi real pero que tiene alguna característica que hace que los test pasen más rápido pero a la vez la convierte en no apto para producción. Un buen ejemplo es una base de datos en memoria.

Mock: es un objeto que tiene respuestas preparadas para el test del SUT y además forma parte del test verificar mediante el objeto mock que ciertos métodos o funciones han sido invocados por el SUT. Su utilización esta más enfocada a testear el comportamiento del SUT con el mock.

Stub: es un objeto tiene respuestas preparadas para el SUT pero a diferencia del mock su uso esta más enfocado a una evaluación final del estado del SUT y no se comprueba el comportamiento del SUT hacia el stub.

En moq podemos crear este tipo de objetos doble, vamos con el ejemplo.
Tenemos un blog donde se publican post y antes de publicar un nuevo post se debe validar que el html es correcto y además se escribe en un log si la publicación se ha realizado con éxito y también si hay error  en el html a publicar.

Tedríamos una clase servicio de blog, un interface para el log y para el validador de html como dependencias.
    public interface ILog
    {
        void Log(string message);
    }

    public interface IHtmlValidator
    {
        bool IsValid(string html);
    }

    public class BlogService
    {
        IHtmlValidator _htmlValidator;
        ILog _log;

        public BlogService(IHtmlValidator htmlValidator, ILog log)
        {
            _htmlValidator = htmlValidator;
        }

        private void Log(string message)
        {
            if (_log != null)
                _log.Log(message);
        }

        public bool PublicPost(string html)
        {
            if (_htmlValidator.IsValid(html))
            {
                Log("Post has published");

                return true;
            }
            else
            {
                Log("publish error, html not valid");

                throw new ArgumentException("html not valid");
            }
        }
    }

Una primera prueba podría ser simplemente crearnos las dependencias sin implementación, a modo de objetos dummy para verificar que no hay ningún problema al crear el servicio de blog.
[TestMethod]
public void TestCreateInstance()
{
    //creamos objetos dummy
    var mockLog = new Mock();
    var mockHtmlValidator = new Mock();

    BlogService blog = new BlogService(mockHtmlValidator.Object, mockLog.Object);

    Assert.IsNotNull(blog);
}

Ahora una siguiente prueba podría ser comprobar que cuando le pasamos un html incorrecto, el servicio de blog lanza una ArgumentException. Preparamos un stub para el interface IHtmlValidator que siempre devuelva false en el método IsValid y en el test con un try catch verificamos que la excepción es lanzada en este caso.
[TestMethod]
public void ShouldThrowArgumentExceptionTest()
{
    var mockLog = new Mock();
    
    //cuando se llame al método log, escribimos en el output
    mockLog.Setup(l => l.Log(It.IsAny())).Callback(param => System.Diagnostics.Debug.Write(param));

    var mockHtmlValidator = new Mock();

    //al validar html queremos comprobar como se comporta el blog si no es valido
    mockHtmlValidator.Setup(v => v.IsValid(It.IsAny())).Returns(false);

    BlogService blog = new BlogService(mockHtmlValidator.Object, mockLog.Object);

    bool testOK = false;

    try
    {
        blog.PublicPost("html no valido");
    }
    catch (ArgumentException ex)
    {
        //el test es correcto
        testOK = true;
    }

    Assert.IsTrue(testOK);
}

En la siguiente prueba vamos a verificar el caso contrario, que todo va bien y no se produce ninguna excepción, además vamos a verificar el servicio de blog realiza una llama a nuestro objeto doble HtmlValidator, lo que lo convierte en un mock.
[TestMethod]
public void PublishOkTest()
{
    var mockLog = new Mock();

    //cuando se llame al método log, escribimos en el output
    mockLog.Setup(l => l.Log(It.IsAny())).Callback(param => System.Diagnostics.Debug.Write(param));

    var mockHtmlValidator = new Mock();

    //al validar html queremos comprobar como se comporta el blog si no es valido
    mockHtmlValidator.Setup(v => v.IsValid(It.IsAny())).Returns(true);

    BlogService blog = new BlogService(mockHtmlValidator.Object, mockLog.Object);

    bool testOK = false;

    try
    {
        blog.PublicPost("html valido");
    }
    catch (ArgumentException ex)
    {
        //el test es correcto
        testOK = false;
    }

    //se comprueba que se invoca el método IsValid
    mockHtmlValidator.Verify(validator => validator.IsValid(It.IsAny()));

    Assert.IsTrue(testOK);
}

Enlace a página de Moq
Enlace a código fuente

Libros Relacionados

Professional Test Driven Development with C#: Developing Real World Applications with TDD

xUnit Test Patterns: Refactoring Test Code

The Art of Unit Testing: With Examples in .Net

Test Driven Development: By Example


2 comentarios:

  1. La test PublishOkTest() falla por que en la linea 20 faltó asignar el resultado a testOK.

    Cambiar linea 20 por:
    testOK = blog.PublicPost("html valido");

    ResponderEliminar
  2. jaja es correcto lo que mencionas xD

    ResponderEliminar