Первая архитектура ASP.NET MVC3 и Entity Framework Code

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

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

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

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

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

Ответ 1

Если вы хотите использовать TDD (или любой другой подход к тестированию с высоким охватом тестирования) и EF вместе, вы должны написать интеграцию или сквозные тесты. Проблема в том, что любой подход с издевательством либо контекста, либо репозитория просто создает тест, который может протестировать вашу логику верхнего уровня (которая использует эти mocks), но не ваше приложение.

Простой пример:

Определить общий репозиторий:

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.

Это был простой пример, но это может произойти с использованием методов в запросах и других распространенных ошибках. Кроме того, это также затрагивает такие методы, как "Добавить", "Обновить", "Удалить", обычно открытые в репозитории. Если вы не напишете макет, который будет точно имитировать поведение контекста EF и ссылочную целостность, вы не будете тестировать свою реализацию.

Другая часть истории - проблемы с Lazy-загрузкой, которые также вряд ли можно обнаружить с помощью модульных тестов против mocks.

Из-за этого вы также должны ввести интеграционные или сквозные тесты, которые будут работать с реальной базой данных, используя реальный контекст 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).
  • Интеграционные тесты помогают тестировать функциональность по нескольким уровням и, как правило, через несколько процессов (приложение, база данных). Вам не нужно иметь их для всего, больше о опыте, чтобы выбрать, где они полезны.
  • Конечные тесты - это что-то вроде проверки истории использования/случая использования/пользователя. Они должны охватывать весь поток требований.

Нет необходимости многократно проверять фету - если вы знаете, что эта функция протестирована в сквозном тесте, вам не нужно записывать интеграционный тест для того же кода. Также, если вы знаете, что метод имеет только один путь выполнения, который покрывается интеграционным тестом, вам не нужно писать unit test для него. Это намного лучше работает с TDD-подходом, где вы начинаете с большого теста (от конца до конца или интеграции) и углубляетесь в модульные тесты.

В зависимости от вашего подхода к разработке вы не должны начинать с нескольких типов тестов с начала, но позже можете их представить, так как ваше приложение станет более сложным. Исключением является TDD/BDD, где вы должны начать использовать, по крайней мере, сквозные и модульные тесты, прежде чем писать одну строку другого кода.

Итак, вы задаете неправильный вопрос. Вопрос не в том, что проще? Вопрос в том, что поможет вам в конце и какая сложность подходит для вашего приложения? Если вы хотите легко протестировать приложение и бизнес-логику, вы должны перенести EF-код на другие классы, которые можно издеваться. Но в то же время вы должны ввести другие типы тестов для обеспечения работы EF-кода.

Я не могу сказать, какой подход подходит вашей среде/проекту/команде/и т.д. Но я могу объяснить пример из моего прошлого проекта:

Я работал над проектом около 5-6 месяцев с двумя коллегами. Проект был основан на ASP.NET MVC 2 + jQuery + EFv4, и он был разработан поэтапно и итеративно. У него было много сложной бизнес-логики и множество сложных запросов к базе данных. Мы начали с общих хранилищ и высокого покрытия кода с помощью модульных тестов + интеграционных тестов для проверки соответствия (простые тесты для вставки, удаления, обновления и выбора объекта). Через несколько месяцев мы обнаружили, что наш подход не работает. У нас было более 1.200 единиц тестов, охват кода около 60% (что не очень хорошо) и много проблем с регрессией. Изменение чего-либо в модели EF может привести к неожиданным проблемам в деталях, которые не были затронуты в течение нескольких недель. Мы обнаружили, что нам не хватает интеграционных тестов или сквозных тестов для нашей прикладной логики. Тот же вывод был сделан в отношении параллельной команды, работавшей над другим проектом, и использование интеграционных тестов рассматривалось как рекомендация для новых проектов.

Ответ 2

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

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

Ответ 3

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

Ответ 4

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