Как стать автором
Обновить

Комментарии 101

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


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


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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

Представил.


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

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


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

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

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

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

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

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

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

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

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

Только в ISTQB последний уровень пирамиды разделяется на системное тестирование и приемочное тестирование.
НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

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


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

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

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


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

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

НЛО прилетело и опубликовало эту надпись здесь
Статья из разряда вредных советов.
Статья задумывалась как слегка провокационная (может переборщил). Только эти вредные советы вполне себе обычный подход во многих фреймворках, например django. Я думаю строго-типизированным языкам вполне есть чему поучиться у динамических.
Я думаю строго-типизированным языкам вполне есть чему поучиться у динамических.

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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




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

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

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


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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

Расскажу в кратце, как мы делаем «это». Приложение (очень в кратце, ибо 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) баз. Это дает довольно хороший бонус — получается, что мы всегда можем сделать «срез» работы системы, основываясь на реальных данных, как система ведет себя во время каждого шага обработки/генерации данных.
Настоящая проблема с примером из первой части в том, что вы быстро набросали полностью рабочий вариант «самым простым способом». Наверняка даже протестировали его вручную. Он готов. И все телодвижения, которые вы делаете после этого, не добавляют продукту, в котором этот код написан, никакой ценности. Если хочется написать покрытый тестами код — то стоит писать тесты до кода.

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

(пункты 2 и 3 можно поменять местами по желанию :)
Если что — про п.3 я серьезно. У нас, правда, пока прецедентов не было, но случаи лишения годового бонуса были, а это почти две зарплаты может быть, что довольно быстро учит нерадивых «везунчиков».
НЛО прилетело и опубликовало эту надпись здесь
15 минут на 1000 тестов — это ужасно долго. У меня на проекте 12800 тестов, из них — 9700 серверных, из которых больше 8000 — интеграционные на базе (так сложилось). Время прогона — 20 минут на CI, примерно 15 минут — на дев-машине.

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

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

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

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

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

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

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

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

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


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


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

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

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

НЛО прилетело и опубликовало эту надпись здесь
Спасибо, за такой подробный ответ! Про пример в статье я уже говорил — он из книги про DI. Очень сложно с примером оказалось — либо много кода, либо все стараются подвергнуть критике сам пример.

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

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

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


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

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории