Freigeben über


Mocken von Entity Framework beim Unit-Testen von ASP.NET Web API 2

von Tom FitzMacken

Abgeschlossenes Projekt herunterladen

Diese Anleitung und Anwendung veranschaulichen, wie Komponententests für Ihre Web-API 2-Anwendung erstellt werden, die das Entity Framework verwendet. Es zeigt, wie Sie den Gerüstcontroller ändern, um das Übergeben eines Kontextobjekts für Tests und das Erstellen von Testobjekten zu ermöglichen, die mit Entity Framework funktionieren.

Eine Einführung in Komponententests mit ASP.NET Web-API finden Sie unter Komponententests mit ASP.NET Web-API 2.

In diesem Lernprogramm wird davon ausgegangen, dass Sie mit den grundlegenden Konzepten ASP.NET Web-API vertraut sind. Ein einführendes Lernprogramm finden Sie unter "Erste Schritte mit ASP.NET Web-API 2".

Im Lernprogramm verwendete Softwareversionen

In diesem Thema

Dieses Thema enthält folgende Abschnitte:

Wenn Sie die Schritte in Komponententests mit ASP.NET Web-API 2 bereits abgeschlossen haben, können Sie zum Abschnitt "Controller hinzufügen" springen.

Voraussetzungen

Visual Studio 2017 Community, Professional oder Enterprise Edition

Code herunterladen

Laden Sie das fertige Projekt herunter. Das herunterladbare Projekt enthält Komponententestcode für dieses Thema und für das Thema "Komponententests" ASP.NET Web-API 2 .

Erstellen einer Anwendung mit Komponententestprojekt

Sie können entweder ein Komponententestprojekt erstellen, wenn Sie Ihre Anwendung erstellen oder einer vorhandenen Anwendung ein Komponententestprojekt hinzufügen. Dieses Lernprogramm zeigt das Erstellen eines Komponententestprojekts beim Erstellen der Anwendung.

Erstellen Sie eine neue ASP.NET Webanwendung mit dem Namen StoreApp.

Wählen Sie in den Fenstern "Neu ASP.NET Projekt" die Vorlage "Leer " aus, und fügen Sie Ordner und Kernverweise für die Web-API hinzu. Wählen Sie die Option " Komponententests hinzufügen " aus. Das Komponententestprojekt heißt automatisch "StoreApp.Tests". Sie können diesen Namen beibehalten.

Erstellen eines Komponententestprojekts

Nach dem Erstellen der Anwendung sehen Sie, dass sie zwei Projekte enthält: StoreApp und StoreApp.Tests.

Erstellen der Modellklasse

Fügen Sie in Ihrem StoreApp-Projekt dem Ordner "Modelle" eine Klassendatei mit dem Namen Product.cs hinzu. Ersetzen Sie den Inhalt der Datei durch den folgenden Code.

using System;

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

Erstellen Sie die Lösung.

Hinzufügen des Controllers

Klicken Sie mit der rechten Maustaste auf den Ordner "Controller", und wählen Sie "Neues Gerüstelementhinzufügen" aus. Wählen Sie den Web-API 2-Controller mit Aktionen mithilfe von Entity Framework aus.

neuen Controller hinzufügen

Legen Sie die folgenden Werte fest:

  • Controllername: ProductController
  • Modellklasse: Produkt
  • Data-Kontextklasse: [Wählen Sie die Schaltfläche Neuer Datenkontext aus, die die unten aufgeführten Werte ausfüllt]

Controller angeben

Klicken Sie auf "Hinzufügen" , um den Controller mit automatisch generierten Code zu erstellen. Der Code enthält Methoden zum Erstellen, Abrufen, Aktualisieren und Löschen von Instanzen der Product-Klasse. Der folgende Code zeigt die Methode zum Hinzufügen eines Produkts. Beachten Sie, dass die Methode eine Instanz von IHttpActionResult zurückgibt.

// 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 ist eines der neuen Features in Web API 2 und vereinfacht die Komponententestentwicklung.

Im nächsten Abschnitt passen Sie den generierten Code an, um das Übergeben von Testobjekten an den Controller zu erleichtern.

Hinzufügen einer Abhängigkeitsinjektion

Derzeit ist die ProductController-Klasse hartcodiert, um eine Instanz der StoreAppContext-Klasse zu verwenden. Sie verwenden ein Muster namens Abhängigkeitsinjektion, um Ihre Anwendung zu ändern und diese hartcodierte Abhängigkeit zu entfernen. Indem Sie diese Abhängigkeit unterbrechen, können Sie beim Testen ein Pseudoobjekt übergeben.

Klicken Sie mit der rechten Maustaste auf den Ordner "Models ", und fügen Sie eine neue Schnittstelle namens "IStoreAppContext" hinzu.

Ersetzen Sie den Code durch den folgenden Code.

using System;
using System.Data.Entity;

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

Öffnen Sie die StoreAppContext.cs Datei, und nehmen Sie die folgenden hervorgehobenen Änderungen vor. Die wichtigen Änderungen, die zu beachten sind:

  • StoreAppContext-Klasse implementiert IStoreAppContext-Schnittstelle
  • MarkAsModified-Methode ist implementiert.
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;
        }
    }
}

Öffnen Sie die datei ProductController.cs. Ändern Sie den vorhandenen Code so, dass er dem hervorgehobenen Code entspricht. Diese Änderungen unterbrechen die Abhängigkeit von StoreAppContext und ermöglichen es anderen Klassen, ein anderes Objekt für die Kontextklasse zu übergeben. Diese Änderung ermöglicht es Ihnen, während komponententests einen Testkontext zu bestehen.

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
}

Es gibt eine weitere Änderung, die Sie in ProductController vornehmen müssen. Ersetzen Sie in der PutProduct-Methode die Zeile, mit der der Entitätsstatus durch einen Aufruf der MarkAsModified-Methode geändert wird.

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

Erstellen Sie die Lösung.

Jetzt können Sie das Testprojekt einrichten.

Installieren von NuGet-Paketen im Testprojekt

Wenn Sie die Leere Vorlage zum Erstellen einer Anwendung verwenden, enthält das Komponententestprojekt (StoreApp.Tests) keine installierten NuGet-Pakete. Andere Vorlagen, z. B. die Web-API-Vorlage, enthalten einige NuGet-Pakete im Komponententestprojekt. In diesem Lernprogramm müssen Sie das Entity Framework-Paket und das Microsoft ASP.NET Web API 2 Core-Paket in das Testprojekt einschließen.

Klicken Sie mit der rechten Maustaste auf das Projekt "StoreApp.Tests", und wählen Sie "NuGet-Pakete verwalten" aus. Sie müssen das Projekt StoreApp.Tests auswählen, um dem Projekt die Pakete hinzuzufügen.

Verwalten von Paketen

Suchen und installieren Sie das EntityFramework-Paket (Version 6.0 oder höher) aus den Onlinepaketen. Wenn das EntityFramework-Paket bereits installiert ist, haben Sie möglicherweise das StoreApp-Projekt anstelle des StoreApp.Tests-Projekts ausgewählt.

Entitätsframework hinzufügen

Suchen und installieren Sie das Microsoft ASP.NET Web API 2 Core-Paket.

Installieren des Web-API-Kernpakets

Schließen Sie das Fenster "NuGet-Pakete verwalten".

Testkontext erstellen

Fügen Sie dem Testprojekt eine Klasse mit dem Namen TestDbSet hinzu. Diese Klasse dient als Basisklasse für Ihren Testdatensatz. Ersetzen Sie den Code durch den folgenden Code.

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

Fügen Sie dem Testprojekt eine Klasse namens TestProductDbSet hinzu, die den folgenden Code enthält.

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

Fügen Sie eine Klasse namens TestStoreAppContext hinzu, und ersetzen Sie den vorhandenen Code durch den folgenden Code.

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

Erstellen von Tests

Ihr Testprojekt enthält standardmäßig eine leere Testdatei mit dem Namen UnitTest1.cs. Diese Datei zeigt die Attribute an, die Sie zum Erstellen von Testmethoden verwenden. In diesem Lernprogramm können Sie diese Datei löschen, da Sie eine neue Testklasse hinzufügen.

Fügen Sie dem Testprojekt eine Klasse namens TestProductController hinzu. Ersetzen Sie den Code durch den folgenden Code.

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

Tests ausführen

Jetzt können Sie die Tests ausführen. Alle Methoden, die mit dem TestMethod-Attribut gekennzeichnet sind, werden getestet. Führen Sie im Menüelement "Test " die Tests aus.

Ausführen von Tests

Öffnen Sie das Fenster "Test-Explorer ", und beachten Sie die Ergebnisse der Tests.

Testergebnisse