Здравствуйте, ·, Вы писали:
·>Ну например: "findUsersByMailDomainAndGeo"
·>1. должно среди данного списка юзеров найти только совпадающих по домену и локации — это аспект поведения.
·>2. должно комбинировать findUsersByMailDomain и findUsersByMailGeo предикатом and — это детали реализации.
·>Я предлагаю тестировать 1, а ты топишь за 2.
·>Причём тут ширина?
Ширина при том, что findUsersByMailDomainAndGeo имеет как минимум четыре "особых точки" в зависимости от того, какой из критериев домена или географии установлен в "any".
Если мы продолжим наворачивать туда критерии (как оно обычно бывает в системах с "настраиваемой отчётностью"), то очень быстро окажется, что у нас нет ресурсов для выполнения e2e по всем-всем мыслимым комбинациям.
·>Может быть, и тест это проверить никак не может.
Ну, то есть проблема всё же в дизайне, а не в тестах.
·>А об архитектуре я речь пытаюсь не вести, но вы всё не в ту степь сворачиваете.
Ну, если ключи искать под фонарём — тогда да.
·>Я не тестирую реализацию, я тестирую что метод ожидается должен делать.
·>Что "это непрактично" и смысла обсуждать не имеет.
Т.е. и без таких наворотов мы получаем приемлемое качество. Ч.Т.Д.
S>>И вы, кстати, делаете точно так же — ваши моки описывают совершенно конкретный дизайн системы, который никакого отношения к бизнес-требованиям не имеет.
·>Давай определимся, "findUsersByMailDomainAndGeo" — имеет или нет?
Имеет конечно. У нас в бизнес требованиях указано, что могут быть ограничения на почтовый домен (по точному совпадению, по частичному совпадению) и по географии (на уровне зоны/страны/региона/населённого пункта). Если задано более одного ограничения — применяются все из них.
·>Зачем? Сразу после выделения (т.е. рефакторинг прод-кода) — тесты не трогаем, они должны остаться без изменений и быть зелёными.
Они не могут остаться без изменений; у вас там стратегию нужно замокать. Ну, вот как с датой — перенос её в TimeSource требует от вас в рамках теста подготовить нужный источник времени.
S>>Опять же — вот вы приводите пример оптимизации, которая типа ломает addLimit. Ну, так эта оптимизация — она откуда взялась? Просто программисту в голову шибануло "а перепишу-ка я рабочий код, а то давненько у нас на проде ничего не падало"? Нет, пришёл бизнес и сказал "вот в таких условиях ваша система тормозит. Почините". И программист починил — новая реализация покрывает новые требования.
·>Ну да. Но все старые тесты остаются как есть, зелёными. Создаём новые тесты для новых требований, но не трогаем старые. Когда задача завершена, и очень хочется, то можно проанализировать какие тесты стали избыточными и их просто выкинуть.
Точно так же, как и в FP-случае.
·>Это не избавляет от необходимости тестировать части в сборе. И проблема в том, что тестировать весь конвеер целиком "но ресурсные ограничения есть". Упс.
Конвеер тестируется в длину, а не в ширину. Для этого достаточно 1 (одного) теста.
·>Нет.
·>Всё верно. Суть в том, что надо тестировать не умение субд, не порождение запросов, не доезжание, а что findUsersByMailDomainAndGeo действительно ищет юзеров по домену и гео. А как он это делает, через sql, через cql, через linq или через чёрта лысого — пофиг.
В теории — да, но на практике "через чёрта лысого" вы просто не покроете тестами никогда.
S>>Это иллюзия.
·>Иллюзия чего? Я вижу, что пример правильный — это любой дебил неиллюзорно видит.
S>>Может быть, find() просто делает return empty().
·>Да пускай, пофиг. Зато этот конкретный сценарий — правильный, и он будет всегда правильный, независимо от реализации, даже для return empty, это инвариант — и это круто.
S>>И даже если вы добавите assert find(vasya) == vasya, это мало что изменит — потому что нет гарантии, что find(vasilisa) != vasya.
·>Конечно. Но ты забыл, что никакие тесты такую гарантию дать и не могут. Твои тоже.
Формально — да, не могут.
На практике мы можем проверить не только статус тестов, но и test coverage. В общем случае по test coverage никаких выводов делать нельзя, т.к. результат зависит от путей исполнения. Но когда мы принудительно линеаризуем исполнение, из code coverage можно делать выводы с высокой степенью надёжности. Вот в вашем примере про добавление оптимизированной ветки в query — ну и прекрасно, она у нас уедет в buildWhere, и там мы проверим результаты тестами, а code coverage покажет нам, что для новой ветки тестов недостаточно. При этом addLimit останется покрытым, потому что он добавляется в отдельном от ветвления между "основным" и "оптимизированным" вариантами месте.
S>>Чтобы нормально покрыть тестами ваш find(), придётся отказаться трактовать его как чёрный ящик.
·>Нет. Тесты — продукт corner case анализа, документация к коду, а не инструмент доказательства корректности и обеспечения гарантий.
Хм. А как вы обеспечиваете гарантии?
·>И то, и то плохо. Если покрасился в красный, это значит мы что-то меняем в поведении и это может привести к неожиданным изменениям у юзеров.
S>>Бизнес-аналитик не будет вам писать тесты. Увы.
·>Не писать, но если ему задать вопрос который описывает given-часть теста — то аналитик ответит какой правильный результат мне прописать в verify-части — contains или notContains.
А если ему задать вопрос про то, почему должен быть такой результат — он скажет, каким должен быть оператор
·>Корректный в смысле синтаксис не сломан? Да пофиг, такое действительно не особо важно проверять.
Синтаксис и семантика.
·>Комбинация чего? И почему именно этого?
комбинация предикатов по домену и по географии. И потому, что это задано бизнес-требованиями.
·>Комбинировать можно столько всего... Проверять нужно что эта самая комбинация — та, которая ожидается.
S>>Из них строятся комбинации, тоже простые.
·>Из тыщи фрагментов можно построить 21000 комбинаций. Но только сотня из них — та, которая нужна.
Ну вот нам достаточно один раз описать способ построения этой комбинации, и в дальнейшем проверять именно его.
·>Да пожалуйста, строй как хошь. Речь о том как это тестировать.
Вроде всё написал.
·>моки это не про state, а про передачу параметров. Для тестирования репо+бд моки не нужны вообще-то. Моки нужны для экономии ресурсов.
Эмм, ваша "модельная БД" — это тоже мок. Нет никакой разницы с т.з. архитектуры тестирования, то ли вы пишете мок вручную и хардкодите в него ответы на find(vasya), то ли вы его описываете декларативно с помощью мок-фреймворка, то ли берёте "квазинастоящую БД" и напихиваете данными перед тестом.
Это всё — про одно и то же "у меня есть stateful dependency, и я моделирую этот state перед каждым тестом".
·>Что показать?
Код.
·>У вас тоже они где-то есть.
Они — на самом верху, в конце конвеера, и их достаточно проверить при помощи немногих e2e тестов.
S>>Ну всё правильно — и вместо N + M тестов вы будете писать N*M, потому что в реализации этого метода вы не будете повторно использовать код ни findUsersByMailDomain, ни findUsersByGeo.
·>Почему не буду?
Потому что в stateful OOP реализации невозможно повторно использовать код этих методов. Они возвращают некомбинируемые результаты.
Ну, то есть формально-то они комбинируемые — можно взять два набора Result, и выполнить их пересечение за O(N logM), но на практике это приведёт к неприемлемой производительности.
S>>А вы по факту останетесь без тестов, т.к. N*M вы писать вспотеете, и останетесь с набором зелёных тестов даже в случае косяков в corner cases.
·>Фантазии.

Жизненный опыт.
S>>Так и тут — если уж мы решили провести границу тестирования вокруг произвольно выбранного нами компонента системы — скажем, репозитория, то что нам помешает порезать этот репозиторий на K компонент?
·>Ну "findUsersByMailDomainAndGeo" — это поведение или что? А вот contains("top(10)") — это по-моему совершенно не поведение.
Поведение.
·>Ну так да, уж несколько месяцев объясняю, что код похожий. Тесты другие.
Ну, штош.