Simulazione di Entity Framework quando unit test API Web ASP.NET 2

di Tom FitzMacken

Scaricare il progetto completato

Questa guida e l'applicazione illustrano come creare unit test per l'applicazione API Web 2 che usa Entity Framework. Illustra come modificare il controller con scaffolding per abilitare il passaggio di un oggetto contesto per il test e come creare oggetti di test che funzionano con Entity Framework.

Per un'introduzione agli unit test con API Web ASP.NET, vedere Unit Testing con API Web ASP.NET 2.

Questa esercitazione presuppone che si abbia familiarità con i concetti di base di API Web ASP.NET. Per un'esercitazione introduttiva, vedere Introduzione con API Web ASP.NET 2.

Versioni software usate nell'esercitazione

Contenuto dell'argomento

In questo argomento sono incluse le sezioni seguenti:

Se sono già stati completati i passaggi in Unit Testing con API Web ASP.NET 2, è possibile passare alla sezione Aggiungere il controller.

Prerequisiti

Visual Studio 2017 Community, Professional o Enterprise Edition

Scaricare il codice

Scaricare il progetto completato. Il progetto scaricabile include il codice di unit test per questo argomento e per l'argomento Unit Testing API Web ASP.NET 2.

Creare un'applicazione con un progetto di unit test

È possibile creare un progetto di unit test durante la creazione dell'applicazione o aggiungere un progetto di unit test a un'applicazione esistente. Questa esercitazione illustra la creazione di un progetto di unit test durante la creazione dell'applicazione.

Creare una nuova applicazione Web ASP.NET denominata StoreApp.

Nelle finestre Nuovo progetto ASP.NET selezionare il modello Vuoto e aggiungere cartelle e riferimenti di base per l'API Web. Selezionare l'opzione Aggiungi unit test . Il progetto di unit test viene denominato automaticamente StoreApp.Tests. È possibile mantenere questo nome.

creare un progetto di unit test

Dopo aver creato l'applicazione, si noterà che contiene due progetti: StoreApp e StoreApp.Tests.

Creare la classe del modello

Nel progetto StoreApp aggiungere un file di classe alla cartella Models denominataProduct.cs. Sostituire il contenuto del file con il codice seguente.

using System;

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

Compilare la soluzione.

Aggiungere il controller

Fare clic con il pulsante destro del mouse sulla cartella Controllers e scegliere Aggiungi e Nuovo elemento con scaffolding. Selezionare Web API 2 Controller con azioni usando Entity Framework.

aggiungere un nuovo controller

Impostare i valori seguenti:

  • Nome controller: ProductController
  • Classe modello: Product
  • Classe contesto dati: [Selezionare il pulsante Nuovo contesto dati che inserisce i valori visualizzati di seguito]

specificare il controller

Fare clic su Aggiungi per creare il controller con codice generato automaticamente. Il codice include metodi per la creazione, il recupero, l'aggiornamento e l'eliminazione di istanze della classe Product. Il codice seguente illustra il metodo per aggiungere un oggetto Product. Si noti che il metodo restituisce un'istanza di 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 è una delle nuove funzionalità dell'API Web 2 e semplifica lo sviluppo di unit test.

Nella sezione successiva verrà personalizzato il codice generato per facilitare il passaggio di oggetti di test al controller.

Aggiungere l'inserimento delle dipendenze

Attualmente, la classe ProductController è hardcoded per usare un'istanza della classe StoreAppContext. Si userà un modello denominato inserimento delle dipendenze per modificare l'applicazione e rimuovere tale dipendenza hardcoded. Interrompendo questa dipendenza, è possibile passare un oggetto fittizio durante il test.

Fare clic con il pulsante destro del mouse sulla cartella Models e aggiungere una nuova interfaccia denominata IStoreAppContext.

Sostituire il codice con il seguente.

using System;
using System.Data.Entity;

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

Aprire il file StoreAppContext.cs e apportare le modifiche evidenziate seguenti. Le modifiche importanti da notare sono:

  • La classe StoreAppContext implementa l'interfaccia IStoreAppContext
  • Il metodo MarkAsModified viene implementato
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;
        }
    }
}

Aprire il file ProductController.cs. Modificare il codice esistente in modo che corrisponda al codice evidenziato. Queste modifiche interrompono la dipendenza da StoreAppContext e consentono ad altre classi di passare un oggetto diverso per la classe di contesto. Questa modifica consentirà di passare un contesto di test durante gli unit test.

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
}

È necessario apportare un'altra modifica in ProductController. Nel metodo PutProduct sostituire la riga che imposta lo stato dell'entità su modificato con una chiamata al metodo 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
}

Compilare la soluzione.

A questo momento è possibile configurare il progetto di test.

Installare pacchetti NuGet nel progetto di test

Quando si usa il modello Vuoto per creare un'applicazione, il progetto di unit test (StoreApp.Tests) non include pacchetti NuGet installati. Altri modelli, ad esempio il modello API Web, includono alcuni pacchetti NuGet nel progetto di unit test. Per questa esercitazione, è necessario includere il pacchetto Entity Framework e il pacchetto Microsoft API Web ASP.NET 2 Core al progetto di test.

Fare clic con il pulsante destro del mouse sul progetto StoreApp.Tests e scegliere Gestisci pacchetti NuGet. È necessario selezionare il progetto StoreApp.Tests per aggiungere i pacchetti al progetto.

gestire i pacchetti

Dai pacchetti online trovare e installare il pacchetto EntityFramework (versione 6.0 o successiva). Se sembra che il pacchetto EntityFramework sia già installato, è possibile che sia stato selezionato il progetto StoreApp anziché il progetto StoreApp.Tests.

aggiungere Entity Framework

Trovare e installare il pacchetto Microsoft API Web ASP.NET 2 Core.

Installare il pacchetto di base dell'API Web

Chiudere la finestra Gestisci pacchetti NuGet.

Creare un contesto di test

Aggiungere una classe denominata TestDbSet al progetto di test. Questa classe funge da classe di base per il set di dati di test. Sostituire il codice con il seguente.

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

Aggiungere una classe denominata TestProductDbSet al progetto di test che contiene il codice seguente.

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

Aggiungere una classe denominata TestStoreAppContext e sostituire il codice esistente con il codice seguente.

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

Creare test

Per impostazione predefinita, il progetto di test include un file di test vuoto denominato UnitTest1.cs. Questo file mostra gli attributi usati per creare metodi di test. Per questa esercitazione, è possibile eliminare questo file perché si aggiungerà una nuova classe di test.

Aggiungere una classe denominata TestProductController al progetto di test. Sostituire il codice con il seguente.

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

Esecuzione dei test

A questo momento è possibile eseguire i test. Verrà testato tutto il metodo contrassegnato con l'attributo TestMethod . Dalla voce di menu Test eseguire i test.

eseguire test

Aprire la finestra Esplora test e notare i risultati dei test.

risultati dei test