Simulación de Entity Framework al realizar pruebas unitarias ASP.NET Web API 2

por Tom FitzMacken

Descargar el proyecto completado

En esta guía y aplicación se muestra cómo crear pruebas unitarias para la aplicación de Api web 2 que usa Entity Framework. Muestra cómo modificar el controlador con scaffolding para permitir pasar un objeto de contexto para las pruebas y cómo crear objetos de prueba que funcionan con Entity Framework.

Para obtener una introducción a las pruebas unitarias con ASP.NET API web, consulte Pruebas unitarias con ASP.NET Web API 2.

En este tutorial se da por supuesto que está familiarizado con los conceptos básicos de ASP.NET API web. Para ver un tutorial introductorio, consulte Introducción a ASP.NET Web API 2.

Versiones de software usadas en el tutorial

En este tema

Este tema contiene las siguientes secciones:

Si ya ha completado los pasos descritos en Pruebas unitarias con ASP.NET Web API 2, puede ir directamente a la sección Agregar el controlador.

Prerrequisitos

Visual Studio 2017 Community, Professional o Enterprise Edition

Descargar código

Descargue el proyecto completado. El proyecto descargable incluye código de prueba unitaria para este tema y para el tema Unit Testing ASP.NET Web API 2 .

Creación de una aplicación con un proyecto de prueba unitaria

Puede crear un proyecto de prueba unitaria al crear la aplicación o agregar un proyecto de prueba unitaria a una aplicación existente. En este tutorial se muestra cómo crear un proyecto de prueba unitaria al crear la aplicación.

Cree una aplicación web ASP.NET denominada StoreApp.

En las ventanas New ASP.NET Project (Nuevo proyecto de ASP.NET), seleccione la plantilla Empty (Vacía ) y agregue carpetas y referencias principales para Web API. Seleccione la opción Agregar pruebas unitarias . El proyecto de prueba unitaria se denomina automáticamente StoreApp.Tests. Puede conservar este nombre.

creación de un proyecto de prueba unitaria

Después de crear la aplicación, verá que contiene dos proyectos: StoreApp y StoreApp.Tests.

Creación de la clase de modelo

En el proyecto StoreApp, agregue un archivo de clase a la carpeta Models denominada Product.cs. Reemplace el contenido del archivo por el código siguiente.

using System;

namespace StoreApp.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

Compile la solución.

Agregar el controlador

Haga clic con el botón derecho en la carpeta Controladores y seleccione Agregar y Nuevo Elemento con Scaffold. Seleccione Controlador de Web API 2 con acciones mediante Entity Framework.

agregar nuevo controlador

Establezca los siguientes valores:

  • Nombre del controlador: ProductController
  • Clase de modelo: Product
  • Clase de contexto de datos: [Seleccionar nuevo botón de contexto de datos que rellena los valores que se muestran a continuación]

especificar controlador

Haga clic en Agregar para crear el controlador con código generado automáticamente. El código incluye métodos para crear, recuperar, actualizar y eliminar instancias de la clase Product. En el código siguiente se muestra el método para agregar un producto. Observe que el método devuelve una instancia de IHttpActionResult.

// POST api/Product
[ResponseType(typeof(Product))]
public IHttpActionResult PostProduct(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    db.Products.Add(product);
    db.SaveChanges();

    return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
}

IHttpActionResult es una de las nuevas características de Web API 2 y simplifica el desarrollo de pruebas unitarias.

En la sección siguiente, personalizará el código generado para facilitar el paso de objetos de prueba al controlador.

Añadir inyección de dependencias

Actualmente, la clase ProductController está codificada de forma rígida para usar una instancia de la clase StoreAppContext. Usará un patrón denominado inserción de dependencias para modificar la aplicación y quitar esa dependencia codificada de forma rígida. Al romper esta dependencia, puede pasar un objeto simulado durante las pruebas.

Haga clic con el botón derecho en la carpeta Models y agregue una nueva interfaz denominada IStoreAppContext.

Reemplace el código por el código siguiente.

using System;
using System.Data.Entity;

namespace StoreApp.Models
{
    public interface IStoreAppContext : IDisposable
    {
        DbSet<Product> Products { get; }
        int SaveChanges();
        void MarkAsModified(Product item);    
    }
}

Abra el archivo StoreAppContext.cs y realice los siguientes cambios resaltados. Los cambios importantes que debe tener en cuenta son:

  • La clase StoreAppContext implementa la interfaz IStoreAppContext
  • Se implementa el método MarkAsModified
using System;
using System.Data.Entity;

namespace StoreApp.Models
{
    public class StoreAppContext : DbContext, IStoreAppContext
    {
        public StoreAppContext() : base("name=StoreAppContext")
        {
        }

        public DbSet<Product> Products { get; set; }
    
        public void MarkAsModified(Product item)
        {
            Entry(item).State = EntityState.Modified;
        }
    }
}

Abra el archivo ProductController.cs. Cambie el código existente para que coincida con el código resaltado. Estos cambios interrumpen la dependencia en StoreAppContext y permiten que otras clases pasen un objeto diferente para la clase de contexto. Este cambio le permitirá pasar un contexto de test durante las pruebas unitarias.

public class ProductController : ApiController
{
    // modify the type of the db field
    private IStoreAppContext db = new StoreAppContext();

    // add these constructors
    public ProductController() { }

    public ProductController(IStoreAppContext context)
    {
        db = context;
    }
    // rest of class not shown
}

Hay un cambio más que debe realizar en ProductController. En el método PutProduct , reemplace la línea que establece el estado de entidad que se va a modificar con una llamada al método MarkAsModified.

// PUT api/Product/5
public IHttpActionResult PutProduct(int id, Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (id != product.Id)
    {
        return BadRequest();
    }

    //db.Entry(product).State = EntityState.Modified;
    db.MarkAsModified(product);
    
    // rest of method not shown
}

Compile la solución.

Ya está listo para configurar el proyecto de prueba.

Instalación de paquetes NuGet en el proyecto de prueba

Cuando se usa la plantilla Empty para crear una aplicación, el proyecto de prueba unitaria (StoreApp.Tests) no incluye ningún paquete NuGet instalado. Otras plantillas, como la plantilla de API web, incluyen algunos paquetes NuGet en el proyecto de prueba unitaria. Para este tutorial, debe incluir el paquete de Entity Framework y el paquete de Microsoft ASP.NET Web API 2 Core en el proyecto de prueba.

Haga clic con el botón derecho en el proyecto StoreApp.Tests y seleccione Administrar paquetes NuGet. Debes seleccionar el proyecto StoreApp.Tests para agregar los paquetes a ese proyecto.

administrar paquetes

En los paquetes en línea, busque e instale el paquete EntityFramework (versión 6.0 o posterior). Si parece que el paquete EntityFramework ya está instalado, es posible que haya seleccionado el proyecto StoreApp en lugar del proyecto StoreApp.Tests.

agregar Entity Framework

Busque e instale el paquete microsoft ASP.NET Web API 2 Core.

instalación del paquete principal de api web

Cierre la ventana Administrar paquetes NuGet.

Creación de contexto de prueba

Agregue una clase denominada TestDbSet al proyecto de prueba. Esta clase actúa como clase base para el conjunto de datos de prueba. Reemplace el código por el código siguiente.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Linq;

namespace StoreApp.Tests
{
    public class TestDbSet<T> : DbSet<T>, IQueryable, IEnumerable<T>
        where T : class
    {
        ObservableCollection<T> _data;
        IQueryable _query;

        public TestDbSet()
        {
            _data = new ObservableCollection<T>();
            _query = _data.AsQueryable();
        }

        public override T Add(T item)
        {
            _data.Add(item);
            return item;
        }

        public override T Remove(T item)
        {
            _data.Remove(item);
            return item;
        }

        public override T Attach(T item)
        {
            _data.Add(item);
            return item;
        }

        public override T Create()
        {
            return Activator.CreateInstance<T>();
        }

        public override TDerivedEntity Create<TDerivedEntity>()
        {
            return Activator.CreateInstance<TDerivedEntity>();
        }

        public override ObservableCollection<T> Local
        {
            get { return new ObservableCollection<T>(_data); }
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        System.Linq.Expressions.Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return _query.Provider; }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }
    }
}

Agregue una clase denominada TestProductDbSet al proyecto de prueba que contiene el código siguiente.

using System;
using System.Linq;
using StoreApp.Models;

namespace StoreApp.Tests
{
    class TestProductDbSet : TestDbSet<Product>
    {
        public override Product Find(params object[] keyValues)
        {
            return this.SingleOrDefault(product => product.Id == (int)keyValues.Single());
        }
    }
}

Agregue una clase denominada TestStoreAppContext y reemplace el código existente por el código siguiente.

using System;
using System.Data.Entity;
using StoreApp.Models;

namespace StoreApp.Tests
{
    public class TestStoreAppContext : IStoreAppContext 
    {
        public TestStoreAppContext()
        {
            this.Products = new TestProductDbSet();
        }

        public DbSet<Product> Products { get; set; }

        public int SaveChanges()
        {
            return 0;
        }

        public void MarkAsModified(Product item) { }
        public void Dispose() { }
    }
}

Creación de pruebas

De forma predeterminada, el proyecto de prueba incluye un archivo de prueba vacío denominado UnitTest1.cs. Este archivo muestra los atributos que se usan para crear métodos de prueba. En este tutorial, puede eliminar este archivo porque agregará una nueva clase de prueba.

Agregue una clase denominada TestProductController al proyecto de prueba. Reemplace el código por el código siguiente.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Web.Http.Results;
using System.Net;
using StoreApp.Models;
using StoreApp.Controllers;

namespace StoreApp.Tests
{
    [TestClass]
    public class TestProductController
    {
        [TestMethod]
        public void PostProduct_ShouldReturnSameProduct()
        {
            var controller = new ProductController(new TestStoreAppContext());

            var item = GetDemoProduct();

            var result =
                controller.PostProduct(item) as CreatedAtRouteNegotiatedContentResult<Product>;

            Assert.IsNotNull(result);
            Assert.AreEqual(result.RouteName, "DefaultApi");
            Assert.AreEqual(result.RouteValues["id"], result.Content.Id);
            Assert.AreEqual(result.Content.Name, item.Name);
        }

        [TestMethod]
        public void PutProduct_ShouldReturnStatusCode()
        {
            var controller = new ProductController(new TestStoreAppContext());

            var item = GetDemoProduct();

            var result = controller.PutProduct(item.Id, item) as StatusCodeResult;
            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(StatusCodeResult));
            Assert.AreEqual(HttpStatusCode.NoContent, result.StatusCode);
        }

        [TestMethod]
        public void PutProduct_ShouldFail_WhenDifferentID()
        {
            var controller = new ProductController(new TestStoreAppContext());

            var badresult = controller.PutProduct(999, GetDemoProduct());
            Assert.IsInstanceOfType(badresult, typeof(BadRequestResult));
        }

        [TestMethod]
        public void GetProduct_ShouldReturnProductWithSameID()
        {
            var context = new TestStoreAppContext();
            context.Products.Add(GetDemoProduct());

            var controller = new ProductController(context);
            var result = controller.GetProduct(3) as OkNegotiatedContentResult<Product>;

            Assert.IsNotNull(result);
            Assert.AreEqual(3, result.Content.Id);
        }

        [TestMethod]
        public void GetProducts_ShouldReturnAllProducts()
        {
            var context = new TestStoreAppContext();
            context.Products.Add(new Product { Id = 1, Name = "Demo1", Price = 20 });
            context.Products.Add(new Product { Id = 2, Name = "Demo2", Price = 30 });
            context.Products.Add(new Product { Id = 3, Name = "Demo3", Price = 40 });

            var controller = new ProductController(context);
            var result = controller.GetProducts() as TestProductDbSet;

            Assert.IsNotNull(result);
            Assert.AreEqual(3, result.Local.Count);
        }

        [TestMethod]
        public void DeleteProduct_ShouldReturnOK()
        {
            var context = new TestStoreAppContext();
            var item = GetDemoProduct();
            context.Products.Add(item);

            var controller = new ProductController(context);
            var result = controller.DeleteProduct(3) as OkNegotiatedContentResult<Product>;

            Assert.IsNotNull(result);
            Assert.AreEqual(item.Id, result.Content.Id);
        }

        Product GetDemoProduct()
        {
            return new Product() { Id = 3, Name = "Demo name", Price = 5 };
        }
    }
}

Ejecución de pruebas

Ya está listo para ejecutar las pruebas. Se probará todo el método marcado con el atributo TestMethod . En el elemento de menú Probar , ejecute las pruebas.

ejecutar pruebas

Abra la ventana Explorador de pruebas y observe los resultados de las pruebas.

resultados de pruebas