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

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

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

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

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

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


person Damb    schedule 10.04.2011    source источник
comment
Вы сказали, что репозитории «усложняют» ваше приложение, но я бы сказал, что это начальные «накладные расходы», которые упрощают тестирование. Имитировать несколько репозиториев проще, чем имитировать весь контекст данных.   -  person Omar    schedule 10.04.2011
comment
Да, но мне не нужны эти начальные накладные расходы в моем текущем случае. Я хочу быстро продвигать приложение. Я уже потерял слишком много времени без какого-либо реального прогресса. Добавление репозиториев приносит такие вещи, как IoC, DI и т. Д., И мне придется написать миллионы тестов, прежде чем я перейду к первому просмотру. Я знаю, что это может быть правильное решение, но я не ищу правильного. Я ищу простое (пока еще тестируемое) решение.   -  person Damb    schedule 10.04.2011


Ответы (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. Кстати. Для правильного использования 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 и разрабатывался поэтапно и итеративно. В нем было много сложной бизнес-логики и много сложных запросов к базе данных. Мы начали с общих репозиториев и высокого покрытия кода с помощью модульных тестов + интеграционных тестов для проверки сопоставления (простые тесты для вставки, удаления, обновления и выбора объекта). Через несколько месяцев мы обнаружили, что наш подход не работает. У нас было более 1200 юнит-тестов, покрытие кода около 60% (что не очень хорошо) и множество проблем с регрессией. Изменение чего-либо в модели EF может вызвать неожиданные проблемы в частях, которые не трогались в течение нескольких недель. Мы обнаружили, что нам не хватает интеграционных или сквозных тестов для логики нашего приложения. Такой же вывод был сделан в отношении параллельной команды, работавшей над другим проектом, и использование интеграционных тестов было рассмотрено как рекомендация для новых проектов.

person Ladislav Mrnka    schedule 10.04.2011
comment
Хм ... так что, если я правильно понял, вы говорите, что использование mocks предназначено для модульного тестирования бизнес-логики, и мне нужно выполнять интеграционные тесты с реальным контекстом ef, а также сквозные тесты (я понимаю это как функциональные / пользовательские тесты .. с такими инструментами, как Watin). Но я не понимаю, что такое архитектура. Я рад, что вы намекнули мне, в чем заключаются проблемы, но у меня нет опыта в этой области, поэтому я не знаю, какое решение лучше. И это то, что я ищу здесь. И я говорю о лучшем в более простом или простом смысле. - person Damb; 10.04.2011
comment
Спасибо. Я высоко ценю ваши ответы и объяснения. Я полагаю, что сейчас использую код ef для переноса в другой класс (мои классы Provider). И просто чтобы добавить некоторый контекст в мои вопросы: я создаю простое приложение, построенное на управлении задачами (в контексте проекта) для пользователей (+ экспертная система, которая в любом случае не меняет архитектуру, потому что она просто потребляет данные и обеспечивает простой вывод). Это мой собственный проект (над ним никто не работает), и я не думаю, что у него будет большое будущее. - person Damb; 10.04.2011
comment
@Ladislav: Возможно ли, что инструменты статического анализа кода могут выявлять проблемы, подобные описанной вами (неподдерживаемые методы Linq для Linq to Entities)? Если это так, то вы могли бы устранить класс ошибок, не создавая для них модульных тестов, и получить больше уверенности в том, что ваши макеты в тестах, которые вы пишете, действительно будут работать. Вероятно, это не может решить проблемы ссылочной целостности, но, как вы сказали, об этом можно позаботиться с помощью интеграционных тестов (а не E2E). - person Merlyn Morgan-Graham; 19.09.2011
comment
Как всегда фантастический ответ. Спасибо, Ладислав. - person Mike Chamberlain; 08.04.2012
comment
Тестируемость - это именно та причина, по которой я не выставляю IQueryable из репозиториев. Я получаю более крупные репозитории, но с методами, у которых есть четкие обязанности и которые намного легче подделать. - person Kugel; 31.07.2013

Добавляет ли сложность использование шаблона репозитория? В вашем сценарии я так не думаю. Это упрощает 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/Introduction-Entity-Framework-Unit-Testing-with-TypeMock-Isolator.aspx

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

person Kamyar    schedule 10.04.2011
comment
Спасибо за ваш вклад, есть несколько интересных ссылок. Но мой вопрос на самом деле не столько о тестировании и насмешках. Это больше о поиске простой, быстрой и легкой архитектуры без накладных расходов. Что-то, что вы можете использовать для быстрого и простого тестирования и разработки приложений, не подготавливая строки кода XYZ, чтобы увидеть, действительно ли ваш метод возвращает строковое значение. {Извините немного сарказма.} - person Damb; 10.04.2011
comment
@dampe: Что ж, вместо того, чтобы писать интерфейсы и вручную имитировать данные, я предложил несколько дополнительных решений, которые могут сделать за вас много работы. Еще раз, я бы использовал общий шаблон репозитория в этих случаях и никогда не чувствовал, что это усложняет мое решение. Надеюсь, поможет. - person Kamyar; 10.04.2011
comment
Что касается предложения об общем репозитории, см. Это руководство: asp.net/entity-framework/tutorials/ - person tdykstra; 13.04.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 ...

person VinnyG    schedule 10.04.2011
comment
Спасибо. Я считаю, что делаю что-то подобное ... кроме общей части и части Ninject :) - person Damb; 10.04.2011

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

person Ben    schedule 10.04.2011
comment
Спасибо, я взглянул на этот код решения, и он делает почти то, что мне не нужно ... все эти репозитории, IoC, фабрики и т. Д. Это не то, что я представляю, когда кто-то говорит о простой архитектуре :) - person Damb; 10.04.2011
comment
Самый простой дизайн, который я могу предложить (хотя и не рекомендую), - это создание объектов контекста EF непосредственно из ваших контроллеров, но, как указано в вашем вопросе, вы уже пробовали это и уже имеете с этим проблемы .. - person Ben; 10.04.2011