Поделиться через


Мокирование Entity Framework при модульном тестировании ASP.NET веб-API 2

Tom FitzMacken

Скачивание завершенного проекта

В этом руководстве и приложении показано, как создавать модульные тесты для приложения Веб-API 2, использующего Entity Framework. В нем показано, как изменить шаблонный контроллер для включения передачи контекстного объекта для тестирования и создания тестовых объектов, работающих с Entity Framework.

Общие сведения о модульном тестировании с помощью веб-API ASP.NET см. в разделе Модульное тестирование с ASP.NET веб-API 2.

В этом руководстве предполагается, что вы знакомы с основными понятиями веб-API ASP.NET. Вводные руководства см. в статье "Начало работы с ASP.NET веб-API 2".

Версии программного обеспечения, используемые в руководстве

В этом разделе

Этот раздел состоит из следующих подразделов.

Если вы уже выполнили действия в модульном тестировании с помощью веб-API ASP.NET 2, можно перейти к разделу "Добавить контроллер".

Необходимые условия

Visual Studio 2017 Community, Professional или Enterprise Edition

Скачать код

Скачайте завершенный проект. Скачиваемый проект включает код модульного теста для этого раздела и для раздела "Модульное тестирование" ASP.NET веб-API 2 .

Создание приложения с помощью проекта модульного теста

При создании приложения можно создать проект модульного теста или добавить проект модульного теста в существующее приложение. В этом руководстве показано создание проекта модульного теста при создании приложения.

Создайте веб-приложение с именем StoreApp ASP.NET.

В окнах "Новый ASP.NET Проект" выберите пустой шаблон и добавьте папки и основные ссылки на веб-API. Выберите параметр "Добавить модульные тесты ". Проект модульного теста автоматически называется StoreApp.Tests. Это имя можно сохранить.

создание проекта модульного теста

После создания приложения вы увидите, что он содержит два проекта — StoreApp и StoreApp.Tests.

Создание класса модели

В проекте StoreApp добавьте файл класса в папку Models с именем Product.cs. Замените содержимое файла следующим кодом.

using System;

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

Создайте решение.

Добавление контроллера

Щелкните правой кнопкой мыши папку "Контроллеры" и выберите "Добавить " и "Создать шаблонный элемент". Выберите контроллер Web API 2 с действиями, используя Entity Framework.

добавление нового контроллера

Задайте следующие значения:

  • Имя контроллера: ProductController
  • Класс модели: Product
  • Класс контекста данных: [Кнопка "Выбрать новый контекст данных ", которая заполняет значения, приведенные ниже]

указание контроллера

Нажмите кнопку "Добавить ", чтобы создать контроллер с автоматически созданным кодом. Код включает методы для создания, извлечения, обновления и удаления экземпляров класса Product. В следующем коде показан метод для добавления Product. Обратите внимание, что метод возвращает экземпляр 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 — это одна из новых возможностей веб-API 2, а также упрощает разработку модульных тестов.

В следующем разделе вы настроите созданный код, чтобы упростить передачу тестовых объектов контроллеру.

Добавить внедрение зависимостей

В настоящее время класс ProductController жестко закодирован для использования экземпляра класса StoreAppContext. Вы будете использовать шаблон, называемый внедрением зависимостей, чтобы изменить приложение и удалить эту жестко закодированную зависимость. Разбив эту зависимость, можно передать объект макета при тестировании.

Щелкните правой кнопкой мыши папку Models и добавьте новый интерфейс с именем IStoreAppContext.

Замените код следующим кодом.

using System;
using System.Data.Entity;

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

Откройте файл StoreAppContext.cs и внесите следующие выделенные изменения. Важные изменения, на которые следует обратить внимание:

  • Класс StoreAppContext реализует интерфейс IStoreAppContext
  • Метод 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;
        }
    }
}

Откройте файл ProductController.cs. Измените существующий код, чтобы он соответствовал выделенному коду. Эти изменения разбивают зависимость от StoreAppContext и позволяют другим классам передавать другой объект для класса контекста. Это изменение позволит передавать контекст выполнения теста во время модульных тестов.

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
}

В ProductController необходимо внести еще одно изменение. В методе PutProduct замените строку, устанавливающую состояние сущности в модифицированное, на вызов метода 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
}

Создайте решение.

Теперь вы готовы настроить тестовый проект.

Установка пакетов NuGet в тестовом проекте

При использовании пустого шаблона для создания приложения проект модульного теста (StoreApp.Tests) не включает установленные пакеты NuGet. Другие шаблоны, такие как шаблон веб-API, включают некоторые пакеты NuGet в проект модульного теста. Для этого учебного пособия необходимо включить пакет Entity Framework и пакет Microsoft ASP.NET Web API 2 Core в тестовый проект.

Щелкните правой кнопкой мыши проект StoreApp.Tests и выберите пункт "Управление пакетами NuGet". Чтобы добавить пакеты в этот проект, необходимо выбрать проект StoreApp.Tests.

управление пакетами

В пакетах Online найдите и установите пакет EntityFramework (версия 6.0 или более поздняя). Если отображается, что пакет EntityFramework уже установлен, возможно, вы выбрали проект StoreApp вместо проекта StoreApp.Tests.

добавить Entity Framework

Найдите и установите пакет Microsoft ASP.NET Web API 2 Core.

Установка основного пакета веб-API

Закройте окно "Управление пакетами NuGet".

Создание тестового контекста

Добавьте класс с именем TestDbSet в тестовый проект. Этот класс служит базовым классом для тестового набора данных. Замените код следующим кодом.

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

Добавьте класс с именем TestProductDbSet в тестовый проект, содержащий следующий код.

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

Добавьте класс с именем TestStoreAppContext и замените существующий код следующим кодом.

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

Создание тестов

По умолчанию тестовый проект содержит пустой файл теста с именем UnitTest1.cs. В этом файле показаны атрибуты, используемые для создания методов тестирования. В этом руководстве вы можете удалить этот файл, так как вы добавите новый тестовый класс.

Добавьте класс с именем TestProductController в тестовый проект. Замените код следующим кодом.

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

Выполнение тестов

Теперь вы готовы выполнить тесты. Все методы, помеченные атрибутом TestMethod , будут проверены. В пункте меню "Тест" выполните тесты.

Выполнение тестов

Откройте окно обозревателя тестов и обратите внимание на результаты тестов.

Результаты теста