Pruebas unitarias en un servicio Web API RESTful de .NET Core

Realizar pruebas unitarias sobre un servicio Web API de .NET es una tarea algo más "complicada" de lo que parecería a simple vista. Esto es debido a que no solamente hay que comprobar la consistencia de los resultados de cada acción, si no que además debemos asegurar que las respuestas (Status Codes) de los Métodos Http del servicio sean las correctas y esperadas en cada momento.

Como continuación al Post Pruebas unitarias en Entity Framework Core - SqLite in-memory, en este caso veremos como realizar las pruebas unitarias a un servicio Web API de .NET Core 2.1, que implementa los métodos Http básicos (GET, POST, PUT, DELETE) a través de un servicio estándar de acceso a datos (CRUD).

El escenario de pruebas

En primer lugar crearemos la clase "controladora" del Web API sobre la cual realizaremos las pruebas unitarias posteriormente (PaisController.cs). Este Web API se encargará de realizar las operaciones básicas de acceso a datos (CRUD) sobre una entidad de modelo de ejemplo (Pais.cs), a través del servicio asíncrono PaisesDataService.cs.

    [Produces("application/json")]
    [Route("api/Pais")]
    public class PaisController : Controller
    {
        private readonly IPaisesDataService paisesDataService;

        public PaisController(IPaisesDataService paisesDataService)
        {
            this.paisesDataService = paisesDataService;
        }

        [HttpGet]
        public async Task<IActionResult> GetPaises()
        {
            var paises = await paisesDataService.ReadPaises();
            // Ok Response Status 200.
            return Ok(paises);
        }

        // GET: api/Pais/5
        [HttpGet("{id}")]
        public async Task<IActionResult> GetPais([FromRoute] int id)
        {
            var pais = await paisesDataService.ReadPais(id);

            if (pais == null)
            {
                // NotFound Response Status 404.
                return NotFound();
            }

            // Ok Response Status 200.
            return Ok(pais);
        }

        // POST: api/Pais
        [HttpPost]
        public async Task<IActionResult> PostPais([FromBody] Pais pais)
        {
            if (!ModelState.IsValid)
            {
                // BadRequest Response Status 400.
                return BadRequest(ModelState);
            }

            if (await paisesDataService.CreatePais(pais) == 0)
            {
                // NotFound Response Status 404.
                return NotFound();
            }

            // Created Response Status 201.
            return CreatedAtAction("GetPais", new { id = pais.Id }, pais);
        }

        // PUT: api/Pais/5
        [HttpPut("{id}")]
        public async Task<IActionResult> PutPais([FromRoute] int id, [FromBody] Pais pais)
        {
            if (!ModelState.IsValid)
            {
                // BadRequest Response Status 400.
                return BadRequest(ModelState);
            }

            if (id != pais.Id)
            {
                // BadRequest Response Status 400.
                return BadRequest();
            }

            if (await paisesDataService.UpdatePais(pais) == 0)
            {
                // NotFound Response Status 404.
                return NotFound();
            }

            // NoContent Response Status 204.
            return NoContent();
        }        

        // DELETE: api/Pais/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeletePais([FromRoute] int id)
        {
            if (await paisesDataService.DeletePais(id) == 0)
            {
                // NotFound Response Status 404.
                return NotFound();
            }

            // Ok Response Status 200.
            return Ok();
        }

    }

Como podemos observar en el código, el controlador PaisController recibe como parámetro en el constructor, una instancia del servicio de acceso a datos PaisesDataService a través de su interfaz IPaisesDataService.

    public interface IPaisesDataService
    {
        Task<int> CreatePais(Pais pais);
        Task<Pais> ReadPais(int id);
        Task<IEnumerable<Pais>> ReadPaises();
        Task<int> UpdatePais(Pais pais);
        Task<int> DeletePais(int id);                
    }

El servicio PaisesDataService.cs es exactamente el mismo que ya vimos y explicamos en el Post Pruebas unitarias en Entity Framework Core - SqLite in-memory.

Las pruebas unitarias

Una vez construido el escenario de pruebas de ejemplo, pasaremos a crear un nuevo proyecto de pruebas unitarias del tipo Proyecto de prueba de MSTest(.NET Core). Para este ejemplo se ha utilizado Visual Studio Community 2017 (con todas las actualizaciones al día).

Por supuesto, es necesario añadir una referencia (dependencia) al proyecto sobre el que vamos a realizar las pruebas (el escenario de pruebas anteriormente creado).

El servicio de acceso a datos de pruebas (Fake)

Para realizar las pruebas unitarias, necesitamos "simular" un entorno de acceso a datos que implemente la interfaz IPaisesDataService. Este entorno o servicio ficticio (Fake) será "inyectado" a través del constructor del Web API (PaisController) a la hora de realizar las pruebas unitarias, y deberá proporcionar la misma funcionalidad que el servicio PaisesDataService nos ofrecería en un entorno real de producción.

    public class PaisesDataServiceFake : IPaisesDataService
    {
        private SqLiteDbFake sqLiteDbFake; 

        public PaisesDataServiceFake()
        {
            sqLiteDbFake = new SqLiteDbFake();
            using (var context = sqLiteDbFake.GetDbContext())
            {
                context.Paises.Add(new Pais { Id = 1, Nombre = "España", Habitantes = 1000, Provincias = new List<Provincia>() });
                context.Paises.Add(new Pais { Id = 2, Nombre = "Francia", Habitantes = 1000, Provincias = new List<Provincia>() });
                context.Paises.Add(new Pais { Id = 3, Nombre = "Italia", Habitantes = 1000, Provincias = new List<Provincia>() });
                context.SaveChangesAsync();
            }
        }

        public async Task<int> CreatePais(Pais pais)
        {
            using (var context = sqLiteDbFake.GetDbContext())
            {
                context.Paises.Add(pais);
                try
                {
                    return await context.SaveChangesAsync();
                }
                catch (Exception)
                {
                    return 0;
                }                
            }
        }

        public async Task<Pais> ReadPais(int id)
        {
            using (var context = sqLiteDbFake.GetDbContext())
            {
                return await context.Paises.Include(x => x.Provincias).SingleOrDefaultAsync(m => m.Id == id);
            }
        }

        public async Task<IEnumerable<Pais>> ReadPaises()
        {
            using (var context = sqLiteDbFake.GetDbContext())
            {
                return await context.Paises.ToListAsync();
            }
        }

        public async Task<int> UpdatePais(Pais pais)
        {
            using (var context = sqLiteDbFake.GetDbContext())
            {
                context.Entry(pais).State = EntityState.Modified;
                try
                {
                    return await context.SaveChangesAsync();
                }
                catch (Exception)
                {
                    return 0;
                }                
            }
        }

        public async Task<int> DeletePais(int id)
        {
            using (var context = sqLiteDbFake.GetDbContext())
            {
                var pais = context.Paises.SingleOrDefault(m => m.Id == id);                
                try
                {
                    context.Paises.Remove(pais);
                    return await context.SaveChangesAsync();
                }
                catch (Exception)
                {
                    return 0;
                }                
            }
        }

    }

Como podemos ver, el servicio de pruebas PaisesDataServiceFake.cs, simula un entorno de acceso a datos CRUD sobre una base de datos en memoria SqLite in-memory. Todo lo relativo a cómo crear bases de datos en memoria y a la clase SqLiteDbFake.cs, se puede ver en el Post Pruebas unitarias en Entity Framework Core - SqLite in-memory.

La clase de pruebas unitarias (TestClass)

Por último, crearemos las clase de pruebas unitarias (PaisControllerTests.cs) para el controlador Web API PaisController.cs.

    [TestClass]
    public class PaisControllerTests
    {
        // Servicio de acceso a datos Fake.
        private PaisesDataServiceFake paisesDataServiceFake;

        public PaisControllerTests()
        {
            paisesDataServiceFake = new PaisesDataServiceFake();
        }
    
        ...
        ...
        // Aquí todas las pruebas unitarias...    
        ...
        ...

    }

Como vemos en el código, en el constructor de la clase PaisControllerTests instanciamos un objeto del servicio de pruebas PaisesDataServiceFake, el cual utilizaremos posteriormente para realizar las pruebas unitarias (TestMethod).

Programando las pruebas unitarias (TestMethod)

Para obtener una mejor legibilidad del código y una estructura fácilmente mantenible, organizaremos las pruebas en función de su cometido. En nuestro caso las dividiremos en GET, POST, PUT y DELETE.

Debemos tener también en cuenta que los nombres de las pruebas deben de ser lo suficientemente descriptivos como para saber a primera vista cual es su función.

Pruebas GET

        [TestMethod]
        public void GetPaises_Retorna_OkResult_con_todos_los_registros_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            // Act
            var response = paisController.GetPaises().Result;
            // Assert
            // El 'Response' es del tipo OkResult y devuelve un Objeto (OkObjectResult).
            Assert.IsInstanceOfType(response, typeof(OkObjectResult));
            // El Objeto devuelto por el OkResult es del tipo IEnumerable<Pais>.
            Assert.IsInstanceOfType((response as OkObjectResult).Value, typeof(IEnumerable<Pais>));
            // El Objeto devuelto por el OkResult (IEnumerable<Pais>) contiene todos los registros de la BD.
            Assert.AreEqual(3, ((response as OkObjectResult).Value as IEnumerable<Pais>).Count());
        }
        
        [TestMethod]
        public void GetPais_Retorna_OkResult_con_el_objeto_al_pasar_Id_existente_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            // Act
            var response = paisController.GetPais(1).Result;
            // Assert
            // El 'Response' es del tipo OkResult y devuelve un Objeto (OkObjectResult).
            Assert.IsInstanceOfType(response, typeof(OkObjectResult));
            // El Objeto devuelto por el OkResult es del tipo Pais.
            Assert.IsInstanceOfType((response as OkObjectResult).Value, typeof(Pais));
            // El Id del Objeto devuelto por el OkResult (Pais) es igual al Id pasado.
           Assert.AreEqual(1, ((response as OkObjectResult).Value as Pais).Id);
        }

        [TestMethod]
        public void GetPais_Retorna_NotFoundResult_al_pasar_Id_inexistente_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            // Act
            var response = paisController.GetPais(0).Result;
            // Assert
            // El 'Response' es del tipo NotFoundResult.
            Assert.IsInstanceOfType(response, typeof(NotFoundResult));
        }

Como podemos observar en las pruebas, cada vez que instanciemos el controlador del Web API para probar la funcionalidad de alguna de sus acciones, le pasaremos a través del constructor la instancia del servicio de acceso a datos de pruebas (Fake) var paisController = new PaisController(paisesDataServiceFake).

La particularidad en este tipo de pruebas, es que no solo debemos comprobar que las funcionalidades CRUD hacia la base de datos funcionan correctamente (Assert.AreEqual(3, ((response as OkObjectResult).Value as IEnumerable<Pais>).Count())), sino que además las respuestas (Status Codes) de los Métodos Http del servicio sean las correctas y esperadas en cada momento (Assert.IsInstanceOfType(response, typeof(OkObjectResult))).

Para el resto de las pruebas unitarias, el procedimiento será básicamente el mismo.

Pruebas POST

        [TestMethod]
        public void PostPais_Retorna_BadRequest_al_pasar_un_objeto_invalido_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            var paisTest = new Pais
            {
                Habitantes = 1000,
            };
            // Añadir el error para que el ModelState.IsValid lo detecte
            // antes de añadir el registro a la BD.
            paisController.ModelState.AddModelError("Nombre", "Required");
            // Act
            var response = paisController.PostPais(paisTest).Result;
            // Assert
            Assert.IsInstanceOfType(response, typeof(BadRequestObjectResult));
        }

        [TestMethod]
        public void PostPais_Retorna_NotFoundResult_si_el_objeto_no_puede_ser_creado_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            var paisTest = new Pais
            {
                Id = 1,
                Nombre = "PaisTest",
                Habitantes = 1000,
            };
            // Act
            var response = paisController.PostPais(paisTest).Result;
            // Assert
            Assert.IsInstanceOfType(response, typeof(NotFoundResult));
            Assert.AreEqual(3, paisesDataServiceFake.ReadPaises().Result.Count());
        }

        [TestMethod]
        public void PostPais_Retorna_CreatedResponsee_con_objeto_creado_al_pasar_y_crear_un_objeto_valido_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            var paisTest = new Pais
            {
                Nombre = "PaisTest",
                Habitantes = 1000,
            };
            // Act
            var response = paisController.PostPais(paisTest).Result;
            //var item = response.Value as Pais;
            // Assert            
            Assert.IsInstanceOfType(response, typeof(CreatedAtActionResult));
            Assert.IsInstanceOfType((response as CreatedAtActionResult).Value, typeof(Pais));
            Assert.AreEqual("PaisTest", ((response as CreatedAtActionResult).Value as Pais).Nombre);
            Assert.AreEqual(4, paisesDataServiceFake.ReadPaises().Result.Count());
        }

Pruebas PUT

        [TestMethod]
        public void PutPais_Retorna_BadRequest_al_pasar_un_objeto_invalido_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            var paisTest = new Pais
            {
                Id = 1,
                Habitantes = 1000,
            };
            // Añadir el error para que el ModelState.IsValid lo detecte
            // antes de añadir el registro a la BD.
            paisController.ModelState.AddModelError("Nombre", "Required");
            // Act
            var response = paisController.PutPais(paisTest.Id, paisTest).Result;
            // Assert
            Assert.IsInstanceOfType(response, typeof(BadRequestObjectResult));
        }

        [TestMethod]
        public void PutPais_Retorna_BadRequest_al_pasar_Id_diferente_a_Id_de_objeto_valido_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            var paisTest = new Pais
            {
                Id = 1,
                Nombre = "PaisTestModificado",
                Habitantes = 1000,
            };
            // Act
            var response = paisController.PutPais(0, paisTest).Result;
            // Assert
            Assert.IsInstanceOfType(response, typeof(BadRequestResult));
        }

        [TestMethod] 
        public void PutPais_Retorna_NotFoundResult_si_el_objeto_no_puede_ser_modificado_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            var paisTest = new Pais
            {
                Id = 10,
                Nombre = "PaisTestModificado",
                Habitantes = 1000,
            };
            // Act
            var response = paisController.PutPais(paisTest.Id, paisTest).Result;
            // Assert
            Assert.IsInstanceOfType(response, typeof(NotFoundResult));
        }

        [TestMethod] 
        public void PutPais_Retorna_NoContentResponse_si_el_objeto_es_modificado_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            var paisTest = new Pais
            {
                Id = 1,
                Nombre = "PaisTestModificado",
                Habitantes = 1000,
            };
            // Act
            var response = paisController.PutPais(paisTest.Id, paisTest).Result;
            var pais = paisesDataServiceFake.ReadPais(1).Result;
            // Assert
            Assert.IsInstanceOfType(response, typeof(NoContentResult));
            Assert.AreEqual("PaisTestModificado", pais.Nombre);
        }

Pruebas DELETE

        [TestMethod]
        public void DeletePais_Retorna_NotFoundResult_si_el_objeto_no_puede_ser_eliminado_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            // Act
            var response = paisController.DeletePais(10).Result;
            // Assert
            Assert.IsInstanceOfType(response, typeof(NotFoundResult));
            Assert.AreEqual(3, paisesDataServiceFake.ReadPaises().Result.Count());
        }

        [TestMethod]
        public void DeletePais_Retorna_OkResult_si_el_objeto_es_eliminado_Async()
        {
            // Arrange
            var paisController = new PaisController(paisesDataServiceFake);
            // Act
            var response = paisController.DeletePais(1).Result;
            // Assert
            Assert.IsInstanceOfType(response, typeof(OkResult));
            Assert.AreEqual(2, paisesDataServiceFake.ReadPaises().Result.Count());
        }

 

   EtiquetasPruebas Unitarias .NET Core Web API

  Compartir


  Nuevo comentario

El campo Comentario es obligatorio.
El campo Nombre es obligatorio.

  Comentarios

Alex Alex

excelente blog, me ayudo mucho. gracias!!!


Utilizamos cookies propias y de terceros para mejorar nuestros servicios y ofrecerle una mejor experiencia de navegación. Si continúa navegando consideramos que acepta su uso. Más información   Acepto