Первая архитектура ASP.NET MVC3 и Entity Framework Code - PullRequest
53 голосов
/ 10 апреля 2011

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

Моя архитектура теперь выглядит так:
Сначала я использую код EF, поэтому я просто создал классы POCO и контекст. Это создает БД и модель.
На более высоком уровне находятся классы бизнес-уровня (провайдеры). Я использую разные провайдеры для каждого домена ... как MemberProvider, RoleProvider, TaskProvider и т. Д., И я делаю новый экземпляр моего DbContext в каждом из этих провайдеров.
Затем я создаю эти провайдеры в своих контроллерах, получаю данные и отправляю их в Views.

Моя первоначальная архитектура включала репозиторий, от которого я избавился, потому что мне сказали, что он просто добавляет сложности, поэтому я не просто использую только EF. Я хотел сделать это .. работать с EF напрямую с контроллеров, но мне пришлось писать тесты, и это было немного сложно с реальной базой данных. Пришлось подделать - как-то издеваться над данными. Поэтому я создал интерфейс для каждого провайдера и сделал фальшивых провайдеров с жестко закодированными данными в списках. И с этим я вернулся к чему-то, где я не уверен, как действовать правильно.

Эти вещи начинают слишком быстро усложняться ... много подходов и "шаблонов" ... это создает слишком много шума и бесполезного кода.

Существует ли какая-либо ПРОСТАЯ и тестируемая архитектура для создания приложений ASP.NET MVC3 и Entity Framework?

Ответы [ 4 ]

93 голосов
/ 10 апреля 2011

Если вы хотите использовать 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, вы получите исключение, потому что перегрузка select с индексами не поддерживается в L2E.

Это был простой пример, но то же самое может случиться с использованием методов в запросах и других распространенныхошибки.Кроме того, это также влияет на такие методы, как Add, Update, Delete, обычно предоставляемые в хранилище.Если вы не пишете макет, который будет точно имитировать поведение контекста EF и ссылочную целостность, вы не будете тестировать свою реализацию.

Еще одна часть истории - проблемы с отложенной загрузкой, которые также трудно обнаружить при модульных тестах на mock.

Из-за этого вам также следует ввести интеграционные или сквозные тесты, которые будутработать с реальной базой данных, используя реальный контекст 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 может привести к неожиданным проблемам в деталях, которые не затрагивались в течение нескольких недель.Мы обнаружили, что нам не хватает интеграционных тестов или сквозных тестов для логики нашего приложения.Тот же вывод был сделан в параллельной команде, работавшей над другим проектом, и использование интеграционных тестов рассматривалось как рекомендация для новых проектов.

13 голосов
/ 10 апреля 2011

Добавляет ли использование шаблона хранилища сложность? По вашему сценарию я так не думаю. Это делает 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

Также вы можете использовать некоторые насмешливые рамки. Самые известные из тех, кого я знаю:

Чтобы увидеть более полный список макетов .NET, посмотрите: https://stackoverflow.com/questions/37359/what-c-mocking-framework-to-use

Другой подход заключается в использовании поставщика базы данных в памяти, например SQLite . Узнайте больше на Есть ли в Entity Framework провайдер в памяти?

Наконец, вот несколько хороших ссылок о модульном тестировании Entity Framework (Некоторые ссылки относятся к Entity Framework 4.0. Но вы поймете идею.):

http://social.msdn.microsoft.com/Forums/en/adodotnetentityframework/thread/678b5871-bec5-4640-a024-71bd4d5c77ff

http://mosesofegypt.net/post/Introducing-Entity-Framework-Unit-Testing-with-TypeMock-Isolator.aspx

Как можно подделать слой моей базы данных в модульном тесте?

2 голосов
/ 10 апреля 2011

Что я делаю, так это использую простой объект 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 ...

1 голос
/ 10 апреля 2011

У меня возникла та же проблема при выборе общего дизайна моего приложения MVC. Этот проект CodePlex Шиджу Варгезе очень помог. Это делается в ASP.net MVC3, EF CodeFirst, а также использует уровень обслуживания и уровень хранилища. Внедрение зависимостей выполняется с использованием Unity. Это просто и очень легко следовать. Это также поддержано приблизительно 4 очень хорошими сообщениями в блоге. Это стоит проверить. И не отказывайтесь от хранилища .. все же.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...