Compartir vía


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

Por Tom FitzMacken

Descargar el proyecto completado

En esta guía y en la aplicación, se muestra cómo crear pruebas unitarias para la aplicación de Web API 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 funcionen con Entity Framework.

Para obtener una introducción a las pruebas unitarias con ASP.NET Web API, 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 Web API. 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 Adición del controlador.

Requisitos previos

Visual Studio 2017 (edición Community, Professional o Enterprise)

Descarga de código

Descargue el proyecto completado. El proyecto descargable incluye el código de las pruebas unitarias de este tema y del tema Pruebas unitarias de ASP.NET Web API 2.

Creación de aplicación con 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 de ASP.NET llamada StoreApp.

En la ventana Nuevo proyecto de ASP.NET, seleccione la plantilla Vacía y agregue las carpetas y referencias básicas de Web API. Seleccione la opción Agregar pruebas unitarias. Se asigna automáticamente el nombre StoreApp.Tests al proyecto de prueba unitaria. Puede conservar este nombre.

create unit test project

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 llamado 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 scaffolding. Seleccione Controlador de Web API 2 con acciones mediante Entity Framework.

add new controller

Establezca los valores siguientes:

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

specify controller

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 que 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.

Adición de la inserció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 llamado 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 ficticio al realizar las pruebas.

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

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

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 rompen 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 prueba 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 la 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 Vacía para crear una aplicación, el proyecto de prueba unitaria (StoreApp.Tests) no incluye los paquetes NuGet instalados. Otras plantillas, como la plantilla Web API, incluyen algunos paquetes NuGet en el proyecto de prueba unitaria. Para este tutorial, debe incluir el paquete Entity Framework y el paquete 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. Debe seleccionar el proyecto StoreApp.Tests para agregar los paquetes a ese proyecto.

manage packages

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

add Entity Framework

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

install web api core package

Cierre la ventana Administrar paquetes NuGet.

Creación de un contexto de pruebas

Agregue una clase llamada 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 siguiente código.

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 llamada 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 llamada 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() { }
    }
}

Crear pruebas

De manera predeterminada, el proyecto de prueba incluye un archivo de prueba vacío llamado 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 llamada TestProductController al proyecto de prueba. Reemplace el código por el siguiente código.

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

Ejecutar pruebas

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

run tests

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

test results