Что-то не то с тестированием в .NET (Java и т.д.)

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

Если ваш язык программирования строго-типизированный и в нем есть интерфейсы — почти наверняка вы будете работать с абстракциями. В динамических языках разработчики предпочитают работать с реальной базой.

В .net интерфейсы есть, а значит выбор очевиден. Я взял пример из замечательной книги Марка Симана “Внедрение зависимостей в .Net”, чтобы показать некоторые проблемы, которые есть в данном подходе.

Необходимо отобразить простой список рекомендуемых товаров, если список просматривает привилегированный пользователь, то цена всех товаров должна быть снижена на 5 процентов.

Реализуем самым простым способом:

public class ProductService
{
        private readonly DatabaseContext _db = new DatabaseContext();
    
        public List<Product> GetFeaturedProducts(bool isCustomerPreffered)
        {
            var discount = isCustomerPreffered ? 0.95m : 1;
            var products = _db.Products.Where(x => x.IsFeatured);
    
            return products.Select(p => new Product
            {
                Id = p.Id,
                Name = p.Name,
                UnitPrice = p.UnitPrice * discount
            }).ToList();
        }
}

Чтобы протестировать этот метод нужно убрать зависимость от базы — создадим интерфейс и репозиторий:

public interface IProductRepository
{
    IEnumerable<Product> GetFeaturedProducts();
}

public class ProductRepository : IProductRepository
{
    private readonly DatabaseContext _db = new DatabaseContext();
    public IEnumerable<Product> GetFeaturedProducts()
    {
        return _db.Products.Where(x => x.IsFeatured);
    }
}

Изменим сервис, чтобы он использовал их:

public class ProductService
{
    IProductRepository _productRepository;
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public List<Product> GetFeaturedProducts(bool isCustomerPreffered)
    {
        var discount = isCustomerPreffered ? 0.95m : 1;
        var products = _productRepository.GetFeaturedProducts();

        return products.Select(p => new Product
        {
            Id = p.Id,
            Name = p.Name,
            UnitPrice = p.UnitPrice * discount
        }).ToList();
    }
}

Все готово для написания теста. Используем mock для создания тестового сценария и проверим, что все работает как ожидается:

[Test]
public void IsPrefferedUserGetDiscount()
{
    var mock = new Mock<IProductRepository>();
    mock.Setup(f => f.GetFeaturedProducts()).Returns(new[] {
        new Product { Id = 1, Name = "Pen", IsFeatured = true, UnitPrice = 50}
    });
    
    var service = new ProductService(mock.Object);
    var products = service.GetFeaturedProducts(true);
    
    Assert.AreEqual(47.5, products.First().UnitPrice);
}

Выглядит просто замечательно, что же тут не так? На мой взгляд… практически все.

Сложность и разделение логики


Даже такой простой пример стал сложнее и разделился на две части. Но эти части очень тесно связаны и такое разделение только увеличивает когнитивную нагрузку при чтении и отладки кода.

Множество сущностей и трудоемкость


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

Dependency Injection


Позитивным побочным эффектом мы получили уменьшение связности кода и улучшили архитектуру. На самом деле скорее всего нет. Все действия были продиктованы желанием избавиться от базы, а не улучшением архитектуры и понятности кода. Так как база данных очень сильно связана с логикой, не уверен, что это приведет к улучшению архитектуры. Это настоящий карго культ — добавить интерфейсы и считать, что архитектура улучшилась.

Протестирована только половина


Это самая серьезная проблема — не протестирован репозиторий. Все тесты проходят, но приложение может работать не корректно (из-за внешних ключей, тригеров или ошибках в самих репозиториях). То есть нужно писать еще и тесты для репозиториев? Не слишком ли уже много возни, ради одного метода? К тому же репозиторий все равно придется абстрагировать от реальной базы и все что мы проверим, как хорошо, он работает с ORM библиотекой.

Mock


Выглядят здорово пока все просто, выглядят ужасно когда все сложно. Если код сложный и выглядит ужасно, его никто не будет поддерживать. Если вы не поддерживаете тесты, то у вас нет тестов.

Подготовка окружения для тестов это самая важная часть и она должна быть простой, понятной и легко поддерживаемой.

Абстракции протекают


Если вы спрятали свою ORM за интерфейс, то с одной стороны, она не использует всех своих возможностей, а с другой ее возможности могут протечь и сыграть злую шутку. Это касается подгрузки связанных моделей, сохранение контекста … и т.д.

Как видите довольно много проблем с этим подходом. А что насчет второго, с реально базой? Мне кажется он намного лучше.

Мы не меняем начальную реализацию ProductService. Тестовый фреймворк для каждого теста предоставляет чистую базу данных, в которую необходимо вставить данные необходимые для проверки сервиса:

[Test]
public void IsPrefferedUserGetDiscount()
{
    using (var db = new DatabaseContext())
    {
        db.Products.Add(new Product { Id = 1, Name = "Pen", IsFeatured = true, UnitPrice = 50});
        db.SaveChanges();
    };
    
    var products = new ProductService().GetFeaturedProducts(true);
    
    Assert.AreEqual(47.5, products.First().UnitPrice);
}

Нет моков, нет разделения логики и протестирована работа с настоящей базой. Такой подход значительно удобнее и понятнее, и к таким тестам больше доверия, что все на самом деле работает как нужно.

Тем не менее, есть небольшая проблема. Настоящая система имеет множество зависимостей в таблицах, необходимо заполнить несколько других таблиц только для вставки одной строки в Продукты. Например, Продукты могут требовать Производителя, а он в свою очередь Страну.

Для этого есть решение: начальные «фикстуры» — текстовые файлы (чаще всего в json), содержащие начальный минимальный набор данных. Большим минусом такого решения является необходимость поддерживать эти файлы вручную (изменения в структуре данных, связь начальных данных друг с другом и с кодом тестов).

При правильном подходе тестирование с реальной базой на порядок проще абстрагирования. А самое главное, что упрощается код сервисов, меньше лишнего бойлерплейт кода. В следующей статье, я расскажу как мы организовали тестовый фреймворк и применили несколько улучшений (например, к фикстурам).
Share post

Similar posts

Comments 101

    +4
    Ок. Работаем дальше.
      +10

      Вы сейчас по сути описали интеграционные тесты. Это отличный от юнит-тестирования подход, который позволяет не городить огород из моков и обеспечить лучшее покрытие кода тестами.


      Мы у себя, например, запускаем приложение целиком на чистой базе, а затем пинаем настоящими HTTP-запросами. Тесты, соответственно, проверяют ответы и/или изменения в состоянии. Разного рода сервисы вроде отправлялки почты/смс заменяются на моки при инициализации приложения. Таким образом проверяется, что приложение работает и выполняет поставленные задачи, а не то, что конкретные классы по-отдельности работают так, как это было задумано.


      Но следует учитывать, что юнит-тесты всё ещё имеет смысл использовать если у пишется не контроллер/сервис/репозиторий, которые удобно просто запустить в составе приложения, а некий компонент, пригодный для работы в изолированном окружении, то есть, не имеющий зоопарка зависимостей по 30 методов у каждой.

        0
        Я не против юнит тестов как класса — каждой проблеме свой инструмент. Опыт показывает, что тесты бизнес-логики на реальной базе писать намного проще и быстрее. Кстати, в django, yii2 и т.д. тесты с базой называются юнит тестами, а интеграционными называют те, что проверяют контроллер вместе с html. Мне кажется тут не вопрос формулировки, а в том, что дает больший эффект при меньших усилиях
          +6
          Модульные тесты не просто так называются модульными. Они тестируют модули. В случае C#, который использован для примеров, — классы. Интеграционные тесты тестируют интеграцию модулей друг с другом. Если класс А в своем методе a() вызывает метод b() класса B, то при тестировании a() вы получаете две ситуации:
          — вы мокаете b() и проверяете только логику a() — это модульный тест;
          — вы не мокаете b() и проверяете логику a() в связке с логикой b() — это интеграционный тест.

          Номенклатура важна, иначе может привести к подмене понятий.

          Природа модульных тестов как самых низлежащих в иерархии тестирования заключается в том, что они максимально «пессимистичны». Предполагается, что у вас сломаться может буквально в каждой функции (и я вам больше скажу — сломается). Интеграционные тесты и выше (приемочные, например), наоборот более «оптимистичны», оттого создается впечатление, что с ними проще и быстрее, так как тест кейсов визуально получается меньше. Однако, как уже отмечали выше, и прогонка их занимает больше времени, уменьшая их ценность именно в цикле разработки, и дебаг потом более сложный и трудоемкий. Ну, представьте, например, что вам не просто эксепшен таймаута к базе вывалился, а неправильный набор данных вернулся. Сиди, программист, гадай-отлаживай, то ли в базе они такие лежат, то ли репозиторий неправильно отфильтровал, то ли бизнес-объекты коряво перелопатили.
            0
            Я поэтому избегал слов юнит тест, его границы в разных идеологиях разные. Подход с абстракциями намного более трудоемок, хотелось бы увидеть что он дает помимо идеологической чистоты?
              0
              Возможность изменять различные аспекты независимо, очевидно. Предположим, в вашей системе вносится некоторое изменение (причем вовсе не обязательно вами), и это изменение ломает интеграционный тест (или тесты). Это вполне возможно (я бы даже сказал, практически неизбежно), т.к. архитектура сильно-сцепленная. Где копать? Автору изменений придется либо пытаться угадать, какое же из этих изменений навернуло тест и почему, либо дебажить. Чем больше будет расти система, тем больше времени это будет отнимать. В какой-то момент (не такой уж отдаленный, как может показаться) время на сложный дебаг после каждого фэйла и время, которое могло бы быть изначально потрачено на разделение аспектов в архитектуре и написание модульных тестов, сравняются. Дальше вы и ваша команда начнете проигрывать, и есть хорошие такие шансы, что ваша производительность начнет стремительно падать лавинообразно.
                0

                Вы говорите очень правильные вещи, только несколько сгущаете краски. Если кто-то вносит изменение в случайное место, то мы видим красный тест и разбираемся что произошло — какую проблему вы тут видите? В базе лежит только тестовый кейс и больше ничего — отличие от моков минимально.
                Мне кстати внедрение зависимости нравится больше, и я до сих испытываю душевные терзания, что мы пишем "неправильный" код. В некоторых проектах (например библиотеках) — DI самый правильный выбор. Проблема в том, что он очень плохо работает (может быть я его не умею готовить, но вот странно все умею, а тесты нет).
                Пока что производительность наоборот растет — мы не тратим время на бойлерплейт код и регулярно ловим регресы тестами


                P.S. множество популярных фреймворков (django, yii2, ruby on rails и т.д.) работают с базой и нисколько не страдают — ни от необходимости настроить окружение для тестов, ни от других гипотетических бед. Меня смущает, что мы в .Net игнорируем этот положительный пример как можно по другому работать с тестами.

                  +3
                  Я и не говорю, что работать с базой не нужно. Интеграционные тесты так же важны, как и модульные. Но ключевое слово здесь — «так же». Область применения у этих тестов разная. Как уже справедливо заметили в комментариях, 15 модульных тестов могут по отдельности пройти, но один интеграционный, эти 15 модулей объединяющий, упасть. У этих типов тестов разная область применения. Модульные буквально заставляют вас проектировать слабо-сцепленную архитектуру, благодаря чему при обнаружении ошибки в бизнес-правилах, вы имеете возможность максимально быстро локализовать проблему с точностью до функции (или даже строки) и соответственно максимально быстро исправить ее.

                  Если хотите реальный пример, то некоторое время назад писали мы геолокационную игру, где была очень мудреная логика чекина — данные читались из одних связанных таблиц, обрабатывались и записывались в другие связанные таблицы. Много работы с базой, много бизнес-правил, и, поскольку были мы еще молоды и глупы, то выглядело это прям, как в вашем примере: метод сервиса и в нем вся каша (с той разницей, что репозитории все-таки были). Модульных тестов мы не писали, были у нас только интеграционные, которые дергали метод сервиса, который так и назывался — CheckIn(), с разными наборами данных. Очевидные ошибки мы отловили достаточно быстро, спору нет, но вот когда пошли дефекты именно в алгоритме — там это не учли, тут это забыли, тут побочный эффект выскочил — каждая дебаг-сессия превращалась в долгий и нудный процесс, потому что понять, что же именно пошло не так, по интеграционному тесту было нельзя. Просто в базе оказывались не те данные, вот и гадай себе. Я твердо убежден, что если бы мы отделили бизнес-логику и написали дня нее модульные тесты, то дело шло бы куда веселее. Хотя бы просто потому, что ошибки в ней возникали гораздо чаще, чем в многочисленных, но достаточно банальных CRUD-операциях с БД.
                    0

                    Если того требуют условия, то конечно есть смысл разделить и протестировать отдельно. Но у вас ведь не во всей бизнес-логике так? Очень часто это простой метод, который сходил в базу — вытащил данные, собрал бизнес-объект или записал что-то. Зачем в такой ситуации плодить сущности?

                      +1
                      На самом деле это тема для большой и долгой дискуссии у камина :) Программирование — это дисциплина, которая до сих пор опирается на опыт больше, чем на что-либо другое. Т.е. нет каких-то четких правил, серебряных пуль, если угодно, которые говорили бы вам: «делать надо так, иначе работать не будет». Будет. Просто либо лучше, либо хуже. И не в вакууме, а в текущих условиях бюджета, сроков, требований к качеству и согласованному объему работ. Все сложно, короче :)

                      Принципиальных моментов здесь, мне кажется, два — избегание дублирования и защита от изменений. Если у вас в двух или более методах сервиса(ов) выполняется один и тот же запрос к БД, например _db.Products.Where(...), то это явный повод выделить репозитории. Банально для улучшения сопровождаемости. Если вам приходится вносить однотипные изменения — это сигнал к выделению и инкапсуляции этих изменений.

                      Agile-методики, в частности XP, например, английским по белому говорят: делайте максимально просто. Иногда этим злоупотребляют. Там ведь и продолжение есть в виде этапа рефакторинга в TDD, который как раз и занимается тем, что описано в предыдущем абзаце. Как говаривал Роберт Мартин, если не ошибаюсь (а может и ошибаюсь, и это был Кент Бек), «первое изменение пропускаем, от второго защищаемся».
                        +2
                        В больших проектах нередко ребром ставится вопрос единообразия подходов. Конечно, принцип редукции никто не отменял и нужно упрощать все, что только можно упрощать, но если у вас есть два сервиса, один из которых ходит в базу прямо тут, а другой дергает репозиторий, то новому разработчику придется нехило задуматься — а какой из двух подходов ему использовать? И не находясь в контексте продукта энное время достаточно сложно сказать, будет ли расширяться/усложняться пресловутый сервис.

                        Свои пять копеек про тестирование базы добавлю. У себя на проекте я принципиально использую репозитории для каждого сервиса, и покрываю интеграционными тестами исключительно их. В то время как сервисы покрыты только модульными тестами и всегда общаются с базой через моки. Для проверки взаимодействия сервисов с базой я сразу пишу селениумные e2e-тесты.

                        Получается, что при изменении бизнес-логики мне крайне редко приходится затрагивать свои «тесты бд», поскольку выборка в моем случае далеко не всегда связана непосредственно с бизнесом. А при изменении выборки мне почти никогда не нужно трогать модульные тесты. Но это конечно полностью специфика моего проекта =)
                0
                Про отладку — в базе лежит только то, что туда положили в arrange фазе
            +6

            На самом деле проблема приведенного подхода — в другом. И начинается она — с постановки задачи.


            если список просматривает привилегированный пользователь, то цена всех товаров должна быть снижена на 5 процентов

            А продавать ему будем по какой цене? По показанной — или по той, которая в базе? Если по той, что в базе — то еще ладно, но в первом-то случае логику формирования цены надо выносить в отдельный класс! И тестировать надо этот самый отдельный класс!


            Надо тестировать бизнес-логику (тот самый алгоритм определения цены одного товара для одного пользователя). Надо тестировать репозиторий — он должен правильно фильтровать получаемые из базы данные. Но нет никакого смысла тестировать код, который делегировал другим всю свою логику работы.

              +3
              Но нет никакого смысла тестировать код, который делегировал другим всю свою логику работы.

              На самом деле смысл прогонять весь конвейер обработки запроса есть, ибо может выясниться, что все 15 компонент протестированы по-отдельности, но из-за ошибки в контроллере или мидлвари/http-модуле в итоге что-то не работает, причём только на этом конкретном сценарии

                +2

                Да, но это имеет смысл только в интеграционном тесте. Нет смысла тестировать конвейр сам по себе, замокав все этапы.

                  0

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

                +1
                Я согласен, что пример не очень корректен. Сложно придумать простой пример — получается или сложно или противоречиво.
                И тестировать надо этот самый отдельный класс!
                Представьте, что вы тестируете тот самый класс, вместо моего примера.
                  0

                  Представил.


                  [Test]
                  public void IsPrefferedUserGetDiscount() {
                      Assert.AreEqual(47.5, BL.GetPrice(
                        new Product { Price = 50 }, 
                        new User { IsPreffered = true }
                      ));
                  }
                    0
                    Ну и зависимости от базы тут нет ) А вы в базу ведь еще и пишите, представьте пример с записью в базу.
                      +2

                      Так я об этом и говорю! Не должна бизнес-логика лезть в базу, ни прямо, ни косвенно.


                      У вас именно потому и возникли проблемы в тестировании, что вы не отдлелили код, который запрашивает из базы данные, от кода который вычисляет цены.

                        –1

                        Кому не должна? Я о том и говорю, что это трудоемко их разделять и это тащит за собой ворох проблем.

                          +1

                          Не видел у вас ничего по поводу трудоемкости их разделения и связанных проблемах.

                0

                Кстати, для тестирования кода, работающего с БД через EF может подойти Effort Сам с ним так и не поработал — но направление у них считаю правильным.

                  +7
                  Вот только не стоит так обобщать.

                  Что-то не так не в .Net/Java и т.д., а исключительно в этом конкретном методе.

                  Небольшие размеры кода — вовсе не повод считать, что существующая архитектура хорошая.

                  Этот метод одновременно достает данные из базы, и вычисляет скидки. Вы намешали в него две совершенно разные функции. Чтож теперь жаловаться, что его сложно тестировать?

                  P.S. Пока писал — уже ровно тоже самое выше изложили другими словами.
                    +3
                    У тестирования на настоящей базе есть другие недостатки, кроме создания и поддержки скриптов инициализации минимального набора данных.
                    Unit тесты можно прогнать любой разработчик сразу, локально и быстро. Для прогона интеграционных тестов, как правило, надо поплясать с настройками локального окружения разработчика.
                    Написание нового интеграционного теста может занимать больше времени, чем любого unit теста, из за возни с предварительной настройкой окружения. И сами тесты могу проходить далеко не быстро, а значит их скорее всего не будут запускать локально вообще или не будут запускать часто.
                    Они могут падать по причине не связанной с самим тестируемым кодом (были проблемы с соединением с базой, кэшом в redis), а значит будет меньше реакция на упавший интеграционный тест в CI, чем на упавший unit тест.

                    Из этого следует, что если какой то метод содержит сложную бизнес логику с множеством кейсов, то писать на каждый такой кейс интеграционный тест не выгодно, так скоро время выполнения всех тестов может перевалить за часы.
                    Как представляется общая картина, сложную бизнес логику лучше выделить в классы которые можно спокойно покрыть необходимым количеством unit тестов. Общую работоспособность фичи можно проверить несколькими интеграционными тестами. А еще можно использовать end to end тестирование которое проверяет какой то конкретный бизнес кейс включающий в себя новую фичу.
                    Получается такая пирамида тестов
                      0
                      Получается такая пирамида тестов

                      Только в ISTQB последний уровень пирамиды разделяется на системное тестирование и приемочное тестирование.
                    • UFO just landed and posted this here
                        0

                        И как оно сможет решать проблему инициализации тестовой БД множеством записей с зависимостями?

                        • UFO just landed and posted this here
                            0

                            Нет, "множество записей" было порождено решаемой задачей. Напомню ее:


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

                            • UFO just landed and posted this here
                                0

                                А если у двух user должен быть общий department?

                                • UFO just landed and posted this here
                                    0

                                    "С трейтом"? "Фабрику"? Вы точно говорите о вот этом файле о тридцати трех строках?


                                    Вы напрасно думаете, что я — это такой бесплатный интерактивный туториал.

                                    Нет, я думаю что вы по-прежнему не понимаете сложность задачи.

                                    • UFO just landed and posted this here
                        +7
                        Статья из разряда вредных советов.
                          0
                          Статья задумывалась как слегка провокационная (может переборщил). Только эти вредные советы вполне себе обычный подход во многих фреймворках, например django. Я думаю строго-типизированным языкам вполне есть чему поучиться у динамических.
                            +1
                            Я думаю строго-типизированным языкам вполне есть чему поучиться у динамических.

                            Как в том методе вы смешали работу с базой и вычисление цены, так здесь смешиваете в кучу языки и фреймворки. Не надо так.

                          0
                          Для этого есть решение: начальные «фикстуры» — текстовые файлы (чаще всего в json), содержащие начальный минимальный набор данных. Большим минусом такого решения является необходимость поддерживать эти файлы вручную (изменения в структуре данных, связь начальных данных друг с другом и с кодом тестов).


                          Не, наполнение базы справочной информацией это тоже тест. Т. е. в данном примере тест должен был начинаться с добавления контрагентов, товаров, установки цен и только потом где-то тестироваться список товаров.
                            0

                            Так откуда базу наполнять-то? Или вы предлагаете сделать несколько тестов, которые можно запускать только в строго определенном порядке?

                              0
                              Да! Ведь тесты в идеале должны покрывать всю функциональность. Контрагенты откуда в базе появляются? Вот на эту операцию нужен тест. На добавление товаров тоже нужен тест, и на установку цен, на все нужен тест в идеале. Нельзя протестировать отгрузку со склада, если на складе нет остатков, нужно сначала оприходовать товар, а потом уже можно и списать. Какие-то тесты нужно запускать в определенном порядке, а какие-то необязательно.
                                0

                                Каждый тест независим и начинает практически с чистого листа. Базовые вещи, которые не влияют на функциональность можно залить одинаковые для всех. Вся специфика задается в самом тесте, разумеется нужны хелперы, с помощью которых легко это будет сделать.

                                  +1

                                  Такой подход сильно затрудняет отладку. Вот вызывается подряд 20 тестов, двадцатый — красный. Я запущу двадцатый тест в отладчике и пройдусь по шагам. А вам придется запускать в отладчике все тесты.

                                    0

                                    удалено

                                      0
                                      Нет, Вы неправильно поняли. Тестов должно быть по максимуму, и перед релизом их нужно все прогонять, надеюсь тут разногласий нет. Но для отладки можно один раз выполнить 19 тестов (или взять готовую базу) и потом многократно запускать 20 тест.
                                        0

                                        У вас в общем случае не получится многократно запускать тест, который меняет внешнее состояние.

                                          0
                                          А у вас получится чтоли?)) По факту руками откатываю состояние, если нужно.
                                          Я вообще уже не понимаю о чем спор, мы же говорим о том, что тест использует настоящую базу, так ведь? И при этом вы утверждаете, что я не могу повторно запустить тест, а вы можете?))
                                            0
                                            А у меня все тесты запускаются на чистой базе. И откатывают транзакцию в конце.

                                            Да, такие тесты можно запускать сколько угодно раз и в любом порядке.
                                    0

                                    Каждый тест начинает работать с чистой базой, в нее заливается базовый набор справочников, остальные таблицы заполняются на этапе Arrange в самом тесте.

                                  +3
                                  У вас DbContext должен инжектиться, а не репозиторий. При таком подходе и репозиторий можно протестить. Неправильная архитектура. Если используете EF для мока Db подойдет InMemoryDb.
                                    0
                                    Это вариант, но сложно мокать такие сценарии.
                                      +1
                                      Это вредный совет. У нас например общая доменная база мобильного приложения и веб сайта/апи. Если последовать вашему совету, то все грустно получается.
                                        0
                                        Объясните подробно свою позицию, почему грустно получается? Т.е. по вашему хардкодить DbContext в репозитории как в примере это хорошо?
                                          0
                                          У каждой платформы свой репозиторий со специфичными для нее зависимостями. Например в веб аби это может быть ms sql, на мобилках это sql lite.

                                          А так как в весь код доменки инжектиться репозиторий(через интерфейс), то доменка не замечает подмены. + в вебе используется декоратор для кеша и/или проверки прав, а на мобилке это ненужно.
                                            0
                                            Если нужно «замечать подмену» — значит, надо заводить разные интерфейсы!
                                      0
                                      Чем не устроил Xunit + class fixture + Collection на тесты с бд? Можно и на некоторые тесты с навешивать очередность. А бд с тестовыми данными разворачивать из sql файла лежащего в ресурсах. Зачем тут json?
                                        0
                                        Мы json как раз не используем, в одну статью все не влезло — получилось слишком длинно. Опубликую наше решение — там как раз, что-то типа class fixtures. Из sql неудобно, там либо много лишнего из продакшн базы… либо тяжело поддерживать руками в sql по мере развития системы
                                        +3
                                        Ну конечно, в «Hello world» любое разделение кода будет выглядеть ненужным размножением сущностей. В реальном приложение у вас не 3 строчки кода же?

                                        Подход с репозиториями — проверенный временем. Не спроста о нем в кинжках пишут.
                                        Пара советов для правильной реализации в контексте .NET:
                                        • Отключите LazyLoad
                                        • Возвращайте IEnumerable либо конкретные типы

                                        Тогда не будет у вас leaky abstractions и будет счастье.

                                        А если удариться в демагогию, то кругом компромиссы.
                                        Нет идеального решения, такого чтобы и писать мало кода и все замечательно тестировалось и поддерживалось.
                                        Вы сами должны решить где для вас наилучший баланс.
                                        Однако он там будет только для вас и конкретно вашего проекта.
                                        Не надо сразу евангелизировать это.
                                          0
                                          Я за разделение кода по ответственности, здесь разделение кода идет только потому что мы решили, что реальная база это слишком сложно.

                                          — Отключение LazyLoad не спасет от того, что я забуду прописать Include в репозитории и все упадет
                                          — Сложно мокать графы объектов (особенно если проект динамично развивается)
                                          — IEnumerable — не понял чем спасет

                                          Не надо сразу евангелизировать это.

                                          Согласен, тоже самое касается и DI. Я честно пробовал подход с зависимостями… это можно описать как «ежики плакали, кололись, но продолжали есть кактус». Тестов было мало и они не гарантировали, что все окей. Сейчас мы перешли на реальную базу — тестирование из мучения превратилось в удовольствие.
                                            0

                                            Отключение LazyLoad позволит коду в случае непрописанного Include быстро упасть во время прогона интеграционных тестов. А вот включенный LazyLoad в аналогичном случае тихой сапой неприлично замедлит программу.


                                            С другой стороны, во время разработки первых версий программы (когда главное — чтобы она работала в принципе) LazyLoad действительно сильно помогает.




                                            IEnumerable спасает от формирования запроса к БД в произвольных местах кода, тем самым прекращая "протечку" ответственности за пределы слоя DAL. В итоге при необходимости сменить схему БД — достаточно переписать один слой. В противном же случае приходится переписывать всю программу.

                                              0
                                              Интеграционных тестов

                                              Так мы же про unit говорим?


                                              IEnumerable спасает от формирования запроса к БД

                                              IEnumerable не спасает от этого.

                                                0

                                                Не выдергивайте цитаты из контекста. IEnumerable спасает от формирования запроса к БД в произвольных местах кода.

                                                  0

                                                  Возврат DbQuery объекта по интерфейсу IEnumerable не заставляет его выполниться (он будет выполненен только при перечислении — как раз в произвольном месте). Нужно возвращать List или array

                                                    0

                                                    А при чем тут выполнение? Речь идет о формировании.

                                                      0
                                                      Это одно и то же в данном случае. В EF же нет кэширования сформированных запросов.

                                                      var iter = (IEnumerable<Model>)db.Models.Where(x => x.ParentId == 5);
                                                      

                                                      Вот тут ничего не происходит, просто в iter у вас лежит тот же DbQuery и запрос будет формироваться и выполняться, каждый раз при итерировании по iter.
                                                      +3

                                                      Вернув IEnumerable, мы запрещаем более высоким слоям дописать к запросу к БД парочку условий в WHERE, добавить JOIN и группировку.


                                                      Благодаря этому запрету появляется возможность в дальнейшем вернуть кешированный список, использовать хранимую процедуру или отрефакторить зависимости (скажем, заменить три параллельные связи один-ко-многим на одну связь многие-ко-многим).


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

                                                        0
                                                        Теперь понятно, согласен
                                            +1
                                            Оптимистам от модульного тестирования и хорошей архитектуры могу рассказать, что если на вас внезапно падает поддержка и динамичное развитие спагетти-хардкода, то без тестирования на живой БД в принципе не обойтись :).

                                            И когда основное предназначение ПО — обмен данными, сиречь заумный экспорт данных из БД в 100500 форматов для кучи получателей, у каждого из которых — собственная гордость, и где бОльшая часть того, что надо тестировать — рабочие SQL запросы, тоже нельзя обойтись без БД, как бы меня ни убеждали, что модульные тесты важнее и круче.

                                            Поэтому любая информация о том, как другие делают «это» (автоматизацию тестирования на реальной БД) вызывает у меня жгучий интерес. Так что с нетерпением жду продолжения.

                                            Что же касается неразмножения сущностей и удобства поддержания тестов — давно уверен, что единообразные тесты надо не хардкодить, а описывать в виде метаданных и формировать на лету. И именно этим я займусь после праздников.
                                              +1
                                              БД — это Data. Сервер приложений — это Application. Браузер — это Presentation.
                                              У каждого слоя свои сложности с разработкой и тестированием.
                                              Очевидно, переносимость подходов к разработке и тестированию в этих разных слоях хоть и коррелирует, но слабо.
                                              Это не отменяет полезности юнит-тестов в слое Application.
                                              И это не отменяет полезности интеграционных тестов, когда все слои собраны вместе.
                                              И это не отменяет все прочие подходы к верификации качества, каждого слоя в отдельности, так и разного их сочетания.

                                                +1
                                                Расскажу в кратце, как мы делаем «это». Приложение (очень в кратце, ибо NDA) предназначено для импорта торгов с бирж в формате «мама, роди меня обратно» и экспорте в FpML для дальнейшей обработки соответствующими регуляторами в соответствующих странах. Скорость поступления сообщений о торгах на сегодня в пике порядка 10000 в минуту. В течение максимум 15 минут (а с 2017-го в течение 5 минут) мы обязаны отчитаться перед регулятором. Из чего следует, что права на малейшую ошибку у нас нет вообще. В принципе. Малейший баг теоретически чреват миллионными штрафами конторе от регулятора. Посему, имеем как юнит- (тестирующие каждый public метод и свойство, которое в гетере имеет какой-либо код, отличный от return variable; ), так и интеграционные тесты, как раз таки пользующие почти реальную базу. Сейчас имеем около 1050 тестов, большая часть из которых (процентов 70 как минимум) — интеграционные. В среднем их прогон занимает около 15 минут.

                                                Теперь насчет почти реальной базы. Отличие от действительно реальной базы — в тестовой нет данных о самих торгах/сделках. Т.е. структура базы — 100% та же, что и на проде (или точнее та, которая будет на проде после очередного деплоя, но это, разумеется, зависит от того, в feature-бранче мы или выкатываем некий hotfix) плюс данные абсолютно всех справочников. Если бэкап продовской базы сейчас порядка 100-150 гиг, то такой вот «тестовой» — около 100 Мб.

                                                Ну а теперь самое интересное. Есть так называемая TestDatabaseTemplate, которая как раз и есть та самая «тестовая», описанная выше, все изменения структуры или данных справочников применяются сперва в ней и только, когда некий функционал закрыт, изменения применяются уже на QA базе.
                                                Все интеграционные тесты разбиты по функционалу или же по каким-то бизнес-группам и во время запуска при инициализации assembly такой вот группы тестов создается бэкап TestDatabaseTemplate и восстановление в некую базу с соответствующим суффиксом (например TestDatabaseFpmlGeneration) и подмена реальной базы для всех интеграционных тестов из данной assembly. На сейчас у нас порядка 20-25 различных групп и, соответственно, 20-25 таких вот TestDatabase(suffix) баз. Это дает довольно хороший бонус — получается, что мы всегда можем сделать «срез» работы системы, основываясь на реальных данных, как система ведет себя во время каждого шага обработки/генерации данных.
                                                +2
                                                Настоящая проблема с примером из первой части в том, что вы быстро набросали полностью рабочий вариант «самым простым способом». Наверняка даже протестировали его вручную. Он готов. И все телодвижения, которые вы делаете после этого, не добавляют продукту, в котором этот код написан, никакой ценности. Если хочется написать покрытый тестами код — то стоит писать тесты до кода.

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

                                                Вы с большей вероятностью добавите багов, чем почините. Видно даже на вашем примере — вы используете `IEnumerable` в репозитории, что вызывает выполнение запроса и материализацию продуктов прямо в момент вызова `GetFeaturedProducts`, а не позже, как это кажется по коду (и как происходило в оригинальном примере).

                                                Попробуйте расписать свой же пример, но через написание двух тестов до кода — «Необходимо отобразить простой список рекомендуемых товаров» и «если список просматривает привилегированный пользователь, то цена всех товаров должна быть снижена на 5 процентов» — это покажет тесты с лучшей стороны.

                                                Интеграционные тесты на базе — не панацея. У них есть один огромный недостаток — они медленные. Т.е. они кажутся хорошей идеей, пока их меньше 10000 штук. Потом они начинают занимать час даже на приличном железе, и вам придется вкладываться в инфраструктуру для билдов. К тому же разработчики перестанут запускать тесты локально и начнут пушить со словами «мне повезет».
                                                  0
                                                  Насчет материализации — я имел ввиду именно факт дополнительного создания еще одного набора (отслеживаемых?) объектов Products, из которых уже потом Linq to Objects делает Select, а не собственно выполнения запроса. Суть в том, что при переписывании готового кода поменялся механизм выполнения запроса, возможно не в лучшую сторону, никто (по легенде поста) это не заметил, и все это подперто одним (!) тестом.
                                                    –1
                                                    Ценность тестов в том, что код не сломается в будущем и в этом их основное назначение, а не в том, что они магически вам построят архитектуру. И так думаю не только я — Does TDD lead to good design

                                                    Медленность тестов с базой это конечно плохо, пока у нас намного меньше тысячи…
                                                      0
                                                      У меня интеграционных тестов > 8000. Оцените время выполнения при использовании вашего фреймворка :) Медленность тестов с базой — это ужасно плохо.

                                                      TDD и «писать тесты до кода» — разные вещи. Я не приверженец TDD, и не предлагаю вам строить архитектуру только на основании тестов. И вообще (вынуждненно, много легаси кода) предпочитаю интеграционные тесты. Но их можно писать до кода, а можно — после. Одни и те же тесты на один и тот же код могут приносят разный профит в зависимости от момента их написания. И дают разный эффект на мотивацию.

                                                      Тесты после кода:

                                                      [время на написание кода] + [время на ручной протык разработчиком и фикс] + [время на написание тестов]

                                                      Недостатки:
                                                      • Разработчик пишет сразу большие куски функционала, которые нужно покрывать несколькими тестами (как у вас в статье). Из-за этого вы получаете меньше тестов, с дырками в покрытии (уберется необходимость скидки — удалят тест — не будет покрыт кейс выборки без скидки)
                                                      • Разработчик воспринимает код после протыка и фикса как «готовый» — и подсознательно считает тесты ненужными, или считает их «дополнительной работой», а не неотъемлимой частью кода. Которая поможет в будущем кому-то (не ему) вносить изменения.


                                                      Тесты до кода:

                                                      [время на написание теста] + [время на написание кода]

                                                      Тот же код, те же тесты — но нет необходимости в активном ручном протыке. И нет ощущения, что вы делате «дополнительную» работу за других. Тесты начинают помогать разработчику вот прямо сейчас, а не когда-нибудь потом. Это прямой стимул, который заодно создает сайдэффект «код не сломается в будущем». Win-Win.

                                                      Внедрение тестов на проекте — это проблема психологии, а не техническая (как писать и запускать) и не менеджерская (выделить время). Нужно сделать так, чтобы разработчики чувствовали себя неуютно при попытке нафигачить код без тестов. Им должно быть физически неудобно это делать. «Лень запускать UI и проверять руками, проще тест написать» — вот эффект, который нужно получить.

                                                      Попробуйте писать свои интеграционные тесты до кода. Почувствуете разницу.
                                                        0
                                                        В такой формулировке согласен полностью. А вы не искали какой-нибудь эмулятор БД? Единственное, что нашел это h2, но непонятно насколько точно он эмулирует
                                                            0
                                                            Немного не то, но надо попробовать. Спасибо!
                                                            +1
                                                            Зависит от проекта. Когда-то давно пытались прикручивать самописный эмулятор — работало для простых запросов, но на чем-то сложном начинались проблемы. Т.е. для простых запросов оказалось удобнее использовать репозиторий (как в статье) и мокать его. Для сложных — наткнулись на разницу эмулятора и реальной базы.

                                                            На текущем проекте — в базе лежит часть логики (права доступа, хаки для иерархических данных и прочее) — и интеграционные тесты требуют реальной базы. Вынести логику из базы полностью не получится (точнее, получится, но работать будет гораздо медленее). И ORM — не EF :)
                                                        0
                                                        Интеграционные тесты на базе — не панацея. У них есть один огромный недостаток — они медленные. Т.е. они кажутся хорошей идеей, пока их меньше 10000 штук. Потом они начинают занимать час даже на приличном железе, и вам придется вкладываться в инфраструктуру для билдов.

                                                        Как писал чуть выше — у нас порядка 1050 тестов, из которых порядка 700 — интеграционные, т.е. использующие базу. Прогон в среднем около 15 минут.

                                                        К тому же разработчики перестанут запускать тесты локально и начнут пушить со словами «мне повезет».

                                                        1. линейкой по рукам
                                                        2. отрывать руки
                                                        3. увольнять

                                                        (пункты 2 и 3 можно поменять местами по желанию :)
                                                        Если что — про п.3 я серьезно. У нас, правда, пока прецедентов не было, но случаи лишения годового бонуса были, а это почти две зарплаты может быть, что довольно быстро учит нерадивых «везунчиков».
                                                          +1
                                                          1. линейкой по рукам
                                                          2. отрывать руки
                                                          3. увольнять

                                                          Это сработает только в такой ситуации как у вас, когда цена ошибки — огромный штраф.
                                                          Обычно это не так.
                                                          Вот у меня все проще, поэтому интеграционные тесты не запускаю (ибо 10 мин), запускаю только те, что сам только что написал. Если что-то пошло не так, мне прилетит от билд сервера.
                                                          Я это все к тому, что разработчикам не особо нужно запускать тесты (я имею ввиду ВСЕ тесты), для этого есть CI.
                                                            +3
                                                            15 минут на 1000 тестов — это ужасно долго. У меня на проекте 12800 тестов, из них — 9700 серверных, из которых больше 8000 — интеграционные на базе (так сложилось). Время прогона — 20 минут на CI, примерно 15 минут — на дев-машине.

                                                            Если бы мы не вложились в CI (билд сервера с SSD + много локальных хаков в тестовом фреймворке) — мы бы получили почти 3 часа на прогон тестов. Вы готовы ждать 2-3 часа перед каждым пушем? А потом еще 3 часа на прогонку на билд-сервере? Ок, у нас на проекте мы смогли вставить хаки и сократить время выполнения. Но я не уверен, сможете ли вы сделать то же самое в своем коде.

                                                            Fear-driven development? Бить по рукам, лишать премии и прочие кнуты может позволить себе только галера, на которой студентов держат за счет того, что они не знают другой жизни и боятся поменять работу. На продуктовой разработке это просто убьет мотивацию и принесет больше вреда, чем пользы.

                                                            Если у вас разработчиков приходится лишать премии за нарушение принятого процесса — значит они не понимают, зачем конкретные практики в вашем процессе приняты. И битьем по рукам вы только усугубите ситуацию — спрячете проблему, загоните ее внутрь — но не решите.

                                                            Есть более гуманные способы — например снизить потери от «красных» коммитов активным бранчеванием (поломает дев свою ветку — всех остальных это не затронет). Процесс должен помогать разработчикам творить — создавать продукт. А не держать их в узде.
                                                              +1
                                                              Нет, не понятно, что я несколько утрировал (ну или иносказательно выразился касаемо «битья по рукам»), т.к. таки вся команда и каждый разработчик должны понимать, чем чреваты их красные билды и отмазываться «авось прокатит» банально не профессионально.

                                                              15 минут на 1000 тестов — это ужасно долго

                                                              Это на дев-машине, на CI порядка 3-5. Но я понимаю, что и это очень долго, т.к. где-то через год объем функционала может вырасти раза в три и количество тестов, боюсь, соответственно.
                                                              Но тем не менее, боюсь, что особо улучшать производительность наших тестов некуда, ибо логика в них такая, что часто запускается несколько потоков на обработку сообщений, а они порой не могут быть обработаны сразу при получении, т.к. зависят от других сообщений, которые биржа еще не послала (я же говорю, что формат «мама, роди меня обратно»)… к примеру, сообщение, инициирующее трейд, может прийти через минуту или две после получения сообщения об изменении или закрытии трейда. Соответственно, и система должна это обрабатывать и тесты должны все это проверять. Т.е. порой задержка в тесте нужна для проверки как раз таки логики работы.
                                                              В «нормальном» приложении, думаю, нет надобности в таких свистоплясках и 15 минут на 1000 тестов — действительно много.

                                                              снизить потери от «красных» коммитов активным бранчеванием (поломает дев свою ветку — всех остальных это не затронет)

                                                              Ваши слова да Богу нашему IT в уши… У нас TFS (хоть и 2015, в котором внутри есть git, но git под запретом), в котором так просто бранч не сделаешь — надо согласовать, создать тикет, получить добро по всей иерархии… Но локально особо продвинутые держат все в локальном git репозитории и делают коммит в TFS только master'а, ну а закоренелые консерваторы так и сидят на «чистом» TFS.
                                                                0

                                                                Если у вас проблема именно что в задержках — тут поможет "виртуальное время". Нужен инструмент, который позволит быстро перематывать время вперед в тестовой среде.


                                                                Где-то я видел инструменты тестирования, которые позволяют подменить системные классы в тестах.


                                                                Или же можно сделать свой велосипед, написав свой слой доступа к времени и запретив использовать стандартные средства через инструменты статического анализа.

                                                                  0
                                                                  Если говорить про .net то это typemock.
                                                                  Правда он платный, хотя бесплатные способные подменять статические и невиртуальные методы я не встречал.
                                                                  0
                                                                  Либо Вы не так меня поняли, либо я не так объяснил :)
                                                                  Проблемы с задержками нет и «перематывать» нам время не зачем. Суть как раз в необходимости проверки того, что поток А будет ожидать завершения обработки другой версии сообщения в потоке Б (или наоборот), или же должен сохранить текущее состояние сообщения в «pending mode», а затем (совершенно уже другой поток), получив «нужную» версию сообщения, сделает его «дообработку» и наконец сохранит сообщение как в базу в нужном состоянии, так и сгенерирует XML по XSLT и «пошлет» его регулятору (тут уже mock).
                                                                  Я что хочу сказать — эти все задержки, а также различные проблемы с «гонками» у потоков — это реальность нашего приложения и от нее никуда не деться и ее нам нужно корректно тестировать во всех возможных извращенных вариантах :)
                                                                    0

                                                                    Так в чем тогда проблема? Посылаем первое сообщение, ждем начала ожидания, посылаем второе, ждем окончания обработки обоих, смотрим результат.

                                                          +1
                                                          Вам не нужно создавать интерфейсы такие как IRepository. Т.к. EF это уже реализация паттерна репозиторий. И EF поддерживает unittesting из коробки (т.е. без дополнительных абстракций).

                                                          Попробую разобрать ваш пример.
                                                          Тут будут мои предположения о вашем Problem Domain и, скорее всего, я сделаю много неверных предположений.
                                                          Не обращайте внимание на мой стиль форматирования, я так привык.

                                                          private readonly DatabaseContext _db = new DatabaseContext();

                                                          Это проблема. Что такое DatabaseContext? Ключевое слово в название этих классов — Context. Отсюда вопрос, действительно ли Database — это часть вашего Problem Domain?
                                                          Возможно это должно называться ProductContext?
                                                          Далее я предполагаю, что это верно.
                                                          Можно выделить два контекста — ProductContext и SalesContext (не знаю как у вас, но мне кажется логичным, чтобы данные о customers и скидках были тут).

                                                          Можно выделить ProductStore.
                                                          public class ProductsContext : DbContext {
                                                            public virtual DbSet<Product> Products { get; set; }
                                                          }
                                                          public class Product {
                                                            public int Id { get; set; }
                                                            public string Name { get; set; }
                                                            public bool IsFeatured { get; set; }
                                                            public decimal UnitPrice { get; set; }
                                                          }
                                                          


                                                          И Sales. Тут же можно вынести логику скидок в отдельный класс.
                                                          public class SalesContext : DbContext {
                                                            public virtual DbSet<Customer> Customers { get; set; }
                                                          }
                                                          public class Customer {
                                                            public int Id { get; set; }
                                                            public string Name { get; set; }
                                                            public bool IsPreferred { get; set; }
                                                          }
                                                          public class DiscountProvider {
                                                            private static Discount FeaturedCustomerDiscount = new Discount(0.95m);
                                                            private static Discount DefaultDiscount = new Discount(1);
                                                          
                                                            public Discount GetFor(Customer customer) {
                                                              return customer.IsPreferred ? FeaturedCustomerDiscount : DefaultDiscount;
                                                            }
                                                          }
                                                          public class Discount {
                                                            private decimal multiplier;
                                                          
                                                            public Discount(decimal multiplier) {
                                                              this.multiplier = multiplier;
                                                            }
                                                          
                                                            public decimal Calculate(decimal amount) {
                                                              return amount * multiplier;
                                                            }
                                                          }
                                                          


                                                          И собственно магазин. Я вынес этот сервис в приложение, т.к. это Application Service, а не Domain Service (по моему мнению).
                                                          public class ProductService {
                                                            private DiscountProvider discountProvider;
                                                            private ProductsContext productContext;
                                                            private SalesContext salesContext;
                                                          
                                                            public ProductService(ProductsContext productContext, SalesContext salesContext, DiscountProvider discountProvider) {
                                                              this.productContext = productContext;
                                                              this.salesContext = salesContext;
                                                              this.discountProvider = discountProvider;
                                                            }
                                                            public IEnumerable<FeaturedProduct> GetFeaturedProductsFor(int customerId) {
                                                              var customer = salesContext.Customers.FirstOrDefault(c => c.Id == customerId);
                                                              var discount = discountProvider.GetFor(customer);
                                                              var products = productContext.Products.Where(p => p.IsFeatured);
                                                          
                                                              return products.AsEnumerable().Select(p => new FeaturedProduct(p, discount));
                                                            }
                                                          }
                                                          
                                                          public class FeaturedProduct {
                                                            private Discount discount;
                                                            private Product product;
                                                          
                                                            public FeaturedProduct(Product product, Discount discount) {
                                                              this.product = product;
                                                              this.discount = discount;
                                                            }
                                                          
                                                            public int Id => product.Id;
                                                            public string Name => product.Name;
                                                            public decimal UnitPrice => discount.Calculate(product.UnitPrice);
                                                          }
                                                          


                                                          А теперь собственно тест. С помощью метода CreateMock можно замокать контексты.
                                                          [TestFixture]
                                                          public class ProductServiceTests {
                                                            private IQueryable<Product> productsData = new List<Product>
                                                                    {
                                                                        new Product { Name = "Nokia 3310", IsFeatured = true, UnitPrice = 100m },
                                                                        new Product { Name = "iPhone" },
                                                                        new Product { Name = "Windows Phone" },
                                                                    }.AsQueryable();
                                                            private IQueryable<Customer> customersData = new List<Customer>
                                                                    {
                                                                        new Customer { Id = 1, Name = "Leonardo" },
                                                                        new Customer { Id = 2, Name = "Donatello", IsPreferred = true },
                                                                        new Customer { Name = "Mike" },
                                                                    }.AsQueryable();
                                                          
                                                            private Mock<Ctx> CreateMock<Ctx, T>(IQueryable<T> data, Expression<Func<Ctx, DbSet<T>>> expr) where Ctx : DbContext where T : class {
                                                              var mockSet = new Mock<DbSet<T>>();
                                                              mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(data.Provider);
                                                              mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(data.Expression);
                                                              mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(data.ElementType);
                                                              mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
                                                          
                                                              var mockContext = new Mock<Ctx>();
                                                              mockContext.Setup(expr).Returns(mockSet.Object);
                                                          
                                                              return mockContext;
                                                            }
                                                          
                                                            [Test]
                                                            public void GetFeaturedProductsForRegularCustomer() {
                                                              // Arrange
                                                              var mockProductsContext = CreateMock<ProductsContext, Product>(productsData, c => c.Products);
                                                              var mockCustomersContext = CreateMock<SalesContext, Customer>(customersData, c => c.Customers);
                                                              var customerId = 1;
                                                              var service = new ProductService(mockProductsContext.Object, mockCustomersContext.Object, new DiscountProvider());
                                                          
                                                              // Act
                                                              var products = service.GetFeaturedProductsFor(customerId);
                                                          
                                                              // Assert
                                                              Assert.AreEqual(1, products.Count());
                                                              Assert.AreEqual("Nokia 3310", products.First().Name);
                                                              Assert.AreEqual(100m, products.First().UnitPrice);
                                                            }
                                                          
                                                            [Test]
                                                            public void GetFeaturedProductsForPreferredCustomer() {
                                                              // Arrange
                                                              var mockProductsContext = CreateMock<ProductsContext, Product>(productsData, c => c.Products);
                                                              var mockCustomersContext = CreateMock<SalesContext, Customer>(customersData, c => c.Customers);
                                                              var customerId = 2;
                                                              var service = new ProductService(mockProductsContext.Object, mockCustomersContext.Object, new DiscountProvider());
                                                          
                                                              // Act
                                                              var products = service.GetFeaturedProductsFor(customerId);
                                                          
                                                              // Assert
                                                              Assert.AreEqual(1, products.Count());
                                                              Assert.AreEqual("Nokia 3310", products.First().Name);
                                                              Assert.AreEqual(95m, products.First().UnitPrice);
                                                            }
                                                          }
                                                          


                                                          Еще конфигурация Autofac. Следите за руками) Ни одного интерфейса и unittest'ы работают без базы.
                                                          builder.RegisterType<ProductsContext>();
                                                          builder.RegisterType<SalesContext>();
                                                          builder.RegisterType<DiscountProvider>();
                                                          builder.RegisterType<ProductService>();
                                                          


                                                          Как бонус, тут мы вынесли скидки, и их можно протестировать отдельно.

                                                          Конечно остаются проблемы, такие как протестировать загрузку связанных данных. Но это можно покрыть парой e2e тестов. Entity Framework Testing with a Mocking Framework
                                                          Обратите внимание на раздел Limitations, он как раз об этом.
                                                            0
                                                            Спасибо, за такой подробный ответ! Про пример в статье я уже говорил — он из книги про DI. Очень сложно с примером оказалось — либо много кода, либо все стараются подвергнуть критике сам пример.

                                                            Интересный подход, попробую примерить его на наши проекты. А как на счет ссылок между контекстами, EF поддерживает такое?
                                                              0
                                                              Между контекстами — нет, не поддерживает. А вот между сущностями — поддерживает даже если некоторые из них не имеют своих DbSet.
                                                                0
                                                                А миграции тоже работают при многих контекстах с перекрестными ссылками?
                                                                  0
                                                                  Смотря что понимать под «работают». Определенно миграции для разных контекстов, ссылающихся на одни и те же таблицы, будут конфликтовать друг с другом. Решением может быть дополнительный контекст только для миграций, содержащий в себе все таблицы.
                                                                +1
                                                                У EF есть один недостаток, который является и его достоинством одновременно. Он диктует как вы должны разрабатывать приложение, а попытка абстрагироваться от него добавляет огромное количество boilerplate кода и в этом случие практически теряется весь смысл от использования EF.

                                                                Нужно понимать, когда используете EF, у вас больше нет слоя доступа к данным, сам EF это и есть слой доступа к данным.
                                                                А то, что в вашем примере DatabaseContext (то что я назвал ProductContext), это получается BoundedContext из DDD. Соответсвенно все модели которые вы используете в этом *Context являются бизнес сущностями и бизнес логику можно да и нужно инкапсулировать в них.

                                                                Конечно у EF есть определенные ограничения на моделирование бизнес сущностей, но это тот самый trade-off, на который мы соглашаемся начиная его использовать.

                                                                Вот неплохая статья о том как моделировать бизнес сущьность и ограничения в EF: Domain modeling with Entity Framework scorecard (Jimmy Bogard).

                                                                А как на счет ссылок между контекстами, EF поддерживает такое?

                                                                Если рассматривать контекст EF, как BoundedContext, то у них не должно быть прямой связи, но приложение может спокойно использовать сразу несколько контекстов (как в моем примере).
                                                                  0
                                                                  Нужно понимать, когда используете EF, у вас больше нет слоя доступа к данным, сам EF это и есть слой доступа к данным.

                                                                  А если Вам понадобится заменить реляционную БД на какой-нибудь другой тип хранилища, как Вы это сделаете? Нам ни что не мешает использовать EF с паттерном репозиторий. Тем более насколько я понимаю, сущность EF это не бизнес объект, а таблица БД и таким образом обращаясь к EF из бизнес логики, мы просто смешиваем логику БД с бизнес логикой. Поправьте меня пожалуйста если я не прав.
                                                                    0
                                                                    А если Вам понадобится заменить реляционную БД на какой-нибудь другой тип хранилища, как Вы это сделаете?

                                                                    Если вы используете EF, то никак. Если уж вы выбрали EF, то нужно понимать с какими БД он работает и какие у него ограничения.
                                                                    Я скажу больше, невозможно безболезненно сменить даже реляционную БД если вы изначально не разрабатывали приложение с прицелом на смену БД или с поддержкой нескольких БД.

                                                                    Нам ни что не мешает использовать EF с паттерном репозиторий.

                                                                    Еще раз повторю, EF это и есть реализация паттерна репозиторий. Но я согласен, вам ничто не мешает добавить еще один уровень абстракции.

                                                                    Тем более насколько я понимаю, сущность EF это не бизнес объект, а таблица БД и таким образом обращаясь к EF из бизнес логики, мы просто смешиваем логику БД с бизнес логикой.

                                                                    По моему мнению, это основная ошибка тех кто с EF работает (то, что я видел).
                                                                    У вас есть обыкновенный класс (бизнес сущность) и правила как его представить в БД, остальное на себя берет EF.
                                                                    Когда у вас anemic domain, тогда да, скорее всего у вас будет практически один в один мапиться бизнес объекты в таблицы (т.к. у вас по сути не бизнес объекты, а просто структуры данных). И в этом случае, то что раньше было в хранимках переносится в код приложения, вот и создают репозитории чтобы положить туда этот код, чтобы не дублировать его всюду.

                                                                    Я это все к тому, что EF это не серебрянная пуля, у него куча проблем и ограничений (как у любого ORM), и все это нужно учитывать выбирая его.
                                                                      0
                                                                      Если я Вас правильно понял Entity Framework-у можно объяснить, как мапить бизнес объект на таблицы? Интересует тот случай, когда бизнес объект — это несколько таблиц.
                                                                      То есть сохраняем мы доменный объект, передаем его EF, а он автоматически распределяет данные хранящиеся в нем по разным таблицам.
                                                                        0
                                                                        Если коротко, то да, поддерживает.

                                                                        Во первых поддерживаются: один к одному, один ко многим, многие ко многим.
                                                                        Во вторых поддерживается наследование: Table per Hierarchy (TPH), Table per Type (TPT), Table per Concrete class (TPC)

                                                                        Вот самый простой пример.
                                                                        Доменного объекта:
                                                                        public class Product {
                                                                          public int Id { get; set; }
                                                                          public string Name { get; set; }
                                                                          public virtual ICollection<ProductProperty> Properties { get; private set; }
                                                                        
                                                                          public Product() {
                                                                            Properties = new List<ProductProperty>();
                                                                          }
                                                                        
                                                                          public void AddProperty(string value) {
                                                                            // some business rule
                                                                            if (string.IsNullOrWhiteSpace(value))
                                                                            {
                                                                              value = "Default Value";
                                                                            }
                                                                        
                                                                            Properties.Add(new ProductProperty { Value = value });
                                                                          }
                                                                        }
                                                                        


                                                                        Пример использования в неком сервисе приложения:
                                                                        var product = new Product { Name = "P1" };
                                                                        product.AddProperty("Some Property");
                                                                        
                                                                        productContext.Products.Add(product);
                                                                        productContext.SaveChanges();
                                                                        


                                                                        Соответственно в БД две таблицы: Products и ProductProperties.
                                                                          0
                                                                          Спасибо.
                                                              +1
                                                              Сложность и разделение логики

                                                              Даже такой простой пример стал сложнее и разделился на две части. Но эти части очень тесно связаны и такое разделение только увеличивает когнитивную нагрузку при чтении и отладки кода.


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

                                                              Насколько я помню в подобной литературе авторы пишут примерно о том, что да кода становится больше, но оно того стоит. Я думаю если есть такие книги, то такой подход проверен и работает, просто порог вхождения в него выше хотя бы потому, что многим может быть лень писать «Лишний код» (тоже самое касается тестов).
                                                                0

                                                                Эти проблемы — из моей практики, конечно нужно пытаться сделать это разделение простым и логичным, но часто оно выглядит очень искусственным. Кстати почти те же проблемы описываются в вики про минусы Dependency Injection. DI — очень мощная концепция, которая тем не менее имеет приличный оверхед. Я лишь за то, чтобы знать какие есть ей альтернативы и использовать подходящий инструмент.

                                                              Only users with full accounts can post comments. Log in, please.