Udostępnij za pośrednictwem


Pozorowanie platformy Entity Framework podczas testowania jednostkowego ASP.NET internetowego interfejsu API 2

– autor Tom FitzMacken

Pobieranie ukończonego projektu

Te wskazówki i aplikacja przedstawiają sposób tworzenia testów jednostkowych dla aplikacji internetowego interfejsu API 2 korzystającej z programu Entity Framework. Pokazano w nim, jak zmodyfikować kontroler szkieletu, aby umożliwić przekazywanie obiektu kontekstu do testowania oraz tworzenie obiektów testowych, które współpracują z programem Entity Framework.

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

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

Wersje oprogramowania używane w samouczku

W tym temacie:

Ten temat zawiera następujące sekcje:

Jeśli kroki opisane w artykule Testowanie jednostkowe zostały już wykonane za pomocą ASP.NET internetowego interfejsu API 2, możesz przejść do sekcji Dodawanie kontrolera.

Wymagania wstępne

Visual Studio 2017 Community, Professional lub Enterprise edition

Pobieranie kodu

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 testu jednostkowego podczas tworzenia aplikacji.

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

W oknach Nowy ASP.NET Project wybierz pusty szablon i dodaj foldery i odwołania podstawowe dla internetowego interfejsu API. Wybierz opcję Dodaj testy jednostkowe . Projekt testu jednostkowego jest automatycznie nazwany StoreApp.Tests. Możesz zachować tę nazwę.

tworzenie projektu testu jednostkowego

Po utworzeniu aplikacji zobaczysz, że zawiera 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; }
    }
}

Skompiluj rozwiązanie.

Dodawanie kontrolera

Kliknij prawym przyciskiem myszy folder Kontrolery i wybierz pozycję Dodaj i Nowy element szkieletowy. Wybierz pozycję Kontroler internetowego interfejsu API 2 z akcjami przy użyciu programu Entity Framework.

dodawanie nowego kontrolera

Ustaw następujące wartości:

  • Nazwa kontrolera: ProductController
  • Klasa modelu: Product
  • Klasa kontekstu danych: [Wybierz przycisk Nowy kontekst 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 w internetowym interfejsie API 2 i upraszcza tworzenie testów jednostkowych.

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

Dodawanie wstrzykiwania zależności

Obecnie klasa ProductController jest zakodowana w kodzie, aby użyć wystąpienia klasy StoreAppContext. Użyjesz wzorca o nazwie iniekcja zależności, aby zmodyfikować aplikację i usunąć ta zakodowana zależność. Przerywając tę zależność, można przekazać w 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 zanotować, 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 przerywają zależność od klasy StoreAppContext i umożliwiają przekazywanie innych klas w innym obiekcie dla klasy kontekstowej. 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 elemecie 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
}

Skompiluj 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 testu jednostkowego. W tym samouczku 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 polecenie Zarządzaj pakietami NuGet. Aby dodać pakiety do tego projektu, musisz wybrać projekt StoreApp.Tests.

zarządzanie pakietami

Z poziomu pakietów 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. W tym samouczku możesz usunąć ten plik, ponieważ dodasz nową klasę testową.

Dodaj klasę o nazwie TestProductController do projektu testowego. Zastąp kod poniższym 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 };
        }
    }
}

Uruchom testy

Teraz możesz przystąpić do uruchamiania testów. 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