Re[98]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 18.03.24 12:37
Оценка:
Здравствуйте, 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 какой-нибдуь?
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.