Здравствуйте, Sinclair, Вы писали:
S>·>Причём тут ширина?
S>Ширина при том, что findUsersByMailDomainAndGeo имеет как минимум четыре "особых точки" в зависимости от того, какой из критериев домена или географии установлен в "any".
Ты ж вроде сам расписал как это линеаризуется.
S>Если мы продолжим наворачивать туда критерии (как оно обычно бывает в системах с "настраиваемой отчётностью"), то очень быстро окажется, что у нас нет ресурсов для выполнения e2e по всем-всем мыслимым комбинациям.
Ну не наворачивай.
S>·>Может быть, и тест это проверить никак не может.
S>Ну, то есть проблема всё же в дизайне, а не в тестах.
Именно. У ваших тестов совершенно другая проблема.
S>·>Что "это непрактично" и смысла обсуждать не имеет.
S>Т.е. и без таких наворотов мы получаем приемлемое качество. Ч.Т.Д.
Угу, не знаю к чему ты завёл речь о системе типов. Это тоже оффтоп.
S>·>Давай определимся, "findUsersByMailDomainAndGeo" — имеет или нет?
S>Имеет конечно. У нас в бизнес требованиях указано, что могут быть ограничения на почтовый домен (по точному совпадению, по частичному совпадению) и по географии (на уровне зоны/страны/региона/населённого пункта). Если задано более одного ограничения — применяются все из них.
Ну вот, это и надо тестировать. А факт того, что "применяются все из них" — говорит о том, что никакого взрыва комбинаций тестировать не надо, т.к. код линейный. Достаточно протестировать каждый критерий отдельно.
S>·>Зачем? Сразу после выделения (т.е. рефакторинг прод-кода) — тесты не трогаем, они должны остаться без изменений и быть зелёными.
S>Они не могут остаться без изменений; у вас там стратегию нужно замокать. Ну, вот как с датой — перенос её в TimeSource требует от вас в рамках теста подготовить нужный источник времени.
Если существующий тест как-то умудрялся работать без подготовленного источника, то он должен работать ровно так же и с подготовленным.
S>>>Опять же — вот вы приводите пример оптимизации, которая типа ломает addLimit. Ну, так эта оптимизация — она откуда взялась? Просто программисту в голову шибануло "а перепишу-ка я рабочий код, а то давненько у нас на проде ничего не падало"? Нет, пришёл бизнес и сказал "вот в таких условиях ваша система тормозит. Почините". И программист починил — новая реализация покрывает новые требования.
S>·>Ну да. Но все старые тесты остаются как есть, зелёными. Создаём новые тесты для новых требований, но не трогаем старые. Когда задача завершена, и очень хочется, то можно проанализировать какие тесты стали избыточными и их просто выкинуть.
S>Точно так же, как и в FP-случае.
Именно. ЧТД, дизайн тут непричём.
S>·>Это не избавляет от необходимости тестировать части в сборе. И проблема в том, что тестировать весь конвеер целиком "но ресурсные ограничения есть". Упс.
S>Конвеер тестируется в длину, а не в ширину. Для этого достаточно 1 (одного) теста.
Судя по тому что писал Pauel этот самый конвеер есть в каждом методе каждого контроллера. Т.е. таких конвееров туча у вас.
S>·>Всё верно. Суть в том, что надо тестировать не умение субд, не порождение запросов, не доезжание, а что findUsersByMailDomainAndGeo действительно ищет юзеров по домену и гео. А как он это делает, через sql, через cql, через linq или через чёрта лысого — пофиг.
S>В теории — да, но на практике "через чёрта лысого" вы просто не покроете тестами никогда.
Возможно, зависит от конкретноого чёрта, вот только моки тут непричём.
S>>>Может быть, find() просто делает return empty().
S>·>Да пускай, пофиг. Зато этот конкретный сценарий — правильный, и он будет всегда правильный, независимо от реализации, даже для return empty, это инвариант — и это круто.
S>>>И даже если вы добавите assert find(vasya) == vasya, это мало что изменит — потому что нет гарантии, что find(vasilisa) != vasya.
S>·>Конечно. Но ты забыл, что никакие тесты такую гарантию дать и не могут. Твои тоже.
S>Формально — да, не могут.
S>На практике мы можем проверить не только статус тестов, но и test coverage. В общем случае по test coverage никаких выводов делать нельзя, т.к. результат зависит от путей исполнения. Но когда мы принудительно линеаризуем исполнение, из code coverage можно делать выводы с высокой степенью надёжности.
Угу. Вот только это зависит от дизайна, стиля, ревью и т.п., а не от наличия моков или подходу к тестированию.
S>Вот в вашем примере про добавление оптимизированной ветки в query — ну и прекрасно, она у нас уедет в buildWhere, и там мы проверим результаты тестами, а code coverage покажет нам, что для новой ветки тестов недостаточно. При этом addLimit останется покрытым, потому что он добавляется в отдельном от ветвления между "основным" и "оптимизированным" вариантами месте.
Именно, ведь наличие addLimit в правильном месте обеспечивается не тестами, а совершенно другими средствами.
S>>>Чтобы нормально покрыть тестами ваш find(), придётся отказаться трактовать его как чёрный ящик.
S>·>Нет. Тесты — продукт corner case анализа, документация к коду, а не инструмент доказательства корректности и обеспечения гарантий.
S>Хм. А как вы обеспечиваете гарантии?
Ты же сам рассказывал: "от обучения разрабочиков, до код ревью и отбора прав" и т.п.
S>>>Бизнес-аналитик не будет вам писать тесты. Увы.
S>·>Не писать, но если ему задать вопрос который описывает given-часть теста — то аналитик ответит какой правильный результат мне прописать в verify-части — contains или notContains.
S>А если ему задать вопрос про то, почему должен быть такой результат — он скажет, каким должен быть оператор 
Не скажет в общем случае, т.к. оператор это детали реализации и специфично для яп. Про sql "between" он может и не знать и его совершенно может не волновать inlcusive он или exclusive. zero-based, one-based индексация и т.п. в итоге приводит к off-by-one ошибкам.
S>·>Корректный в смысле синтаксис не сломан? Да пофиг, такое действительно не особо важно проверять.
S>Синтаксис и семантика.
Корректность семантики проверить
тестами невозможно.
S>·>Из тыщи фрагментов можно построить 21000 комбинаций. Но только сотня из них — та, которая нужна.
S>Ну вот нам достаточно один раз описать способ построения этой комбинации, и в дальнейшем проверять именно его.
Так проблема в том, как, например, проверять опечатки в этом самом описании. Учти, эти опечатки могут быть даже в требованиях. Бизнес-аналист написал "исползуем оператор <" но перепутал левую часть с правой. Ты просто скопипастишь < в прод-код и тест-код — и всё зелёное, ибо works as coded. А вот по внезапно красному тесту "saveDocument(2024); findDocumentsNewerThan(2025) == empty" который очевидно верный — ты сразу увидишь ошибку.
Может для < это ещё просто, но в каких-то более хитрых сценариях, когда разные знаки например, payer/payee, buy/sell, call/put, bid/ask — очень легко заблудиться.
S>·>моки это не про state, а про передачу параметров. Для тестирования репо+бд моки не нужны вообще-то. Моки нужны для экономии ресурсов.
S>Эмм, ваша "модельная БД" — это тоже мок. Нет никакой разницы с т.з. архитектуры тестирования, то ли вы пишете мок вручную и хардкодите в него ответы на find(vasya), то ли вы его описываете декларативно с помощью мок-фреймворка, то ли берёте "квазинастоящую БД" и напихиваете данными перед тестом.
S>Это всё — про одно и то же "у меня есть stateful dependency, и я моделирую этот state перед каждым тестом".
Так это один-в-одит переводится в ФП без state:
val records = [vasya]; find(records, criteria) == vasya. Тут дело не в state, а что именно проверяется тестами.
Ещё раз — моки это лишь механизм передачи данных. Цель тестов с субд не в том, чтобы иметь состояние, а в том, чтобы проверять взаимодействие нашего кода с субд, что все компоненты (запрос, bind-парамы, схема, поля в select, result-set процессоры, етс...) согласованы. Проверять эту согласованность через e2e — очень накладно. Но где-то придётся, в любом случае.
S>·>Что показать?
S>Код.
Да такой же как ты показал. Разница только в том, что делается в тестах для тестирования этого кода.
S>·>У вас тоже они где-то есть.
S>Они — на самом верху, в конце конвеера, и их достаточно проверить при помощи немногих e2e тестов.
В конце каждого из конвееров.
S>·>Почему не буду?
S>Потому что в stateful OOP реализации невозможно повторно использовать код этих методов. Они возвращают некомбинируемые результаты.
S>Ну, то есть формально-то они комбинируемые — можно взять два набора Result, и выполнить их пересечение за O(N logM), но на практике это приведёт к неприемлемой производительности.
Причём тут вообще state?
S>>>Так и тут — если уж мы решили провести границу тестирования вокруг произвольно выбранного нами компонента системы — скажем, репозитория, то что нам помешает порезать этот репозиторий на K компонент?
S>·>Ну "findUsersByMailDomainAndGeo" — это поведение или что? А вот contains("top(10)") — это по-моему совершенно не поведение.
S>Поведение.
А если вместо top(10) реализуешь то же самое через between какой-нибдуь?