Если ваш язык программирования строго-типизированный и в нем есть интерфейсы — почти наверняка вы будете работать с абстракциями. В динамических языках разработчики предпочитают работать с реальной базой.
В .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), содержащие начальный минимальный набор данных. Большим минусом такого решения является необходимость поддерживать эти файлы вручную (изменения в структуре данных, связь начальных данных друг с другом и с кодом тестов).
При правильном подходе тестирование с реальной базой на порядок проще абстрагирования. А самое главное, что упрощается код сервисов, меньше лишнего бойлерплейт кода. В следующей статье, я расскажу как мы организовали тестовый фреймворк и применили несколько улучшений (например, к фикстурам).
Комментарии (88)
kekekeks
28.12.2016 13:21+10Вы сейчас по сути описали интеграционные тесты. Это отличный от юнит-тестирования подход, который позволяет не городить огород из моков и обеспечить лучшее покрытие кода тестами.
Мы у себя, например, запускаем приложение целиком на чистой базе, а затем пинаем настоящими HTTP-запросами. Тесты, соответственно, проверяют ответы и/или изменения в состоянии. Разного рода сервисы вроде отправлялки почты/смс заменяются на моки при инициализации приложения. Таким образом проверяется, что приложение работает и выполняет поставленные задачи, а не то, что конкретные классы по-отдельности работают так, как это было задумано.
Но следует учитывать, что юнит-тесты всё ещё имеет смысл использовать если у пишется не контроллер/сервис/репозиторий, которые удобно просто запустить в составе приложения, а некий компонент, пригодный для работы в изолированном окружении, то есть, не имеющий зоопарка зависимостей по 30 методов у каждой.
justserega
28.12.2016 16:57Я не против юнит тестов как класса — каждой проблеме свой инструмент. Опыт показывает, что тесты бизнес-логики на реальной базе писать намного проще и быстрее. Кстати, в django, yii2 и т.д. тесты с базой называются юнит тестами, а интеграционными называют те, что проверяют контроллер вместе с html. Мне кажется тут не вопрос формулировки, а в том, что дает больший эффект при меньших усилиях
Cromathaar
28.12.2016 17:31+6Модульные тесты не просто так называются модульными. Они тестируют модули. В случае C#, который использован для примеров, — классы. Интеграционные тесты тестируют интеграцию модулей друг с другом. Если класс А в своем методе a() вызывает метод b() класса B, то при тестировании a() вы получаете две ситуации:
— вы мокаете b() и проверяете только логику a() — это модульный тест;
— вы не мокаете b() и проверяете логику a() в связке с логикой b() — это интеграционный тест.
Номенклатура важна, иначе может привести к подмене понятий.
Природа модульных тестов как самых низлежащих в иерархии тестирования заключается в том, что они максимально «пессимистичны». Предполагается, что у вас сломаться может буквально в каждой функции (и я вам больше скажу — сломается). Интеграционные тесты и выше (приемочные, например), наоборот более «оптимистичны», оттого создается впечатление, что с ними проще и быстрее, так как тест кейсов визуально получается меньше. Однако, как уже отмечали выше, и прогонка их занимает больше времени, уменьшая их ценность именно в цикле разработки, и дебаг потом более сложный и трудоемкий. Ну, представьте, например, что вам не просто эксепшен таймаута к базе вывалился, а неправильный набор данных вернулся. Сиди, программист, гадай-отлаживай, то ли в базе они такие лежат, то ли репозиторий неправильно отфильтровал, то ли бизнес-объекты коряво перелопатили.justserega
28.12.2016 18:38Я поэтому избегал слов юнит тест, его границы в разных идеологиях разные. Подход с абстракциями намного более трудоемок, хотелось бы увидеть что он дает помимо идеологической чистоты?
Cromathaar
28.12.2016 21:41Возможность изменять различные аспекты независимо, очевидно. Предположим, в вашей системе вносится некоторое изменение (причем вовсе не обязательно вами), и это изменение ломает интеграционный тест (или тесты). Это вполне возможно (я бы даже сказал, практически неизбежно), т.к. архитектура сильно-сцепленная. Где копать? Автору изменений придется либо пытаться угадать, какое же из этих изменений навернуло тест и почему, либо дебажить. Чем больше будет расти система, тем больше времени это будет отнимать. В какой-то момент (не такой уж отдаленный, как может показаться) время на сложный дебаг после каждого фэйла и время, которое могло бы быть изначально потрачено на разделение аспектов в архитектуре и написание модульных тестов, сравняются. Дальше вы и ваша команда начнете проигрывать, и есть хорошие такие шансы, что ваша производительность начнет стремительно падать лавинообразно.
justserega
28.12.2016 22:55Вы говорите очень правильные вещи, только несколько сгущаете краски. Если кто-то вносит изменение в случайное место, то мы видим красный тест и разбираемся что произошло — какую проблему вы тут видите? В базе лежит только тестовый кейс и больше ничего — отличие от моков минимально.
Мне кстати внедрение зависимости нравится больше, и я до сих испытываю душевные терзания, что мы пишем "неправильный" код. В некоторых проектах (например библиотеках) — DI самый правильный выбор. Проблема в том, что он очень плохо работает (может быть я его не умею готовить, но вот странно все умею, а тесты нет).
Пока что производительность наоборот растет — мы не тратим время на бойлерплейт код и регулярно ловим регресы тестами
P.S. множество популярных фреймворков (django, yii2, ruby on rails и т.д.) работают с базой и нисколько не страдают — ни от необходимости настроить окружение для тестов, ни от других гипотетических бед. Меня смущает, что мы в .Net игнорируем этот положительный пример как можно по другому работать с тестами.
Cromathaar
28.12.2016 23:37+3Я и не говорю, что работать с базой не нужно. Интеграционные тесты так же важны, как и модульные. Но ключевое слово здесь — «так же». Область применения у этих тестов разная. Как уже справедливо заметили в комментариях, 15 модульных тестов могут по отдельности пройти, но один интеграционный, эти 15 модулей объединяющий, упасть. У этих типов тестов разная область применения. Модульные буквально заставляют вас проектировать слабо-сцепленную архитектуру, благодаря чему при обнаружении ошибки в бизнес-правилах, вы имеете возможность максимально быстро локализовать проблему с точностью до функции (или даже строки) и соответственно максимально быстро исправить ее.
Если хотите реальный пример, то некоторое время назад писали мы геолокационную игру, где была очень мудреная логика чекина — данные читались из одних связанных таблиц, обрабатывались и записывались в другие связанные таблицы. Много работы с базой, много бизнес-правил, и, поскольку были мы еще молоды и глупы, то выглядело это прям, как в вашем примере: метод сервиса и в нем вся каша (с той разницей, что репозитории все-таки были). Модульных тестов мы не писали, были у нас только интеграционные, которые дергали метод сервиса, который так и назывался — CheckIn(), с разными наборами данных. Очевидные ошибки мы отловили достаточно быстро, спору нет, но вот когда пошли дефекты именно в алгоритме — там это не учли, тут это забыли, тут побочный эффект выскочил — каждая дебаг-сессия превращалась в долгий и нудный процесс, потому что понять, что же именно пошло не так, по интеграционному тесту было нельзя. Просто в базе оказывались не те данные, вот и гадай себе. Я твердо убежден, что если бы мы отделили бизнес-логику и написали дня нее модульные тесты, то дело шло бы куда веселее. Хотя бы просто потому, что ошибки в ней возникали гораздо чаще, чем в многочисленных, но достаточно банальных CRUD-операциях с БД.justserega
29.12.2016 04:09Если того требуют условия, то конечно есть смысл разделить и протестировать отдельно. Но у вас ведь не во всей бизнес-логике так? Очень часто это простой метод, который сходил в базу — вытащил данные, собрал бизнес-объект или записал что-то. Зачем в такой ситуации плодить сущности?
Cromathaar
29.12.2016 11:18+1На самом деле это тема для большой и долгой дискуссии у камина :) Программирование — это дисциплина, которая до сих пор опирается на опыт больше, чем на что-либо другое. Т.е. нет каких-то четких правил, серебряных пуль, если угодно, которые говорили бы вам: «делать надо так, иначе работать не будет». Будет. Просто либо лучше, либо хуже. И не в вакууме, а в текущих условиях бюджета, сроков, требований к качеству и согласованному объему работ. Все сложно, короче :)
Принципиальных моментов здесь, мне кажется, два — избегание дублирования и защита от изменений. Если у вас в двух или более методах сервиса(ов) выполняется один и тот же запрос к БД, например _db.Products.Where(...), то это явный повод выделить репозитории. Банально для улучшения сопровождаемости. Если вам приходится вносить однотипные изменения — это сигнал к выделению и инкапсуляции этих изменений.
Agile-методики, в частности XP, например, английским по белому говорят: делайте максимально просто. Иногда этим злоупотребляют. Там ведь и продолжение есть в виде этапа рефакторинга в TDD, который как раз и занимается тем, что описано в предыдущем абзаце. Как говаривал Роберт Мартин, если не ошибаюсь (а может и ошибаюсь, и это был Кент Бек), «первое изменение пропускаем, от второго защищаемся».
mayorovp
28.12.2016 13:25+6На самом деле проблема приведенного подхода — в другом. И начинается она — с постановки задачи.
если список просматривает привилегированный пользователь, то цена всех товаров должна быть снижена на 5 процентов
А продавать ему будем по какой цене? По показанной — или по той, которая в базе? Если по той, что в базе — то еще ладно, но в первом-то случае логику формирования цены надо выносить в отдельный класс! И тестировать надо этот самый отдельный класс!
Надо тестировать бизнес-логику (тот самый алгоритм определения цены одного товара для одного пользователя). Надо тестировать репозиторий — он должен правильно фильтровать получаемые из базы данные. Но нет никакого смысла тестировать код, который делегировал другим всю свою логику работы.
kekekeks
28.12.2016 13:35+3Но нет никакого смысла тестировать код, который делегировал другим всю свою логику работы.
На самом деле смысл прогонять весь конвейер обработки запроса есть, ибо может выясниться, что все 15 компонент протестированы по-отдельности, но из-за ошибки в контроллере или мидлвари/http-модуле в итоге что-то не работает, причём только на этом конкретном сценарии
mayorovp
28.12.2016 13:39+2Да, но это имеет смысл только в интеграционном тесте. Нет смысла тестировать конвейр сам по себе, замокав все этапы.
gorlanovS
28.12.2016 16:58Если правильно вас понял, то считаю что замокав все этапы, мы всё равно можем убедиться что все этапы вызываются как мы и хотели с нужными параметрами. Для меня это преимущество, что это просто дешевле при разработке запустить тест, чем ждать пока поднимется всё окружение с микросервисами чтобы сделать один запрос и узнать что не работает, потому что не ту переменную передал.
justserega
28.12.2016 17:06+1Я согласен, что пример не очень корректен. Сложно придумать простой пример — получается или сложно или противоречиво.
И тестировать надо этот самый отдельный класс!
Представьте, что вы тестируете тот самый класс, вместо моего примера.mayorovp
28.12.2016 17:20Представил.
[Test] public void IsPrefferedUserGetDiscount() { Assert.AreEqual(47.5, BL.GetPrice( new Product { Price = 50 }, new User { IsPreffered = true } )); }
justserega
28.12.2016 18:00Ну и зависимости от базы тут нет ) А вы в базу ведь еще и пишите, представьте пример с записью в базу.
mayorovp
29.12.2016 09:45+2Так я об этом и говорю! Не должна бизнес-логика лезть в базу, ни прямо, ни косвенно.
У вас именно потому и возникли проблемы в тестировании, что вы не отдлелили код, который запрашивает из базы данные, от кода который вычисляет цены.
justserega
29.12.2016 10:20-1Кому не должна? Я о том и говорю, что это трудоемко их разделять и это тащит за собой ворох проблем.
mayorovp
29.12.2016 10:33+1Не видел у вас ничего по поводу трудоемкости их разделения и связанных проблемах.
sshikov
28.12.2016 13:44+7Вот только не стоит так обобщать.
Что-то не так не в .Net/Java и т.д., а исключительно в этом конкретном методе.
Небольшие размеры кода — вовсе не повод считать, что существующая архитектура хорошая.
Этот метод одновременно достает данные из базы, и вычисляет скидки. Вы намешали в него две совершенно разные функции. Чтож теперь жаловаться, что его сложно тестировать?
P.S. Пока писал — уже ровно тоже самое выше изложили другими словами.
KirillFormado
28.12.2016 14:03+3У тестирования на настоящей базе есть другие недостатки, кроме создания и поддержки скриптов инициализации минимального набора данных.
Unit тесты можно прогнать любой разработчик сразу, локально и быстро. Для прогона интеграционных тестов, как правило, надо поплясать с настройками локального окружения разработчика.
Написание нового интеграционного теста может занимать больше времени, чем любого unit теста, из за возни с предварительной настройкой окружения. И сами тесты могу проходить далеко не быстро, а значит их скорее всего не будут запускать локально вообще или не будут запускать часто.
Они могут падать по причине не связанной с самим тестируемым кодом (были проблемы с соединением с базой, кэшом в redis), а значит будет меньше реакция на упавший интеграционный тест в CI, чем на упавший unit тест.
Из этого следует, что если какой то метод содержит сложную бизнес логику с множеством кейсов, то писать на каждый такой кейс интеграционный тест не выгодно, так скоро время выполнения всех тестов может перевалить за часы.
Как представляется общая картина, сложную бизнес логику лучше выделить в классы которые можно спокойно покрыть необходимым количеством unit тестов. Общую работоспособность фичи можно проверить несколькими интеграционными тестами. А еще можно использовать end to end тестирование которое проверяет какой то конкретный бизнес кейс включающий в себя новую фичу.
Получается такая пирамида тестовtundrawolf_kiba
28.12.2016 15:04Получается такая пирамида тестов
Только в ISTQB последний уровень пирамиды разделяется на системное тестирование и приемочное тестирование.
am-amotion-city
28.12.2016 14:05Для этого есть решение: начальные «фикстуры» — текстовые файлы (чаще всего в json)
Не только.
mayorovp
28.12.2016 14:16И как оно сможет решать проблему инициализации тестовой БД множеством записей с зависимостями?
am-amotion-city
28.12.2016 14:21Оно еще кофе варить не умеет. «Множество записей» только что породила ваша невнимательность в следовании контексту. В заметке была фраза «множество зависимостей».
Но и множество записей — не проблема в языках, снабженных конструкцией «go to» (или любым циклом).
mayorovp
28.12.2016 14:25Нет, "множество записей" было порождено решаемой задачей. Напомню ее:
Надо заполнить различимыми данными тестовую реляционную БД, чтобы можно было проверять работу модулей, делающих к ней запросы.
am-amotion-city
28.12.2016 14:31Мне недосуг сейчас объяснять тривиальные вещи. Я поделился с автором ссылкой на инструмент, позволяющий не использовать фикстуры.
Нужны различимые данные — используйте генератор.
Нужно много записей — воспользуйтесь циклом.
Основной смысл заключается в том, что фабрика умеет создавать гроздь объектов одним вызовом: если у инстанса
user
обязательно должен быть зависимый инстансprofile
с ключом в базе, фабрика создаст их обоих. Различимость данных достигается другими, тоже легко находимыми в поисковых системах, средствами.mayorovp
28.12.2016 15:04А если у двух user должен быть общий department?
am-amotion-city
28.12.2016 15:07-1Вы напрасно думаете, что я — это такой бесплатный интерактивный туториал.
Сделайте фабрику
department
с трейтомwith_two_users
. Или, наоборот,user
с трейтомwith_crowded_department
.mayorovp
28.12.2016 16:04"С трейтом"? "Фабрику"? Вы точно говорите о вот этом файле о тридцати трех строках?
Вы напрасно думаете, что я — это такой бесплатный интерактивный туториал.
Нет, я думаю что вы по-прежнему не понимаете сложность задачи.
am-amotion-city
28.12.2016 16:20-1А, ну значит для
.net
нормальной имплементации нет, пардон. Я заглянул проверить толькоjava
-версию, и то поверхностно.
Я прекрасно понимаю сложность задачи, просто я думал, что в мире
.net
тоже есть люди, способные нормально скопировать функциональность с оригинала и выложить в открытый доступ. Оказалось, ошибался.
Frank59
28.12.2016 14:09+7Статья из разряда вредных советов.
justserega
28.12.2016 17:24Статья задумывалась как слегка провокационная (может переборщил). Только эти вредные советы вполне себе обычный подход во многих фреймворках, например django. Я думаю строго-типизированным языкам вполне есть чему поучиться у динамических.
Bringoff
30.12.2016 09:13+1Я думаю строго-типизированным языкам вполне есть чему поучиться у динамических.
Как в том методе вы смешали работу с базой и вычисление цены, так здесь смешиваете в кучу языки и фреймворки. Не надо так.
medvedevia
28.12.2016 15:27Для этого есть решение: начальные «фикстуры» — текстовые файлы (чаще всего в json), содержащие начальный минимальный набор данных. Большим минусом такого решения является необходимость поддерживать эти файлы вручную (изменения в структуре данных, связь начальных данных друг с другом и с кодом тестов).
Не, наполнение базы справочной информацией это тоже тест. Т. е. в данном примере тест должен был начинаться с добавления контрагентов, товаров, установки цен и только потом где-то тестироваться список товаров.mayorovp
28.12.2016 16:07Так откуда базу наполнять-то? Или вы предлагаете сделать несколько тестов, которые можно запускать только в строго определенном порядке?
medvedevia
28.12.2016 21:08Да! Ведь тесты в идеале должны покрывать всю функциональность. Контрагенты откуда в базе появляются? Вот на эту операцию нужен тест. На добавление товаров тоже нужен тест, и на установку цен, на все нужен тест в идеале. Нельзя протестировать отгрузку со склада, если на складе нет остатков, нужно сначала оприходовать товар, а потом уже можно и списать. Какие-то тесты нужно запускать в определенном порядке, а какие-то необязательно.
justserega
28.12.2016 22:18Каждый тест независим и начинает практически с чистого листа. Базовые вещи, которые не влияют на функциональность можно залить одинаковые для всех. Вся специфика задается в самом тесте, разумеется нужны хелперы, с помощью которых легко это будет сделать.
mayorovp
29.12.2016 09:47+1Такой подход сильно затрудняет отладку. Вот вызывается подряд 20 тестов, двадцатый — красный. Я запущу двадцатый тест в отладчике и пройдусь по шагам. А вам придется запускать в отладчике все тесты.
medvedevia
29.12.2016 14:51Нет, Вы неправильно поняли. Тестов должно быть по максимуму, и перед релизом их нужно все прогонять, надеюсь тут разногласий нет. Но для отладки можно один раз выполнить 19 тестов (или взять готовую базу) и потом многократно запускать 20 тест.
mayorovp
29.12.2016 15:52У вас в общем случае не получится многократно запускать тест, который меняет внешнее состояние.
medvedevia
29.12.2016 16:06А у вас получится чтоли?)) По факту руками откатываю состояние, если нужно.
Я вообще уже не понимаю о чем спор, мы же говорим о том, что тест использует настоящую базу, так ведь? И при этом вы утверждаете, что я не могу повторно запустить тест, а вы можете?))mayorovp
29.12.2016 16:14А у меня все тесты запускаются на чистой базе. И откатывают транзакцию в конце.
Да, такие тесты можно запускать сколько угодно раз и в любом порядке.
justserega
28.12.2016 22:16Каждый тест начинает работать с чистой базой, в нее заливается базовый набор справочников, остальные таблицы заполняются на этапе Arrange в самом тесте.
SergeyVoyteshonok
28.12.2016 15:44+3У вас DbContext должен инжектиться, а не репозиторий. При таком подходе и репозиторий можно протестить. Неправильная архитектура. Если используете EF для мока Db подойдет InMemoryDb.
Lailore
29.12.2016 08:41+1Это вредный совет. У нас например общая доменная база мобильного приложения и веб сайта/апи. Если последовать вашему совету, то все грустно получается.
SergeyVoyteshonok
29.12.2016 15:27Объясните подробно свою позицию, почему грустно получается? Т.е. по вашему хардкодить DbContext в репозитории как в примере это хорошо?
Lailore
30.12.2016 08:39У каждой платформы свой репозиторий со специфичными для нее зависимостями. Например в веб аби это может быть ms sql, на мобилках это sql lite.
А так как в весь код доменки инжектиться репозиторий(через интерфейс), то доменка не замечает подмены. + в вебе используется декоратор для кеша и/или проверки прав, а на мобилке это ненужно.
verysimplenick
28.12.2016 17:25Чем не устроил Xunit + class fixture + Collection на тесты с бд? Можно и на некоторые тесты с навешивать очередность. А бд с тестовыми данными разворачивать из sql файла лежащего в ресурсах. Зачем тут json?
justserega
28.12.2016 17:26Мы json как раз не используем, в одну статью все не влезло — получилось слишком длинно. Опубликую наше решение — там как раз, что-то типа class fixtures. Из sql неудобно, там либо много лишнего из продакшн базы… либо тяжело поддерживать руками в sql по мере развития системы
DGolubets
28.12.2016 17:25+3Ну конечно, в «Hello world» любое разделение кода будет выглядеть ненужным размножением сущностей. В реальном приложение у вас не 3 строчки кода же?
Подход с репозиториями — проверенный временем. Не спроста о нем в кинжках пишут.
Пара советов для правильной реализации в контексте .NET:
- Отключите LazyLoad
- Возвращайте IEnumerable либо конкретные типы
Тогда не будет у вас leaky abstractions и будет счастье.
А если удариться в демагогию, то кругом компромиссы.
Нет идеального решения, такого чтобы и писать мало кода и все замечательно тестировалось и поддерживалось.
Вы сами должны решить где для вас наилучший баланс.
Однако он там будет только для вас и конкретно вашего проекта.
Не надо сразу евангелизировать это.justserega
28.12.2016 17:43Я за разделение кода по ответственности, здесь разделение кода идет только потому что мы решили, что реальная база это слишком сложно.
— Отключение LazyLoad не спасет от того, что я забуду прописать Include в репозитории и все упадет
— Сложно мокать графы объектов (особенно если проект динамично развивается)
— IEnumerable — не понял чем спасет
Не надо сразу евангелизировать это.
Согласен, тоже самое касается и DI. Я честно пробовал подход с зависимостями… это можно описать как «ежики плакали, кололись, но продолжали есть кактус». Тестов было мало и они не гарантировали, что все окей. Сейчас мы перешли на реальную базу — тестирование из мучения превратилось в удовольствие.mayorovp
29.12.2016 10:01Отключение LazyLoad позволит коду в случае непрописанного Include быстро упасть во время прогона интеграционных тестов. А вот включенный LazyLoad в аналогичном случае тихой сапой неприлично замедлит программу.
С другой стороны, во время разработки первых версий программы (когда главное — чтобы она работала в принципе) LazyLoad действительно сильно помогает.
IEnumerable спасает от формирования запроса к БД в произвольных местах кода, тем самым прекращая "протечку" ответственности за пределы слоя DAL. В итоге при необходимости сменить схему БД — достаточно переписать один слой. В противном же случае приходится переписывать всю программу.
justserega
29.12.2016 10:58Интеграционных тестов
Так мы же про unit говорим?
IEnumerable спасает от формирования запроса к БД
IEnumerable не спасает от этого.
mayorovp
29.12.2016 12:29Не выдергивайте цитаты из контекста. IEnumerable спасает от формирования запроса к БД в произвольных местах кода.
justserega
29.12.2016 13:00Возврат DbQuery объекта по интерфейсу IEnumerable не заставляет его выполниться (он будет выполненен только при перечислении — как раз в произвольном месте). Нужно возвращать List или array
mayorovp
29.12.2016 13:06А при чем тут выполнение? Речь идет о формировании.
justserega
29.12.2016 13:14Это одно и то же в данном случае. В EF же нет кэширования сформированных запросов.
var iter = (IEnumerable<Model>)db.Models.Where(x => x.ParentId == 5);
Вот тут ничего не происходит, просто в iter у вас лежит тот же DbQuery и запрос будет формироваться и выполняться, каждый раз при итерировании по iter.
mayorovp
29.12.2016 13:10+3Вернув IEnumerable, мы запрещаем более высоким слоям дописать к запросу к БД парочку условий в WHERE, добавить JOIN и группировку.
Благодаря этому запрету появляется возможность в дальнейшем вернуть кешированный список, использовать хранимую процедуру или отрефакторить зависимости (скажем, заменить три параллельные связи один-ко-многим на одну связь многие-ко-многим).
Также этот запрет позволяет отделить хранимые сущности от возвращаемых, что в свою очередь позволяет вовсе кардинально сменить структуру хранения.
Danik-ik
28.12.2016 22:59+1Оптимистам от модульного тестирования и хорошей архитектуры могу рассказать, что если на вас внезапно падает поддержка и динамичное развитие спагетти-хардкода, то без тестирования на живой БД в принципе не обойтись :).
И когда основное предназначение ПО — обмен данными, сиречь заумный экспорт данных из БД в 100500 форматов для кучи получателей, у каждого из которых — собственная гордость, и где бОльшая часть того, что надо тестировать — рабочие SQL запросы, тоже нельзя обойтись без БД, как бы меня ни убеждали, что модульные тесты важнее и круче.
Поэтому любая информация о том, как другие делают «это» (автоматизацию тестирования на реальной БД) вызывает у меня жгучий интерес. Так что с нетерпением жду продолжения.
Что же касается неразмножения сущностей и удобства поддержания тестов — давно уверен, что единообразные тесты надо не хардкодить, а описывать в виде метаданных и формировать на лету. И именно этим я займусь после праздников.ggo
29.12.2016 13:04+1БД — это Data. Сервер приложений — это Application. Браузер — это Presentation.
У каждого слоя свои сложности с разработкой и тестированием.
Очевидно, переносимость подходов к разработке и тестированию в этих разных слоях хоть и коррелирует, но слабо.
Это не отменяет полезности юнит-тестов в слое Application.
И это не отменяет полезности интеграционных тестов, когда все слои собраны вместе.
И это не отменяет все прочие подходы к верификации качества, каждого слоя в отдельности, так и разного их сочетания.
drcolombo
30.12.2016 00:18+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) баз. Это дает довольно хороший бонус — получается, что мы всегда можем сделать «срез» работы системы, основываясь на реальных данных, как система ведет себя во время каждого шага обработки/генерации данных.
PashaPash
29.12.2016 13:18+2Настоящая проблема с примером из первой части в том, что вы быстро набросали полностью рабочий вариант «самым простым способом». Наверняка даже протестировали его вручную. Он готов. И все телодвижения, которые вы делаете после этого, не добавляют продукту, в котором этот код написан, никакой ценности. Если хочется написать покрытый тестами код — то стоит писать тесты до кода.
Тесты после кода, особенно если под них приходится править свеженаписанный рабочий и уже протестированный (запущенный пару раз) код — это пустая трата времени.
Вы с большей вероятностью добавите багов, чем почините. Видно даже на вашем примере — вы используете `IEnumerable` в репозитории, что вызывает выполнение запроса и материализацию продуктов прямо в момент вызова `GetFeaturedProducts`, а не позже, как это кажется по коду (и как происходило в оригинальном примере).
Попробуйте расписать свой же пример, но через написание двух тестов до кода — «Необходимо отобразить простой список рекомендуемых товаров» и «если список просматривает привилегированный пользователь, то цена всех товаров должна быть снижена на 5 процентов» — это покажет тесты с лучшей стороны.
Интеграционные тесты на базе — не панацея. У них есть один огромный недостаток — они медленные. Т.е. они кажутся хорошей идеей, пока их меньше 10000 штук. Потом они начинают занимать час даже на приличном железе, и вам придется вкладываться в инфраструктуру для билдов. К тому же разработчики перестанут запускать тесты локально и начнут пушить со словами «мне повезет».PashaPash
29.12.2016 13:55Насчет материализации — я имел ввиду именно факт дополнительного создания еще одного набора (отслеживаемых?) объектов Products, из которых уже потом Linq to Objects делает Select, а не собственно выполнения запроса. Суть в том, что при переписывании готового кода поменялся механизм выполнения запроса, возможно не в лучшую сторону, никто (по легенде поста) это не заметил, и все это подперто одним (!) тестом.
justserega
29.12.2016 14:39-1Ценность тестов в том, что код не сломается в будущем и в этом их основное назначение, а не в том, что они магически вам построят архитектуру. И так думаю не только я — Does TDD lead to good design
Медленность тестов с базой это конечно плохо, пока у нас намного меньше тысячи…PashaPash
30.12.2016 15:54У меня интеграционных тестов > 8000. Оцените время выполнения при использовании вашего фреймворка :) Медленность тестов с базой — это ужасно плохо.
TDD и «писать тесты до кода» — разные вещи. Я не приверженец TDD, и не предлагаю вам строить архитектуру только на основании тестов. И вообще (вынуждненно, много легаси кода) предпочитаю интеграционные тесты. Но их можно писать до кода, а можно — после. Одни и те же тесты на один и тот же код могут приносят разный профит в зависимости от момента их написания. И дают разный эффект на мотивацию.
Тесты после кода:
[время на написание кода] + [время на ручной протык разработчиком и фикс] + [время на написание тестов]
Недостатки:
- Разработчик пишет сразу большие куски функционала, которые нужно покрывать несколькими тестами (как у вас в статье). Из-за этого вы получаете меньше тестов, с дырками в покрытии (уберется необходимость скидки — удалят тест — не будет покрыт кейс выборки без скидки)
- Разработчик воспринимает код после протыка и фикса как «готовый» — и подсознательно считает тесты ненужными, или считает их «дополнительной работой», а не неотъемлимой частью кода. Которая поможет в будущем кому-то (не ему) вносить изменения.
Тесты до кода:
[время на написание теста] + [время на написание кода]
Тот же код, те же тесты — но нет необходимости в активном ручном протыке. И нет ощущения, что вы делате «дополнительную» работу за других. Тесты начинают помогать разработчику вот прямо сейчас, а не когда-нибудь потом. Это прямой стимул, который заодно создает сайдэффект «код не сломается в будущем». Win-Win.
Внедрение тестов на проекте — это проблема психологии, а не техническая (как писать и запускать) и не менеджерская (выделить время). Нужно сделать так, чтобы разработчики чувствовали себя неуютно при попытке нафигачить код без тестов. Им должно быть физически неудобно это делать. «Лень запускать UI и проверять руками, проще тест написать» — вот эффект, который нужно получить.
Попробуйте писать свои интеграционные тесты до кода. Почувствуете разницу.justserega
30.12.2016 16:08В такой формулировке согласен полностью. А вы не искали какой-нибудь эмулятор БД? Единственное, что нашел это h2, но непонятно насколько точно он эмулирует
PashaPash
31.12.2016 00:43+1Зависит от проекта. Когда-то давно пытались прикручивать самописный эмулятор — работало для простых запросов, но на чем-то сложном начинались проблемы. Т.е. для простых запросов оказалось удобнее использовать репозиторий (как в статье) и мокать его. Для сложных — наткнулись на разницу эмулятора и реальной базы.
На текущем проекте — в базе лежит часть логики (права доступа, хаки для иерархических данных и прочее) — и интеграционные тесты требуют реальной базы. Вынести логику из базы полностью не получится (точнее, получится, но работать будет гораздо медленее). И ORM — не EF :)
drcolombo
30.12.2016 00:22Интеграционные тесты на базе — не панацея. У них есть один огромный недостаток — они медленные. Т.е. они кажутся хорошей идеей, пока их меньше 10000 штук. Потом они начинают занимать час даже на приличном железе, и вам придется вкладываться в инфраструктуру для билдов.
Как писал чуть выше — у нас порядка 1050 тестов, из которых порядка 700 — интеграционные, т.е. использующие базу. Прогон в среднем около 15 минут.
К тому же разработчики перестанут запускать тесты локально и начнут пушить со словами «мне повезет».
1. линейкой по рукам
2. отрывать руки
3. увольнять
(пункты 2 и 3 можно поменять местами по желанию :)
Если что — про п.3 я серьезно. У нас, правда, пока прецедентов не было, но случаи лишения годового бонуса были, а это почти две зарплаты может быть, что довольно быстро учит нерадивых «везунчиков».sentyaev
30.12.2016 05:17+11. линейкой по рукам
2. отрывать руки
3. увольнять
Это сработает только в такой ситуации как у вас, когда цена ошибки — огромный штраф.
Обычно это не так.
Вот у меня все проще, поэтому интеграционные тесты не запускаю (ибо 10 мин), запускаю только те, что сам только что написал. Если что-то пошло не так, мне прилетит от билд сервера.
Я это все к тому, что разработчикам не особо нужно запускать тесты (я имею ввиду ВСЕ тесты), для этого есть CI.
PashaPash
30.12.2016 15:04+315 минут на 1000 тестов — это ужасно долго. У меня на проекте 12800 тестов, из них — 9700 серверных, из которых больше 8000 — интеграционные на базе (так сложилось). Время прогона — 20 минут на CI, примерно 15 минут — на дев-машине.
Если бы мы не вложились в CI (билд сервера с SSD + много локальных хаков в тестовом фреймворке) — мы бы получили почти 3 часа на прогон тестов. Вы готовы ждать 2-3 часа перед каждым пушем? А потом еще 3 часа на прогонку на билд-сервере? Ок, у нас на проекте мы смогли вставить хаки и сократить время выполнения. Но я не уверен, сможете ли вы сделать то же самое в своем коде.
Fear-driven development? Бить по рукам, лишать премии и прочие кнуты может позволить себе только галера, на которой студентов держат за счет того, что они не знают другой жизни и боятся поменять работу. На продуктовой разработке это просто убьет мотивацию и принесет больше вреда, чем пользы.
Если у вас разработчиков приходится лишать премии за нарушение принятого процесса — значит они не понимают, зачем конкретные практики в вашем процессе приняты. И битьем по рукам вы только усугубите ситуацию — спрячете проблему, загоните ее внутрь — но не решите.
Есть более гуманные способы — например снизить потери от «красных» коммитов активным бранчеванием (поломает дев свою ветку — всех остальных это не затронет). Процесс должен помогать разработчикам творить — создавать продукт. А не держать их в узде.drcolombo
30.12.2016 19:57+1Нет, не понятно, что я несколько утрировал (ну или иносказательно выразился касаемо «битья по рукам»), т.к. таки вся команда и каждый разработчик должны понимать, чем чреваты их красные билды и отмазываться «авось прокатит» банально не профессионально.
15 минут на 1000 тестов — это ужасно долго
Это на дев-машине, на CI порядка 3-5. Но я понимаю, что и это очень долго, т.к. где-то через год объем функционала может вырасти раза в три и количество тестов, боюсь, соответственно.
Но тем не менее, боюсь, что особо улучшать производительность наших тестов некуда, ибо логика в них такая, что часто запускается несколько потоков на обработку сообщений, а они порой не могут быть обработаны сразу при получении, т.к. зависят от других сообщений, которые биржа еще не послала (я же говорю, что формат «мама, роди меня обратно»)… к примеру, сообщение, инициирующее трейд, может прийти через минуту или две после получения сообщения об изменении или закрытии трейда. Соответственно, и система должна это обрабатывать и тесты должны все это проверять. Т.е. порой задержка в тесте нужна для проверки как раз таки логики работы.
В «нормальном» приложении, думаю, нет надобности в таких свистоплясках и 15 минут на 1000 тестов — действительно много.
снизить потери от «красных» коммитов активным бранчеванием (поломает дев свою ветку — всех остальных это не затронет)
Ваши слова даБогунашему IT в уши… У нас TFS (хоть и 2015, в котором внутри есть git, но git под запретом), в котором так просто бранч не сделаешь — надо согласовать, создать тикет, получить добро по всей иерархии… Но локально особо продвинутые держат все в локальном git репозитории и делают коммит в TFS только master'а, ну а закоренелые консерваторы так и сидят на «чистом» TFS.mayorovp
31.12.2016 17:06Если у вас проблема именно что в задержках — тут поможет "виртуальное время". Нужен инструмент, который позволит быстро перематывать время вперед в тестовой среде.
Где-то я видел инструменты тестирования, которые позволяют подменить системные классы в тестах.
Или же можно сделать свой велосипед, написав свой слой доступа к времени и запретив использовать стандартные средства через инструменты статического анализа.
sentyaev
30.12.2016 05:01+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, он как раз об этом.justserega
30.12.2016 06:11Спасибо, за такой подробный ответ! Про пример в статье я уже говорил — он из книги про DI. Очень сложно с примером оказалось — либо много кода, либо все стараются подвергнуть критике сам пример.
Интересный подход, попробую примерить его на наши проекты. А как на счет ссылок между контекстами, EF поддерживает такое?mayorovp
30.12.2016 09:04Между контекстами — нет, не поддерживает. А вот между сущностями — поддерживает даже если некоторые из них не имеют своих DbSet.
justserega
30.12.2016 09:10А миграции тоже работают при многих контекстах с перекрестными ссылками?
mayorovp
30.12.2016 09:34Смотря что понимать под «работают». Определенно миграции для разных контекстов, ссылающихся на одни и те же таблицы, будут конфликтовать друг с другом. Решением может быть дополнительный контекст только для миграций, содержащий в себе все таблицы.
sentyaev
30.12.2016 14:00+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, то у них не должно быть прямой связи, но приложение может спокойно использовать сразу несколько контекстов (как в моем примере).
vaniaPooh
Ок. Работаем дальше.