Первая архитектура ASP.NET MVC3 и Entity Framework Code
Мой предыдущий вопрос снова заставил меня задуматься о слоях, репозитории, внедрении зависимостей и подобных вещах.
Моя архитектура теперь выглядит так:
Сначала я использую код EF, поэтому я просто создал классы POCO и контекст. Это создает БД и модель.
На уровень выше классы бизнес-уровня (провайдеры). Я использую разные провайдеры для каждого домена... как MemberProvider, RoleProvider, TaskProvider и т. Д., И я делаю новый экземпляр моего DbContext в каждом из этих провайдеров.
Затем я создаю экземпляры этих провайдеров на своих контроллерах, получаю данные и отправляю их в Views.
Моя первоначальная архитектура включала репозиторий, от которого я избавился, потому что мне сказали, что он просто добавляет сложности, поэтому я не просто использую только EF. Я хотел сделать это.. работая с EF напрямую с контроллеров, но мне пришлось писать тесты, и это было немного сложно с реальной базой данных. Пришлось подделывать - как-то издеваться над данными. Поэтому я создал интерфейс для каждого провайдера и сделал фальшивых провайдеров с жестко закодированными данными в списках. И с этим я вернулся к чему-то, где я не уверен, как действовать правильно.
Эти вещи начинают слишком быстро усложняться... много подходов и "шаблонов"... это создает слишком много шума и бесполезного кода.
Существует ли простая и тестируемая архитектура для создания приложений ASP.NET MVC3 с Entity Framework?
4 ответа
Если вы хотите использовать TDD (или любой другой подход к тестированию с высоким охватом тестирования) и EF вместе, вы должны написать интеграционные или сквозные тесты. Проблема здесь в том, что любой подход с макетом контекста или репозитория просто создает тест, который может проверить вашу логику верхнего уровня (которая использует эти макеты), но не ваше приложение.
Простой пример:
Давайте определим общий репозиторий:
public interface IGenericRepository<TEntity>
{
IQueryable<TEntity> GetQuery();
...
}
И давайте напишем некоторый бизнес-метод:
public IEnumerable<MyEntity> DoSomethingImportant()
{
var data = MyEntityRepo.GetQuery().Select((e, i) => e);
...
}
Теперь, если вы смоделируете репозиторий, вы будете использовать Linq-To-Objects и у вас будет зеленый тест, но если вы запустите приложение с Linq-To-Entities, вы получите исключение, потому что перегрузка выбора с индексами не поддерживается в L2E.
Это был простой пример, но то же самое может случиться с использованием методов в запросах и других распространенных ошибок. Кроме того, это также влияет на такие методы, как Add, Update, Delete, обычно предоставляемые в хранилище. Если вы не пишете макет, который будет точно имитировать поведение контекста EF и ссылочную целостность, вы не будете тестировать свою реализацию.
Другая часть истории - проблемы с отложенной загрузкой, которые также вряд ли можно обнаружить с помощью модульных тестов на макеты.
В связи с этим вам также следует ввести интеграционные или сквозные тесты, которые будут работать с реальной базой данных с использованием реального контекста EF и L2E. Btw. для правильного использования TDD требуется использование сквозных тестов. Для написания сквозных тестов в ASP.NET MVC вы можете использовать WatiN и, возможно, также SpecFlow для BDD, но это действительно добавит много работы, но ваше приложение будет действительно протестировано. Если вы хотите больше узнать о TDD, я рекомендую эту книгу (единственный недостаток - примеры на Java).
Интеграционные тесты имеют смысл, если вы не используете общий репозиторий и скрываете свои запросы в каком-то классе, который не будет выставлен IQueryable
но возвращает напрямую данные.
Пример:
public interface IMyEntityRepository
{
MyEntity GetById(int id);
MyEntity GetByName(string name);
}
Теперь вы можете просто написать интеграционный тест, чтобы протестировать реализацию этого репозитория, потому что запросы скрыты в этом классе и не отображаются на верхних уровнях. Но этот тип хранилища почему-то считается старой реализацией, используемой с хранимыми процедурами. В этой реализации вы потеряете много функций ORM, или вам придется проделать много дополнительной работы - например, добавить шаблон спецификации, чтобы иметь возможность определять запрос на верхнем уровне.
В ASP.NET MVC вы можете частично заменить сквозные тесты интеграционными тестами на уровне контроллера.
Редактировать на основе комментария:
Я не говорю, что вам нужны юнит-тесты, интеграционные тесты и сквозные тесты. Я говорю, что создание проверенных приложений требует гораздо больше усилий. Количество и типы необходимых тестов зависят от сложности вашего приложения, ожидаемого будущего приложения, ваших навыков и умений других членов команды.
Небольшие прямые проекты могут быть созданы без тестов вообще (хорошо, это не очень хорошая идея, но мы все сделали это, и в конце это сработало), но как только проект преодолевает некоторый порог, вы можете обнаружить, что введение новых функций или обслуживание проекта очень трудно, потому что вы никогда не уверены, что это сломает что-то, что уже сработало - это называется регрессией. Лучшая защита от регрессии - хороший набор автоматизированных тестов.
- Модульные тесты помогут вам проверить метод. Такие тесты в идеале должны охватывать все пути выполнения в методе. Эти тесты должны быть очень короткими и простыми в написании - для сложной части можно настроить зависимости (mocks, faktes, stubs).
- Интеграционные тесты помогают тестировать функциональность на нескольких уровнях и обычно на нескольких процессах (приложение, база данных). Вам не нужно иметь их для всего, это больше опыта, чтобы выбрать, где они полезны.
- Сквозные тесты - это что-то вроде проверки варианта использования / пользовательской истории / функции. Они должны охватывать весь поток требований.
Нет необходимости тестировать выборку несколько раз - если вы знаете, что функция тестируется в сквозном тесте, вам не нужно писать интеграционный тест для одного и того же кода. Также, если вы знаете, что метод имеет только один путь выполнения, который покрывается интеграционным тестом, вам не нужно писать для него модульный тест. Это работает намного лучше с подходом TDD, когда вы начинаете с большого теста (сквозного или интеграционного) и углубляетесь в модульные тесты.
В зависимости от вашего подхода к разработке вам не нужно начинать с нескольких типов тестов с самого начала, но вы можете представить их позже, поскольку ваше приложение станет более сложным. Исключением является TDD/BDD, где вы должны начать использовать как минимум сквозные и модульные тесты, прежде чем писать хотя бы одну строку другого кода.
Итак, вы задаете не тот вопрос. Вопрос не в том, что проще? Вопрос в том, что поможет вам в конце и какая сложность подходит для вашего приложения? Если вы хотите, чтобы приложение и бизнес-логика легко тестировались модульно, вы должны связать код EF с некоторыми другими классами, которые можно смоделировать. Но в то же время вы должны ввести другие типы тестов, чтобы убедиться, что код EF работает.
Я не могу сказать вам, какой подход подойдет вашей среде / проекту / команде / и т.д., но я могу объяснить пример из моего прошлого проекта:
Я работал над проектом около 5-6 месяцев с двумя коллегами. Проект был основан на ASP.NET MVC 2 + jQuery + EFv4 и разрабатывался поэтапно и итеративно. У него было много сложной бизнес-логики и много сложных запросов к базе данных. Мы начали с универсальных репозиториев и высокого покрытия кода с модульными тестами + интеграционными тестами для проверки соответствия (простые тесты для вставки, удаления, обновления и выбора сущности). Через несколько месяцев мы обнаружили, что наш подход не работает. У нас было более 1.200 модульных тестов, охват кода около 60% (что не очень хорошо) и множество проблем регрессии. Изменение чего-либо в модели EF может привести к неожиданным проблемам в деталях, которые не затрагивались в течение нескольких недель. Мы обнаружили, что нам не хватает интеграционных тестов или сквозных тестов для логики нашего приложения. Такой же вывод был сделан для параллельной команды, работавшей над другим проектом, и использование интеграционных тестов рассматривалось как рекомендация для новых проектов.
Добавляет ли использование шаблона хранилища сложность? По вашему сценарию я так не думаю. Это делает TDD проще и ваш код более управляемым. Попробуйте использовать шаблон общего хранилища для большего разделения и более чистого кода.
Если вы хотите узнать больше о TDD и шаблонах проектирования в Entity Framework, взгляните на: http://msdn.microsoft.com/en-us/ff714955.aspx
Однако, похоже, что вы ищете подход к пробному тестированию Entity Framework. Одним из решений будет использование метода виртуального затравки для генерации данных при инициализации базы данных. Взгляните на раздел Seed по адресу: http://blogs.msdn.com/b/adonet/archive/2010/09/02/ef-feature-ctp4-dbcontext-and-databases.aspx
Также вы можете использовать некоторые насмешливые рамки. Самые известные из них:
- Rhino Mocks
- Moq
- Typemock (Коммерческий)
Чтобы увидеть более полный список фреймворков для.NET, ознакомьтесь: https://stackru.com/questions/37359/what-c-mocking-framework-to-use
Другой подход заключается в использовании поставщика базы данных в памяти, такого как SQLite. Узнайте больше о том, существует ли поставщик оперативной памяти для Entity Framework?
Наконец, вот несколько хороших ссылок о модульном тестировании Entity Framework (некоторые ссылки относятся к Entity Framework 4.0. Но вы поймете идею.):
http://mosesofegypt.net/post/Introducing-Entity-Framework-Unit-Testing-with-TypeMock-Isolator.aspx
Как можно подделать слой моей базы данных в модульном тесте?
Я использую простой объект ISession и EFSession, который легко подделать в моем контроллере, легко получить доступ с помощью Linq и строго типизировать. Внедрить с помощью DI, используя Ninject.
public interface ISession : IDisposable
{
void CommitChanges();
void Delete<T>(Expression<Func<T, bool>> expression) where T : class, new();
void Delete<T>(T item) where T : class, new();
void DeleteAll<T>() where T : class, new();
T Single<T>(Expression<Func<T, bool>> expression) where T : class, new();
IQueryable<T> All<T>() where T : class, new();
void Add<T>(T item) where T : class, new();
void Add<T>(IEnumerable<T> items) where T : class, new();
void Update<T>(T item) where T : class, new();
}
public class EFSession : ISession
{
DbContext _context;
public EFSession(DbContext context)
{
_context = context;
}
public void CommitChanges()
{
_context.SaveChanges();
}
public void Delete<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new()
{
var query = All<T>().Where(expression);
foreach (var item in query)
{
Delete(item);
}
}
public void Delete<T>(T item) where T : class, new()
{
_context.Set<T>().Remove(item);
}
public void DeleteAll<T>() where T : class, new()
{
var query = All<T>();
foreach (var item in query)
{
Delete(item);
}
}
public void Dispose()
{
_context.Dispose();
}
public T Single<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new()
{
return All<T>().FirstOrDefault(expression);
}
public IQueryable<T> All<T>() where T : class, new()
{
return _context.Set<T>().AsQueryable<T>();
}
public void Add<T>(T item) where T : class, new()
{
_context.Set<T>().Add(item);
}
public void Add<T>(IEnumerable<T> items) where T : class, new()
{
foreach (var item in items)
{
Add(item);
}
}
/// <summary>
/// Do not use this since we use EF4, just call CommitChanges() it does not do anything
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="item"></param>
public void Update<T>(T item) where T : class, new()
{
//nothing needed here
}
Если я хочу переключиться с EF4, скажем, на MongoDB, мне нужно только сделать MongoSession, который реализует ISession...
У меня возникла та же проблема при выборе общего дизайна моего приложения MVC. Этот проект CodePlex Шиджу Варгезе очень помог. Это делается в ASP.net MVC3, EF CodeFirst, а также использует уровень обслуживания и уровень хранилища. Внедрение зависимостей выполняется с использованием Unity. Это просто и очень легко следовать. Это также поддержано приблизительно 4 очень хорошими сообщениями в блоге. Это стоит проверить. И не разочаровывайтесь в хранилище.