Udostępnij za pośrednictwem


Mockowanie Entity Framework podczas testowania jednostkowego w ASP.NET Web API 2.

Autor: Tom FitzMacken

Pobieranie ukończonego projektu

W tych wskazówkach i aplikacji pokazano, jak utworzyć testy jednostkowe dla aplikacji internetowego interfejsu API 2 korzystającej z programu Entity Framework. Przedstawiono w nim sposób modyfikowania szkieletowego kontrolera w celu umożliwienia przekazywania obiektu kontekstu do testowania oraz tworzenia obiektów testowych, które współpracują z programem Entity Framework.

Aby zapoznać się z wprowadzeniem do testowania jednostkowego przy użyciu internetowego interfejsu API ASP.NET, zobacz Artykuł Unit Testing with ASP.NET Web API 2 (Testowanie jednostkowe przy użyciu internetowego interfejsu API 2).

W tym samouczku założono, że znasz podstawowe pojęcia dotyczące internetowego interfejsu API ASP.NET. Aby zapoznać się z samouczkiem wprowadzającym, zobacz Getting Started with ASP.NET Web API 2 (Wprowadzenie do internetowego interfejsu API 2).

Wersje oprogramowania używane w samouczku

W tym temacie

Ten temat zawiera następujące sekcje:

Jeśli wykonałeś już kroki opisane w artykule Testowanie jednostkowe z ASP.NET Web API 2, możesz przejść do sekcji Dodaj kontroler.

Wymagania wstępne

Visual Studio 2017 edycja Community, Professional lub Enterprise

Pobierz kod

Pobierz ukończony projekt. Projekt do pobrania zawiera kod testu jednostkowego dla tego tematu oraz temat Unit Testing ASP.NET Web API 2 .

Tworzenie aplikacji przy użyciu projektu testów jednostkowych

Projekt testu jednostkowego można utworzyć podczas tworzenia aplikacji lub dodać projekt testu jednostkowego do istniejącej aplikacji. W tym samouczku przedstawiono tworzenie projektu testów jednostkowych podczas tworzenia aplikacji.

Utwórz nową aplikację internetową ASP.NET o nazwie StoreApp.

W oknach Nowy Projekt ASP.NET wybierz szablon Pusty i dodaj foldery oraz podstawowe odwołania dla interfejsu API sieci Web. Wybierz opcję Dodaj testy jednostkowe . Projekt testu jednostkowego ma automatycznie nazwę StoreApp.Tests. Możesz zachować tę nazwę.

tworzenie projektu testów jednostkowych

Po utworzeniu aplikacji zobaczysz, że zawiera ona dwa projekty — StoreApp i StoreApp.Tests.

Tworzenie klasy modelu

W projekcie StoreApp dodaj plik klasy do folderu Models o nazwie Product.cs. Zastąp zawartość pliku następującym kodem.

using System;

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

Stwórz rozwiązanie.

Dodawanie kontrolera

Kliknij prawym przyciskiem myszy folder Kontrolery i wybierz polecenie Dodaj i Nowy Element Szkieletowy. Wybierz kontroler Web API 2 z akcjami, za pomocą Entity Framework.

dodawanie nowego kontrolera

Ustaw następujące wartości:

  • Nazwa kontrolera: ProductController
  • Klasa modelu: Product
  • Klasa kontekstu danych: [Wybierz przycisk Nowego kontekstu danych , który wypełnia wartości widoczne poniżej]

określ kontroler

Kliknij przycisk Dodaj , aby utworzyć kontroler z automatycznie wygenerowanym kodem. Kod zawiera metody tworzenia, pobierania, aktualizowania i usuwania wystąpień klasy Product. Poniższy kod przedstawia metodę dodawania produktu. Zwróć uwagę, że metoda zwraca wystąpienie elementu 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 jest jedną z nowych funkcji internetowego interfejsu API 2 i upraszcza opracowywanie testów jednostkowych.

W następnej sekcji dostosujesz wygenerowany kod, aby ułatwić przekazywanie obiektów testowych do kontrolera.

Dodaj wstrzykiwanie zależności

Obecnie klasa ProductController jest zaprogramowana na sztywno, aby używać wystąpienia klasy StoreAppContext. Użyjesz wzorca o nazwie wstrzykiwanie zależności, aby zmodyfikować aplikację i usunąć ta zakodowana zależność. Przerywając tę zależność, można przekazać pozorny obiekt podczas testowania.

Kliknij prawym przyciskiem myszy folder Models i dodaj nowy interfejs o nazwie IStoreAppContext.

Zastąp kod następującym kodem.

using System;
using System.Data.Entity;

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

Otwórz plik StoreAppContext.cs i wprowadź następujące wyróżnione zmiany. Ważne zmiany, które należy zwrócić uwagę, to:

  • Klasa StoreAppContext implementuje interfejs IStoreAppContext
  • Metoda MarkAsModified jest implementowana
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;
        }
    }
}

Otwórz plik ProductController.cs. Zmień istniejący kod, aby był zgodny z wyróżnionym kodem. Te zmiany znoszą zależność od StoreAppContext i umożliwiają innym klasom przekazywanie innego obiektu dla klasy kontekstu. Ta zmiana umożliwi przekazanie kontekstu testowego podczas testów jednostkowych.

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
}

Istnieje jeszcze jedna zmiana, którą należy wprowadzić w productController. W metodzie PutProduct zastąp wiersz, który ustawia stan jednostki na zmodyfikowany za pomocą wywołania metody 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
}

Stwórz rozwiązanie.

Teraz możesz przystąpić do konfigurowania projektu testowego.

Instalowanie pakietów NuGet w projekcie testowym

Jeśli używasz szablonu Empty do utworzenia aplikacji, projekt testu jednostkowego (StoreApp.Tests) nie zawiera żadnych zainstalowanych pakietów NuGet. Inne szablony, takie jak szablon internetowego interfejsu API, zawierają niektóre pakiety NuGet w projekcie testów jednostkowych. Na potrzeby tego samouczka należy dołączyć pakiet Entity Framework i pakiet Microsoft ASP.NET Web API 2 Core do projektu testowego.

Kliknij prawym przyciskiem myszy projekt StoreApp.Tests i wybierz pozycję Zarządzaj pakietami NuGet. Musisz wybrać projekt StoreApp.Tests, aby dodać pakiety do tego projektu.

zarządzanie pakietami

W pakietach online znajdź i zainstaluj pakiet EntityFramework (wersja 6.0 lub nowsza). Jeśli okaże się, że pakiet EntityFramework jest już zainstalowany, być może wybrano projekt StoreApp zamiast projektu StoreApp.Tests.

dodawanie programu Entity Framework

Znajdź i zainstaluj pakiet Microsoft ASP.NET Web API 2 Core.

instalowanie pakietu podstawowego interfejsu API sieci Web

Zamknij okno Zarządzanie pakietami NuGet.

Tworzenie kontekstu testowego

Dodaj klasę o nazwie TestDbSet do projektu testowego. Ta klasa służy jako klasa bazowa dla zestawu danych testowych. Zastąp kod następującym kodem.

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

Dodaj klasę o nazwie TestProductDbSet do projektu testowego zawierającego następujący kod.

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

Dodaj klasę o nazwie TestStoreAppContext i zastąp istniejący kod następującym kodem.

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

Tworzenie testów

Domyślnie projekt testowy zawiera pusty plik testowy o nazwie UnitTest1.cs. Ten plik przedstawia atrybuty używane do tworzenia metod testowych. Na potrzeby tego samouczka możesz usunąć ten plik, ponieważ dodasz nową klasę testową.

Dodaj klasę o nazwie TestProductController do projektu testowego. Zastąp kod następującym kodem.

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

Uruchamianie testów

Teraz możesz uruchomić testy. Wszystkie metody oznaczone atrybutem TestMethod zostaną przetestowane. W elemencie menu Test uruchom testy.

uruchamianie testów

Otwórz okno Eksplorator testów i zwróć uwagę na wyniki testów.

wyniki testu