Re[66]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 02.02.24 14:12
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Всё правильно он пишет. Вы заменили прямолинейный понятный код, в котором негде сделать ошибку, на код с распределённой ответственностью. Теперь у вас Time.now() расположен максимально далеко от места его использования.

Где можно сделать ошибки — я описывал. Проблемы с Time.now я тоже описывал. Несколько раз. Вы на это просто закрываете глаза.

S>·> а в вашем стиле lastMonday(LocalDateTime.now()) и nextFriday(LocalDateTime.now()) можно вызывать, да?!

S>Можно, но так никто не делает. Потому, что то место, где они нужны — это тоже функция. И в неё по умолчанию не передаётся ссылка на instantSource, а передаются готовые аргументы start и end. Не её дело принимать решения о том, где начало интервала, а где — конец.
S>Поэтому мы в неё передаём значения. А значения — тоже вычисляются чистой функцией. Чистота означает, что мы не делаем из неё грязных вызовов — поэтому никаких lastMonday(LocalDateTime.now()) и nextFriday(LocalDateTime.now()), а исключительно lastMonday(currentTime) и nextFriday(currentTime)/
Это очевидный детский сад. Я говорю о коде, где будет стоять LocalDateTime.now(). И как это место протестировать?

S>·>Ну ясен пень. Это слишком простой вопрос, не стоящий обсуждения. Ну да, мы должны получить некий момент и от него плясать по бизнес-логике. Скажем, тот же TradeDate нужно взять только один раз для данного ордера, в момент его прихода в handler, запомнить его и плясать от него далее, вычисляя всякие там settlement date, days to maturity и т.п.

S>Ну вы же так не делаете.
Делаю конечно, не фантазируй. Я говорю об "острых краях" приложения, а ты о скучной рутине внутри. Как именно покрывать максимальное количество кода легковесными быстрыми тестами, чтобы подавляющее большинство ошибок ловить ещё до коммита, а не после "успешного" деплоя на прод.

S>Ну, пока что ваш код не удовлетворяет предлагаемым вами же требованиям Напомню, что ваш кэш знать не знает ни про какой event log. И возвращает всегда локальное поле, а чтобы его обновить, нужно срабатывание таймера. Таймер ваш никак с instantSource не связан, это прямая дорога к рассогласованию.

Связан, конечно.

S>·>Я в первую очередь говорю о главном применении моков — отрезать входы-выходы системы (тот самый Time.Now, а ещё сеть, фс, бд, етс), которые в реальности завязываются на что-то тяжело контролируемое или очень медленное.

S>Ок, давайте откажемся от обсуждения времени — в конце концов, сделать lastFriday достаточно медленной, чтобы наше обсуждение имело смысл, скорее всего не получится.
Ну lastFriday это довольно условный пример. И медленность там может много откуда взяться. Да даже тупо проверка всех таблиц таймзон, dst-переходов, а ещё можно придумать логику хождения в бд для проверки праздников и т.п. — внезапно и не такой уж скучный пример.

S>Правильный способ отрезания входов-выходов — ровно такой, как настаивает коллега Pauel. Потому что в вашем способе вы их никуда не отрезаете — вы их заменяете плохими аналогами. Ващ подход мы, как и все на рынке, применяли в бою. И неоднократно обожглись о то, что мок не мокает настоящее решение; он мокает ожидания разработчика об этом решении.

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

S>·>Вот это я не понял. У него в примере тест буквально сравнивал текст sql "select * ...". Как такое поймает это?

S>Это он просто переводит с рабочего на понятный. Он же пытался вам объяснить, что проверяет структурную эквивалентность. Тут важно понимать, какие компоненты у нас в пайплайне.
При этом он меня обвинил в том, что мои тесты проверяют как написано, а не ожидания пользователей. А бревно в глазу не замечаете — пользователям-то похрен какая-то там эквивалентность у вас, структурная или не очень.

S>Если вы строите текст SQL руками — ну да, тогда будет именно сравнение текстов. Но в наше время так делать неэффективно. Обычно мы порождаем не текст SQL, а его AST. Генерацией текста по AST занимается отдельный компонент (чистая функция), и он покрыт своими тестами — нам их писать не надо.

И накой сдалась эта ваша AST вашим пользователям? А что если завтра понадобится заменить вашу AST на очередной TLA, то придётся переписывать все тесты, рискуя всё сломать.

S>·>Суть в том, что программист пишет код+тест. В тесте вручную прописывает ожидание =="sellect * ...". Потом тест, ясен пень проходит, потом ревью, который тоже наверняка пройдёт, не каждый ревьювер способен заметить лишнюю букву.

S>Ну, во-первых, программист же пишет код не в воздухе. Как правило, сначала нужный SQL пишется руками и проверяется интерактивно с тестовой базой, пока не начнёт работать так, как ожидается. Лично я работаю с SQL примерно 30 лет, из них 10 проработал очень плотно. Но и то — сходу не всегда могу написать корректный SQL. Поэтому мы сначала пишем несколько характерных вариантов запроса и прогоняем их на базе (в том числе — используем ровно тот движок, с которым работает прод. Никаких замен боевого постгре на тестовый SQLite или ещё каких-нибудь моков.)
S>Потом вставляем готовые команды в тест. Потом пишем код.
Именно! Против этих всех ручных пассов я и возражаю. Верю, что писать запросики в консоли и копипастить в код было ещё ок 30 лет назад... но пора сказать стоп! Ровно так же можно подключаться из тестов к какому хошь движку и напрямую _автоматически_ выполнять запросы и _автоматически_ ассертить результаты чем вручную копипастить туда-сюда из консоли в код и проверять глазками, похож ли результат ли на правду. А если sqlite (есть кстати ещё h2 которая неплохо умеет притворяться популярными субд) чем-то не устраивает, то запустить с точностью до бита идентичный проду постре что локально, что в CI-пайплайне для прогона тестов — дело минут в современном мире докеров.

S>·>И только потом, когда некий тест пройдёт от логина до отчёта, то оно грохнется, может быть, если этот тест таки использует именно эту ветку кода для тестовых параметров отчёта. Ведь итесты в принципе не могут покрыть все комбинации, их может быть просто экспоненциально дохрена.

S>У вас — та же проблема, просто вы её предпочитаете не замечать. Вы там запустили тест, а вместо опечатки sellect у вас там вызов функции, которая в inmemory DB работает, а в боевой — нет. SQL вообще состоит из диалектных различий чуть менее, чем полностью.
Эта проблема надумана.

S>·>С этим осторожнее. Вот этот твой кеш оказался — с состоянием, внезапно, да ещё и static global.

S>Да, такие вещи нужно делать аккуратно, вы правы.
Тут у вас вся ваша функциональщина и вылазит боком, т.к. кеш — это состояние, даже хуже того — шаред, по определению. И вся пюрешность улетучивается. Приходится нехило прыгать по монадам и прочим страшным словам. А в шарпах-ts где нет нормальной чистоты, только и остаётся отстреливать себе ноги.

S>>>Не знаю. А откуда у вас в CalendarLogic берётся instantSource?

S>·>Через DI/CI.
S>Ну вот он у вас в тестах — один, в продакшне — другой. Упс.
Суть в том, что тест от прода отличается ровно одной строчкой кода, в этом и цель всего этого. И строчка эта если и меняется, то раз в никогда. И если требует изменения — это становится явным и очевидным, требует более аккуратной валидации релиза. У вас же эта вся грязь будет в каждом методе каждого контроллера.

S>·>CompositionRoot — один на всё приложение, меняется относительно редко, обычно тупой линейный код, и составляет доли процента от объёма всего приложения. Его можно ежедневно глазками каждую строчку просматривать всей командой.

S>Ну, так это ровно тот же аргумент "против", который вы мне только что рассказывали. Вот вы взяли и заменили ваш код глобально, не разбираясь, где там можно применять кэширование, а где — нельзя.
Именно. Т.к. nextFriday() — всё сразу видно, всё на ладони — нужно заглянуть ровно в одно место чтобы понять можно ли применять кеширование и если можно то какое, и, во время релиза, не надо проверять каждый метод контроллера, а достаточно один сценарий, на то что хоть где-то этот кеш заработал ожидаемым способом, т.к. во всех остальных местах всё работает так же, т.к. код идентичный.

S>Потому что ваш кэширующий CalendarLogic полагается на то, что в нём timeSource согласован с timer, а в коде это никак не выражено. Вот вы написали replay, который полагается на возможность замены глобального timeSource на чтение из eventLog, и погоняли его. И так получилось, что данные, на которых вы его гоняли, границу пятницы не пересекали. Откуда вы знаете, что нужно пересечь границу пятницы? Да ниоткуда, у вас в проекте таких "особых моментов" — тыщи и тыщи. Код коверадж вам красит весь ваш CalendarLogic зелёненьким, причин сомневаться в работоспособности нету.

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

S>·>И вот такой модуль можно тестировать простыми тестами, подсовывая туда моковый timeSource и ровно этот же модуль будет зваться из "main" метода реального приложения, куда будет запихан SystemClock в качестве timeSource.

S>Проблема опять только в двух вещах:
S>1. Слишком много кода в стеке при выполнении тестов
Тесты, которые тестируют такой модуль, конечно, тяжелее чем типичные юнит-тесты, но они всё равно достаточно быстрые, т.к. никакого IO нет, всё внутри языка.

S>2. Слишком много доверия к тому, что моки адекватно изображают реальные компоненты.

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

S>·>И это по всему коду — везде в тысячах мест. Да? И как это тестировать? Как делать replay?

S>Смотря как мы сохраняем историю для этого replay. Но в целом — код, в котором зашит Time.now(), replay не поддаётся. Это одна из его проблем.
Ясен пень, типичный синглтон же.

S>Поэтому у нас там будут вызовы не Time.now(), а какого-нибудь request.ArrivalTime. С самого начала, естественно. Обратите внимание — в каждом вызове время будет своё, и мне достаточно поднять реквест из лога и скормить его ровно тому же методу обработки.

А откуда в request появится ArrivalTime? И как все эти места протестировать? Учти, у нас 100500 различных видов request.

S>Вам придётся лепить костыли и шаманить, чтобы прикрутить магический instantSource, способный доставать время не из глобального RTC-источника, а из "текущего обрабатываемого запроса".

event sourcing.

S>·>Почему? У нас появляется требование считать nextFriday для платёжки — добавляем новый код для этого, другой код вообще можно не трогать.

S>Вы же только что поняли, что "другой код" всё же придётся трогать
Ы?

S>·>Я обычно использую специальный тестовый шедулер, который запускает установленные таймеры при изменении тестового источника времени. У есть метод типа tickClock(7, DAYS). Код писать лень, но идея, надеюсь, очевидна...

S>Не, нифига не очевидна. В том-то и дело, что ваш тестовый "источник времени" — это мок, который только и умеет что отдавать заданную вами константу в ответ на now(). Написать его так, чтобы он умел корректно триггерить таймеры можно, но не факт, что вы про это вспомните вовремя
Самое позднее когда я вспомню — это после упавшего локально теста, ещё до коммита.

S>·>Именно. Универсальность — не даёт делать более эффективую реализацию.

S>Даёт, даёт
А ты попробуй. Над мемоизацией посмеялись, над статической глобальной переменной _cache поплакали. Ещё идеи остались?

S>>>И это у вас прекрасно грохнется в продакшне, как только на машине сменят таймзону аккурат около полуночи пятницы.

S>·>Нет, конечно. Таймеры ставят на instant, т.е. абсолютный момент времени, независящий от зоны.
S>Вот именно об этом я и говорю. Инстант не наступил, а nextFriday уже должна возвращать новое значение. Упс.
Шозабред? Этот инстант логически эквивалентен твоему _cache.nextFriday. В любом случае, это всё элементарно покрывается юнит-тестами (ага, с моками), даже если я и налажал код в браузере правильно написать.

S>·>Ну я код в браузере набирал, мог и ошибиться. В реальности буду запускать тесты, ясен пень.

S>Так у вас и тесты ничего не покажут.
Шозабред.

S>·>Через моки же — возвращай что хошь в каком хошь порядке.

S>Ну так для начала надо захотеть. Вы — не захотели.
Для моей реализации это и не надо.

S>·>В сымсле ботва с таймзонами? Ну это у меня уже привычка — писать тесты для дат около всяких dst переходов и с экзотическими таймзонами.

S>Ботва с таймзонами очень простая: у вас один и тот же instantSource() сначала возвращает "01:00 пятницы", а потом — "17:00 четверга". Просто пользователь перелетел через Атлантику, делов-то.
Это ты наверное имеешь в виду, если wall clock поменяет таймзону. Только это невозможно в моём случае по дизайну. Или ты не понимаешь что такое instant. Это конретная точка на линии физического времени, а не показания на циферблате часов типа "01:00 пятница" для человеков.
Хотя да, это наверное может поломаться, если эффекты СТО учитывать...

S>·>Ну смысле если мы из одного юнита А перенесли код в другой юнит Б, то тесты А всё ещё должны проходить. Но они будут идти через А в Б. Это некрасиво. После того как убедились, что А всё ещё работает. Тесты желательно перенести чтобы тестить Б напрямую.

S>Не перенести, а скопировать. А для полноты ощущений наверное нужно в тестах А заменить Б на мок Б — чтобы это был именно юнит тест, а то уже какая-то интеграция получается.
Можно и скопировать. Потом удалить лишнее, если мешается.

S>>>А как тогда вы защититесь от будущего джуна, который там что-нибудь отвинтит?

S>·>Не понял, что "что-нибудь"?
S>Значит возьмёт и поменяет вызов вашего компонента Б на какую-нибудь ерунду.
А если в твоей функции джун поменяет вызов на какую-нибудь ерунду?

S>·>Ну не нравится, не ешь. Об чём спор? Для чего нужен таймер — я тебе написал, для low latency. Без таймера твой вариант у меня тоже элементарно реализуется, правда мне совесть не позволит убить ещё одного котёнка делая static глобальную переменную. Можно даже гибридное решение наворотить, обновляя кеш по таймеру из другого треда, но если вдруг момент timeSource.now() промазывает по кешу, то идти по медленному пути.

S>Это решение, очевидно, хуже обоих обсуждаемых вариантов. Оно одновременно оборудовано таймером (усложнение зависимостей, хрупкость) и всё ещё иногда идёт по медленному пути.
Это же я фантазирую. Наверно да, такое вряд ли понадобится.

S>Так-то и в ФП ваш вариант элементарно реализуется, потому что способ закинуть замыкание в таймер есть более-менее везде.

Как?

S>·>Это как? Если у нас в SLA написано, что max response time 10ms, а SlowlyCalculatePrevAndNextFridays работает 11ms, то выбора тупо нет, и похрен всем твоя точка зрения.

S>Ну, ок, я таких SLA не встречал. Это примерно как 100% availability — обеспечить невозможно. Если у вас такой SLA — ну, ок, согласен, это может влиять на дизайн.
Ну... было всего 99.99% афаир.

S>>>В-третьих, там этот "спайк задержки" работает на 1 запрос. Практически невозможно придумать SLA, на которое это повлияет.

S>·>Открой для себя мир low latency.
S>С удовольствием. Пришлите ссылку на публичную SLA, в которой есть такие требования, я почитаю.
Было дело в lmax. Насчёт публичных доков — не знаю.

S>·>У нас это было серьёзный production incident. Каждый запрос, обработка которого была >10ms — тикет и расследование. Больше таких двух в день — и клиент нам звонит для разборок.

S>Ну, тогда у нас есть тесты не только функциональные, но и с бенчмарками, и никто не выкатывает в прод код, надеясь, что он быстрый. И ошибки вроде промахов кэша обнаруживаются регулярным способом, а не мамойклянус.
Ясен пень. Но это ещё и означает, что такие фривольности с глобальными кешами просто недопустимы. Т.к. промахи могут быть недетерминированными и все эти бенчмарки их ловят негарантированно. Как в примере со _слишком_ старой платёжкой. Обычно платёжки не очень старые и даже прогон месячной прод-нагрузки может ничего не выявить, а случайный залётный дятел попадается редко, но метко.

S>·>Это повлияет только на новый код с платёжкой. Подменить "случайно" по всему приложению так не выйдет. А вот твоя глобальная переменная — happy debugging.

S>У нас тоже подменить случайно по всему приложению не выйдет — потому что "всё приложение" пользуется функцией nextFriday(x). А кэширование к этой функции приделано только там, где это потребовалось.
Требоваться может везде (или почти везде), в этом и проблема. Суть примера была в том, что "всё приложение" использует не общий "nextFriday(x)", а конкретный "nextFriday(Time.Now)" во многих местах (см. моё начальное "Если у нас конечная цель иметь именно nextFriday()"). Поэтому этот общий код у меня и вынесен в одно место, которое безопасно кешировать, т.к. известен этот самый x. У тебя — неизвестно, в этом и беда. Напомню: ЧПФ.

S>·>У меня была цель продемонстрировать не основы объектно-ориентированного анализа, а факт, что можно максимально оптимизировать реализацию nextFriday(), т.к. у нас известно многое внутри самого метода. Навешивая это всё снаружи, приходится делать всё более пессимистично, т.к. инфы тупо меньше.

S>Нет такого факта, я вам в который раз повторяю. В вашем instantSource нет никакой гарантии, что следующий вызов now() не вернёт момент времени раньше, чем предыдущий.
Это не так. В худшем случае, я могу тупо найти все те одно-два места использования _конструктора_ CalendarLogic удостовериться что там может быть и какими свойствами обладает. Ещё раз. ЧПФ.

S>И по коду, который вы пишете, примерно понятно, как вы будете это делать в продакшн — в частности, вы даже не задумались о том, что CalendarLogic у вас будет декоратором или наследником старого CalendarLogic. Просто взяли и заменили код, на который завязан весь проект. Ну заменили вы "статик глобал" на "глобал синглтон" — что улучшилось-то?

Нет никакого глобал. Есть composition root.

S>>>который ФП-программист пишет инстинктивно, т.к. в ФП такая идиоматика.

S>·>Ага, смешно у Pauel инстинкт с memoize сработал.
S>Бывает, чо. В реале он бы привинтил к nextFriday memoize, убедился, что тот не помогает, и сел бы разбираться в причинах и искать способ сделать это корректно. Но у него понятная архитектура.
Совершенно верно! Любая, даже самая сложная проблема обязательно имеет простое, легкое для понимания, неправильное решение.

S>>>А надо — наоборот: зависимостей — избегать, стейта — избегать.

S>·>Ага, и ты тут такой: static (DateTime prevFriday, DateTime nextFriday)? _cache А потом хрясь и в догонку nextFriday(LocalDateTime.now())!
S>Всё правильно — весь стейт торчит на самом верху, и его мало. А глубокие вызовы все pure, и позволяют тестироваться по табличкам безо всяких моков.
Может я не понял, что ты называешь самым верхом, но это у вас — метод контроллера, коих в типичном приложении сотни. У же меня этим верхом является один на всех composition root.

S>>>Понадобится стейт — всегда можно добавить его по необходимости.

S>·>А вот попробуй избавиться от этого static и у тебя полезет колбасня.
S>Не полезет у меня никакой колбасни
S>Просто будет локальная переменная в замыкании.
Как эта локальная переменная будет шариться между контроллерами?

S>>>Нет у вас никакого широкого простора для оптимизаций. Есть иллюзии того, что timeSource.now() ведёт себя как-то иначе, чем аргумент (x). И, естественно, оптимизации, построенные на этих иллюзиях, будут приводить к фейлам.

S>·>ЧПФ же. У нас зафиксирован timeSource же.
S>Ну и что, что ЧПФ? Вы вообще ближе к началу этого поста признались, что вам как таковой instantSource() вовсе не нужен, а нужен ровно один instant, от которого пляшет вся бизнес-логика.
S>Так и какой смысл подменять параметр типа instant на параметр типа instantSource, который вы хотите позвать ровно один раз в рамках экземпляра бизнес-логики? С какой целью вы это делаете?
В случае trade date, instant — это физическая точка во времени, для железки. А для ордера нужны календарные показания, для трейдеров. CalendarLogic эту логику и обеспечивает.

S>·>Сокет ещё утром был подключён к конкретной бирже. Диспечерезацией уже занимается сетевая карта.

S>Ну и прекрасно — тогда у нас есть глобальная информация о том, с какой биржей мы работаем, внутри messageHandler.
Что значит глобальная? Приложение может подключаться не нескольким биржам одновременно, разными сокетами.

S>·>Почему неверный? Таймеры тоже срабатывают по timeSource. Проигрывание лога воспроизводит всё поведение системы, но уже без привязки к физическим часам. Ровно это же используется в тестах, когда мы выполняем шаги: "Делаем трейд. Ждём до конца месяца. Получаем стейтмент".

S>Где у вас это выражено в коде? Как у вас срабатывают таймеры, когда время идёт "назад"?
Время не ходит назад. Это физически невозможно. Проблема возникла конкретно у тебя, т.к. у тебя общий всемогутер nextFriday(x), где в качестве x может быть как и текущее время, так и старые платёжки.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[67]: Что такое Dependency Rejection
От: Sinclair Россия https://github.com/evilguest/
Дата: 03.02.24 14:09
Оценка:
Здравствуйте, ·, Вы писали:

·>Это очевидный детский сад. Я говорю о коде, где будет стоять LocalDateTime.now(). И как это место протестировать?

Примерно так же, как тестируется ваш "compositionRoot" .

S>>Ну вы же так не делаете.

·>Делаю конечно, не фантазируй. Я говорю об "острых краях" приложения, а ты о скучной рутине внутри. Как именно покрывать максимальное количество кода легковесными быстрыми тестами, чтобы подавляющее большинство ошибок ловить ещё до коммита, а не после "успешного" деплоя на прод.
Понимаете, тут вот в чём сложность: превратить код в "нашем" с Pauel стиле в код в вашем стиле — дело простое и очевидное. Просто берём хорошо и дёшево оттестированный stateless код и запихиваем в него state (в виде обращений к instantSource, который в терминах FP — обычный future) или грязь (в виде обращения к time.now()).
Таким образом, можно комбинировать преимущества обоих подходов там, где это нужно.
А вот сделать из грязного недетерминистического кода чистый детерминистический — дудки.


·>Связан, конечно.

В коде это никак не отражено (c).

·>Ну lastFriday это довольно условный пример. И медленность там может много откуда взяться. Да даже тупо проверка всех таблиц таймзон, dst-переходов, а ещё можно придумать логику хождения в бд для проверки праздников и т.п. — внезапно и не такой уж скучный пример.

Ну, это ещё более хороший пример — потому, что чем дальше вы навешиваете на эту lastFriday стейтфул-сложность, тем дороже и хуже будет обходиться его тестирование.
Теперь он у вас зависит от "источника инфы про таймзоны", "бд со списком праздников", етк. Чтобы протестировать собственно логику всего этого contrivance вам придётся скормить ему чёртову гору моков, и шансы на то, что вы всё это верно замокаете, падают по экспоненте.

·>Ожидания разработчика берутся не из пустого места, а из фиксации поведения боевой системы. Ну по крайней мере если использовать моки правильно, а не так как Pauel думает их используют.

Это какие-то абстрактные рассуждения, простите. Получается, для написания мока вам надо сначала провести все нужные тесты с боевой системой, а уже потом что-то там мокать.
Не у всех есть эта роскошь — боевая система может быть недоступна (вам дали спецификацию от сервиса, который будет выпущен в марте 2024), или медленно работать (и вы будете дожидаться тестового покрытия неделями и месяцами). И некоторые аспекты поведения вы можете на боевой системе просто не увидеть — в силу банальных причин. Из самого простого — может так оказаться, что в вашей боевой системе случайно ни разу не встетился пользователь с именем, в котором есть не-Latin1 символы. Нет никакого поведения, которое вы могли бы "зафиксировать" — только слова разработчиков "боевой системы" о том, как это должно работать.
А потом такой пользователь заводится — и всё, оказывается, что где-то там внутри разработчик ограничил длину байтами, а не code point-ами. Упс.

·>При этом он меня обвинил в том, что мои тесты проверяют как написано, а не ожидания пользователей. А бревно в глазу не замечаете — пользователям-то похрен какая-то там эквивалентность у вас, структурная или не очень.

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

·>И накой сдалась эта ваша AST вашим пользователям? А что если завтра понадобится заменить вашу AST на очередной TLA, то придётся переписывать все тесты, рискуя всё сломать.

Это вы сейчас аргументируете в пользу отказа от юнит-тестирования, я правильно понял?
Потому что разница, собственно, примерно такая: вот у нас есть цепочка зависимостей A->B->C->D, где A — это у нас БЛ, B — это query-часть ORM, С — это генератор SQL-запросов, а D — это БД с тестовыми данными.
И вы типа говорите "мы хотим тестировать всю цепочку, чтобы сэкономить на поддержании тестов в случае замены компонентов B/C".
Это разумная идея, которая будет хорошо работать в тех случаях, когда компоненты B/C подлежат частой замене; при этом у вас легко поддерживать как D, так и тесты результатов к ней. Например, запросы у вас сводятся к GetObjectById(), который вы вчера делали при помощи метода "репозитория" в стиле Фаулера тридцатилетней давности с зашитым внутри "select * from Objects where id = ?", а сегодня вы переехали на JOOQ или Hibernate, но всё остальное осталось неизменным.
Но вот когда начинаются широко разветвлённые пути исполнения в вашем построении запросов, этот подход утонет из-за размера таблиц истинности. Просто возможность указать поле, по которому сортировать, из списка в десяток, потребует от вас довольно-таки длинного набора записей в тестовую таблицу — иначе ваш тест будет некорректным. Нельзя просто добавить пару записей, которые отличаются одним полем, попросить отсортировать по нему, и убедиться, что вторая идёт после первой. Очень может оказаться, что в запросе — косяк, просто он не проявляется на этих данных. Вам нужен такой набор записей, который для каждого из заданных 10 полей будет идти в своём, определённом порядке.
Как только начинается вариативность в критериях отбора, всё становится ещё веселее. Вылезает та самая экспонента, о которой вам твердит Pauel.
И не надо думать, что это касается только data warehouse, проблемы которых можно закрыть, просто передав их в другой отдел — "мы вообще не будем писать никакие отчёты на Java, пусть там аналитики самостоятельно пердолятся в своём Tableau".
Запросто может оказаться, что во вполне себе OLTP-коде навёрнута развесистая логика вроде "если запрошенная лицензия ещё не проэкспайрилась, то находим SKU для её renewal и order date будет датой экспирации; если проэкспайрилась меньше месяца назад — берём SKU для renewal, но дата будет сегодняшим днём; иначе берём SKU для новой покупки, дата ордера будет сегодняшняя, но дата следующего обновления округляется до целого месяца вверх", и поверх этого всякие "если продукт снят с продажи — поискать продукт следуюшей версии в таблице преемственности; если нет новой версии — поискать продукт в списке преемственности; если лицензия учитывает платформу — поискать продукт с такой же платформой; если не нашёлся или лицензия была anyplatform — поискать продукт anyplatform", и такой ботвы — шесть страниц со сносками, примечаниями, и исключениями.
Вот эта вот вся штука — одна функция типа "найди мне код продукта для продления лицензии", один входной параметр, один выходной параметр.
Если вы захотите покрыть всё это при помощи заботливо приготовленной тестовой таблицы (точнее, пяти таблиц) и некоторого набора запросов — удачи.
Есть только два способа с этим справиться:
1. Молиться, мониторить фидбек пользователей, ежемесячно править баги после выхода каждого нового прайс-листа
2. Пилить эту развесистую функцию, зависящую от внешней БД, на набор простых чистых функций, и тестировать каждую из них в отдельности.

Обратите внимание — тут как раз юмор в том, что "ожидания пользователя" — это как раз то, что "если вот такой поиск не удался, то будем использовать вот такой". А не так, что "да похрен, как там SQL написан — главное, чтобы она возвращала что должна". Нет никакой возможности даже сформулировать "что должна" в терминах конечных данных. Например, потому, что прайслиста за июнь 2024 ещё не существует в природе, и какие там будут данные — а хрен его знает. Какие-то продукты будут закрыты вовсе, какие-то смигрированы на другие платформы и так далее.

И вот такого как правило в реальных системах — дохрена и больше. Иногда разработчики просто делают вид, что этого ужаса не существует — ну, такое отрицание реальности. Дескать, "да что там может пойти не так — я же прогоняю тесты с настоящим постгре, и даже запихиваю туда тестовые данные". А потом выясняется, что QA-шники в тестовых данных позабивали всяких очевидных "острых случаев" вроде пользователей с именем AAAAAAAAAAAAAAA, а потом кто-то в реальной системе пытается продлить подписку, за которую оформлен возврат, и получается непредвиденный результат.


·>Именно! Против этих всех ручных пассов я и возражаю. Верю, что писать запросики в консоли и копипастить в код было ещё ок 30 лет назад... но пора сказать стоп! Ровно так же можно подключаться из тестов к какому хошь движку и напрямую _автоматически_ выполнять запросы и _автоматически_ ассертить результаты чем вручную копипастить туда-сюда из консоли в код и проверять глазками, похож ли результат ли на правду.

В мало-мальски реалистичном случае вы вспотеете автоматически ассертить результаты запроса к данным.

·>А если sqlite (есть кстати ещё h2 которая неплохо умеет притворяться популярными субд) чем-то не устраивает, то запустить с точностью до бита идентичный проду постре что локально, что в CI-пайплайне для прогона тестов — дело минут в современном мире докеров.

Минут????? Ну круто — вы только что предложили перейти от тестов, которые исполняются миллисекунды, к тестам длиной в минуты.
И у нас ещё будет большой вопрос про согласованность тестов и данных.
Потому что есть два подхода:
1. Берём контейнер с СУБД, маунтим образ тестовой БД, прогоняем тесты и ассерты
2. Берём контейнер с СУБД, создаём пустую БД, накатываем в неё тестовые данные, прогоняем тесты и ассерты.
Первый вариант — быстрее, но рискованнее: наши ассерты должны соответствовать содержимому тестовой БД. Можно запросто сломать пачку тестов, просто добавив пару записей. И вернуться к тому, с чего начинали — глазками просматривать различия между ожиданиями и реальностью, и править расхождения.
Второй вариант — надёжнее, т.к. параметры и ожидания лежат рядом друг с другом, и скорее соответствуют друг другу.
Но он и заметно медленнее — потому, что нам нужен какой-никакой размер тестовых данных, а коммит в СУБД работает сильно медленнее, чем восстановление файла из образа.
Ну, и есть трудности с параллельным исполнением. Не, я понимаю, что никто нам не мешает поднять одновременно полсотни контейнеров, в кажлом из которых постгре обрабатывает свой изолированный тест.
Но и ресурсов на это нужно примерно в бесконечность раз больше, чем для прогона unit-тестов, проверяющих факты вроде "если у лицензии не задана платформа, то её не будем учитывать, а если задана — то будем".


·>Эта проблема надумана.

Выстрадана кровью и потом.

·>Тут у вас вся ваша функциональщина и вылазит боком, т.к. кеш — это состояние, даже хуже того — шаред, по определению. И вся пюрешность улетучивается.

Никуда она не улетучивается. У нас — 90% пюрешного кода обмазаны 10% императивной грязи; а у вас — все 100% кода состоят из грязи, и даже для тестирования банальных вещей вроде арифметики приходится туда и сюда просовывать моки.

·>Приходится нехило прыгать по монадам и прочим страшным словам. А в шарпах-ts где нет нормальной чистоты, только и остаётся отстреливать себе ноги.


Признаюсь честно — сам я на практике (в пет-проджектах) тяготею к написанию тестов именно в вашем стиле; по ряду причин. Основная из которых — непреодолимая привлекательность: сходу понятно, что именно писать. Куда проще проверить, что запрос вида "2*2+2" вернёт 6, чем сидеть и формализовывать "умножение матриц у нас должно конвертироваться вот в такой вот набор скалярных умножений, а сложение — вот в такой".
Но и ловится такими тестами далеко не всё. Недавно у меня случился баг как раз такого типа — вроде и возвращалось всё правильно; а поменял размеры тестовых массивов — и упс! оказалось, в одном месте при переходе от скаляров к векторам менялся знак.

·>Суть в том, что тест от прода отличается ровно одной строчкой кода, в этом и цель всего этого. И строчка эта если и меняется, то раз в никогда. И если требует изменения — это становится явным и очевидным, требует более аккуратной валидации релиза. У вас же эта вся грязь будет в каждом методе каждого контроллера.

Если захотим — да. А если мы увидим в этом проблему, то "вся грязь" будет выписана в виде грязной ФВП и применена единообразно. Вы же не думаете, что агрегация — это прерогатива рич-ООП?
Нет ровно никакой проблемы взять и добавить к чистой функции nextFriday(timestamp) грязную функцию nextFriday()=>nextFriday(Time.now()), и заменить вызовы повсеместно.
Но это будет осознанным шагом, а не следствием случайного дизайна под давлением "ну мы хотели просто повызывать Console.WriteLine и DateTime.Now, но тим лид велел сделать юнит-тесты через моки".


·>Именно. Т.к. nextFriday() — всё сразу видно, всё на ладони — нужно заглянуть ровно в одно место чтобы понять можно ли применять кеширование и если можно то какое

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

·>Тесты, которые тестируют такой модуль, конечно, тяжелее чем типичные юнит-тесты, но они всё равно достаточно быстрые, т.к. никакого IO нет, всё внутри языка.

И тем не менее — тут умножили в 2 раза, там в 2 раза — так и набегает "тесты исполняются несколько часов".

·>Это ортогональная проблема. В тестировании пюрешек ровно эта же проблема тоже есть, для параметров. Тебе приходится верить, что твои тестовые параметры изображают реальные значения. Ещё раз, моки отличаются от параметров лишь способом доставки значений. Реальность данных относится именно к самим значениям, а не способу их передачи.

Ну, в некоторых изолированных случаях — да.
Можно писать код в стиле
GetItems(int a, List<int> list)
{
  list.Add(a);
  list.Add(a*2);
}

и тестировать его при помощи стейтфул-"мока":
var l = new List<int>();
GetItems(2, l);
Assert.SequenceEquals(l, new[]{2, 4});

(ну или задом-наперёд, написать моку каких вызовов ожидать).
Можно переписать это в виде чистой функции — и тестировать ровно то же самое:
IEnumerable<int> GetItems(int a, List<int> list)
{
  yield return a;
  yield return a*2;
}

...

var l = GetItems(2);
Assert.SequenceEquals(l, new[]{2, 4});

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

·>Ясен пень, типичный синглтон же.

Не, типичный хардкод.

·>А откуда в request появится ArrivalTime? И как все эти места протестировать? Учти, у нас 100500 различных видов request.

Как откуда? Из общей инфраструктуры. У нас система типов построена так, что в любом request есть ArrivalTime, а его генерация делается примерно в одном месте, которое обеспечивает приём запроса ещё до его роутинга.

·>Ы?

Там, где вы "старый" код, который обращался к instantSource, заменили на вызов "новой" nextFriday(instantSource.now()).

·>Самое позднее когда я вспомню — это после упавшего локально теста, ещё до коммита.

Это если он упадёт

·>А ты попробуй. Над мемоизацией посмеялись, над статической глобальной переменной _cache поплакали. Ещё идеи остались?

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

S>>Вот именно об этом я и говорю. Инстант не наступил, а nextFriday уже должна возвращать новое значение. Упс.

·>Шозабред? Этот инстант логически эквивалентен твоему _cache.nextFriday. В любом случае, это всё элементарно покрывается юнит-тестами (ага, с моками), даже если я и налажал код в браузере правильно написать.
Ну, всё верно. Именно поэтому у меня там не одна пятница, а две, и код возвращает закешированное значение только в том случае, если instant попадает в их диапазон

S>>Так у вас и тесты ничего не покажут.

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

·>Для моей реализации это и не надо.



S>>Ботва с таймзонами очень простая: у вас один и тот же instantSource() сначала возвращает "01:00 пятницы", а потом — "17:00 четверга". Просто пользователь перелетел через Атлантику, делов-то.

·>Это ты наверное имеешь в виду, если wall clock поменяет таймзону. Только это невозможно в моём случае по дизайну. Или ты не понимаешь что такое instant. Это конретная точка на линии физического времени, а не показания на циферблате часов типа "01:00 пятница" для человеков.
Тогда вы, наверное, неверно решили задачу. Потому что человека интересовала следующая пятница по wall time, а не абстрактная точка на линии "физического времени". Если бы речь щла о физическом времени, то вся эта ботва со следующей пятницей сводилась бы к паре делений и умножений.

S>>Значит возьмёт и поменяет вызов вашего компонента Б на какую-нибудь ерунду.

·>А если в твоей функции джун поменяет вызов на какую-нибудь ерунду?
В какой именно? У меня нет двух разных calendarLogic,

S>>Так-то и в ФП ваш вариант элементарно реализуется, потому что способ закинуть замыкание в таймер есть более-менее везде.

·>Как?
В смысле "как"?
Так и пишем:
    public static Func<int, int> Cache(Func<int, int> func)
    {
        int? c = null;
        return (i) => 
            (c is int r && r >= i) ? r : (c = func(i)).Value ;
    }

Здесь у нас на каждый вызов Cache() возвращается новая функция со своим кэшем, независимым от результатов других вызовов. АФАИК, в джаве это работает так же.

S>>С удовольствием. Пришлите ссылку на публичную SLA, в которой есть такие требования, я почитаю.

·>Было дело в lmax. Насчёт публичных доков — не знаю.
Ну, LMAX — парни известные. Но если почитать их Terms of Business, то слово latency там упомянуто всего лишь трижды, и ни в одном из случаев там не фигурируют никакие цифры. https://www.lmax.com/documents/LMAXGlobal-eu-Terms-of-Business.pdf
Наверняка вы с лёгкостью найдёте каких-то других парней (да хоть вашего работодателя), где есть SLA c параметрами латентности, да ещё и не процентилях, а в виде ограничения сверху.

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

·>Ясен пень. Но это ещё и означает, что такие фривольности с глобальными кешами просто недопустимы. Т.к. промахи могут быть недетерминированными и все эти бенчмарки их ловят негарантированно.

Вопрос к автору бенчмарков

·>Как в примере со _слишком_ старой платёжкой. Обычно платёжки не очень старые и даже прогон месячной прод-нагрузки может ничего не выявить, а случайный залётный дятел попадается редко, но метко.

Расскажите, как в вашем подходе решается эта проблема. Как вы собираетесь поймать "клиента со слишком старой платёжкой"?

·>Требоваться может везде (или почти везде), в этом и проблема. Суть примера была в том, что "всё приложение" использует не общий "nextFriday(x)", а конкретный "nextFriday(Time.Now)" во многих местах (см. моё начальное "Если у нас конечная цель иметь именно nextFriday()"). Поэтому этот общий код у меня и вынесен в одно место, которое безопасно кешировать, т.к. известен этот самый x. У тебя — неизвестно, в этом и беда. Напомню: ЧПФ.

На это уже ответил выше — ЧПФ придумали не в ООП

S>>Нет такого факта, я вам в который раз повторяю. В вашем instantSource нет никакой гарантии, что следующий вызов now() не вернёт момент времени раньше, чем предыдущий.

·>Это не так. В худшем случае, я могу тупо найти все те одно-два места использования _конструктора_ CalendarLogic удостовериться что там может быть и какими свойствами обладает. Ещё раз. ЧПФ.
Вы повторяете это как мантру. ЧПФ вам тут никак не поможет — нет такой системы типов, которая бы позволила вам записать монотонность стейтфул-функции.

·>Нет никакого глобал. Есть composition root.

Ну, ок.

S>>Всё правильно — весь стейт торчит на самом верху, и его мало. А глубокие вызовы все pure, и позволяют тестироваться по табличкам безо всяких моков.

·>Может я не понял, что ты называешь самым верхом, но это у вас — метод контроллера, коих в типичном приложении сотни. У же меня этим верхом является один на всех composition root.
Если нас не устраивает высота "метода контроллера", то мы запихиваем то, что вы называете "composition root" туда, откуда эти контроллеры вызываются.

S>>Просто будет локальная переменная в замыкании.

·>Как эта локальная переменная будет шариться между контроллерами?
Как захотим — так и будет. Через указатель на функцию

S>>Так и какой смысл подменять параметр типа instant на параметр типа instantSource, который вы хотите позвать ровно один раз в рамках экземпляра бизнес-логики? С какой целью вы это делаете?

·>В случае trade date, instant — это физическая точка во времени, для железки. А для ордера нужны календарные показания, для трейдеров. CalendarLogic эту логику и обеспечивает.
Ну так этой логике нужен ровно один instant, который физическая точка во времени. А не возможность получить неограниченное количество этих инстантов, продолжая вызывать недетерминистическую timeSource.now().

S>>Ну и прекрасно — тогда у нас есть глобальная информация о том, с какой биржей мы работаем, внутри messageHandler.

·>Что значит глобальная? Приложение может подключаться не нескольким биржам одновременно, разными сокетами.
Ну так наверное у них и хэндлеры будут разными. Вы же как-то должны понимать, с какой из бирж работаете.

S>>Где у вас это выражено в коде? Как у вас срабатывают таймеры, когда время идёт "назад"?

·> Время не ходит назад. Это физически невозможно.
При replay — ещё как идёт. Вот только что был понедельник, и тут поехал replay четверга. И ещё есть много случаев.
Вы пытаетесь искать ключи под фонарём: "а вот у меня instant source — он другой, я про него всё знаю". Нет, нихрена вы не знаете, потому что у вас instantSource — это интерфейс.
Императивные инварианты — это вообще такая штука, которую крайне сложно выразить в терминах системы типов. Банальнейшие вещи вроде "делать socket.Read() нельзя до socket.Open() и после socket.Close()" в терминах ООП невыразимы. А в реальных системах встречаются ещё и чудесные вещи типа "после того, как сделал var u = service.CreateUser(...), нужно выждать не менее 5 минут перед service.UpdateUser(u, ...)", с которыми совсем всё плохо.

Правильный способ с такими вещами работать требует очень сильно другой матаппарат, чем реализован в той же джаве.
Поэтому беспокоиться нужно не о том, что джун заменит request.ArrivalTime на Time.now() (в конце концов, это можно отловить через styleCop и прочие радости жизни) — беспокоиться надо о том, как покрыть все ожидания адекватными тестами. В том числе и все вот эти вот ожидания по поводу валидных трансформаций состояния.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[68]: Что такое Dependency Rejection
От: Буравчик Россия  
Дата: 03.02.24 16:20
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>·>Это ортогональная проблема. В тестировании пюрешек ровно эта же проблема тоже есть, для параметров. Тебе приходится верить, что твои тестовые параметры изображают реальные значения. Ещё раз, моки отличаются от параметров лишь способом доставки значений. Реальность данных относится именно к самим значениям, а не способу их передачи.


S>Ну, в некоторых изолированных случаях — да.


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

Пример приводил здесь do_logic.
http://rsdn.org/forum/design/8648665.1
Автор: Буравчик
Дата: 10.12.23


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

Полагаю, что работа с внешними зависимостями будут закрыты функциями, которые будут переданы в функцию do_logic. Аналогично для моков зависимости будут закрыты интерфейсом (группа функций), и он будет передан в do_logic. Т.е. с точки зрения тестирования разницы нет — в обоих случаях подменяется поведение внешней системы — передается функция возвращающая нужное для теста значение.
Best regards, Буравчик
Re[69]: Что такое Dependency Rejection
От: Sinclair Россия https://github.com/evilguest/
Дата: 04.02.24 05:24
Оценка:
Здравствуйте, Буравчик, Вы писали:
Б>Я так и не понял, как протестировать код, завязанный на внешние системы. Ответ от Pauel если и был, то размазался где-то среди кучи сообщений.
Б>Полагаю, что работа с внешними зависимостями будут закрыты функциями, которые будут переданы в функцию do_logic. Аналогично для моков зависимости будут закрыты интерфейсом (группа функций), и он будет передан в do_logic. Т.е. с точки зрения тестирования разницы нет — в обоих случаях подменяется поведение внешней системы — передается функция возвращающая нужное для теста значение.
Для внешних систем у нас вариантов немного — либо моки, либо интеграционное тестирование.
При этом я предпочитаю интеграцию — потому, что она даёт нам гораздо больше уверенности в результатах.
Даже "эмулятор" продакшн системы на стороне партнёра часто даёт неверные результаты.
У интеграции есть ровно один недостаток — она дорогая. Но обычно это приемлемая стоимость, с учётом того, что интеграционных тестов гораздо меньше. В моей практике обычно основная сложность (в которой можно напороть) спрятана именно в бизнес логике на нашей стороне; а взаимодействие со внешними системами — это ограниченное количество сценариев. И нам собственно нужно оттестировать как раз не саму логику, а расхождения между нашими ожиданиями от внешней системы и её реализацией.

Вот, кстати, когда мы работали с CREST API от Microsoft, нами был напилен набор интеграционных тестов, которые гонялись в каждом acceptance. И мы чаще всего ловили там не глюки нашего софта (которые были отловлены юнит-тестами), а регрессию со стороны Microsoft .
Было всего несколько случаев, когда поведение API было не таким, как мы ожидали, и это не было признано багом.

Но я могу себе представить ситуацию, когда 100% интеграционное тестирование недоступно или настолько дорого, чтобы оправдать написание моков. Тут вы совершенно правы — принцип остаётся ровно тем же самым: мы будем передавать в do_logic какие-то функции, эмулирующие поведение внешней системы.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[69]: Что такое Dependency Rejection
От: Sinclair Россия https://github.com/evilguest/
Дата: 04.02.24 05:24
Оценка:
Здравствуйте, Буравчик, Вы писали:
Б>Я так и не понял, как протестировать код, завязанный на внешние системы. Ответ от Pauel если и был, то размазался где-то среди кучи сообщений.
Б>Полагаю, что работа с внешними зависимостями будут закрыты функциями, которые будут переданы в функцию do_logic. Аналогично для моков зависимости будут закрыты интерфейсом (группа функций), и он будет передан в do_logic. Т.е. с точки зрения тестирования разницы нет — в обоих случаях подменяется поведение внешней системы — передается функция возвращающая нужное для теста значение.
Для внешних систем у нас вариантов немного — либо моки, либо интеграционное тестирование.
При этом я предпочитаю интеграцию — потому, что она даёт нам гораздо больше уверенности в результатах.
Даже "эмулятор" продакшн системы на стороне партнёра часто даёт неверные результаты.
У интеграции есть ровно один недостаток — она дорогая. Но обычно это приемлемая стоимость, с учётом того, что интеграционных тестов гораздо меньше. В моей практике обычно основная сложность (в которой можно напороть) спрятана именно в бизнес логике на нашей стороне; а взаимодействие со внешними системами — это ограниченное количество сценариев. И нам собственно нужно оттестировать как раз не саму логику, а расхождения между нашими ожиданиями от внешней системы и её реализацией.

Вот, кстати, когда мы работали с CREST API от Microsoft, нами был напилен набор интеграционных тестов, которые гонялись в каждом acceptance. И мы чаще всего ловили там не глюки нашего софта (которые были отловлены юнит-тестами), а регрессию со стороны Microsoft .
Было всего несколько случаев, когда поведение API было не таким, как мы ожидали, и это не было признано багом.

Но я могу себе представить ситуацию, когда 100% интеграционное тестирование недоступно или настолько дорого, чтобы оправдать написание моков. Тут вы совершенно правы — принцип остаётся ровно тем же самым: мы будем передавать в do_logic какие-то функции, эмулирующие поведение внешней системы.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[68]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 04.02.24 21:49
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>·>Это очевидный детский сад. Я говорю о коде, где будет стоять LocalDateTime.now(). И как это место протестировать?

S>Примерно так же, как тестируется ваш "compositionRoot" .
composition root — это технический, "подвальный" код. Конкретно инжекция системного источника времени — одна строчка кода, которая за жизнь проекта трогается практически ни разу. И тестируется относительно просто — приложение скомпилировалось, стартовало, даёт зелёный healthcheck — значит работает.
У тебя же now() лежит в коде бизнес-логики (как я понял из объяснений Pauel). Будет стоять в каждом втором методе контроллеров, т.е. в сотнях мест и код этот постоянно меняется.

S>·>Делаю конечно, не фантазируй. Я говорю об "острых краях" приложения, а ты о скучной рутине внутри. Как именно покрывать максимальное количество кода легковесными быстрыми тестами, чтобы подавляющее большинство ошибок ловить ещё до коммита, а не после "успешного" деплоя на прод.

S>Понимаете, тут вот в чём сложность: превратить код в "нашем" с Pauel стиле в код в вашем стиле — дело простое и очевидное. Просто берём хорошо и дёшево оттестированный stateless код и запихиваем в него state (в виде обращений к instantSource, который в терминах FP — обычный future) или грязь (в виде обращения к time.now()).
S>Таким образом, можно комбинировать преимущества обоих подходов там, где это нужно.
S>А вот сделать из грязного недетерминистического кода чистый детерминистический — дудки.
Если бы такое писать на хаскеле... А так это всё лирика. На практике же — понадобился кеш и полезло static по ногам стрелять...

S>·>Связан, конечно.

S>В коде это никак не отражено (c).
В сигнатуре nextFriday() это собсвенно негде даже отражать, в этом и суть. А в CalendarLogic — отразить можно. Опять же — это одно место на всё приложение.

S>·>Ну lastFriday это довольно условный пример. И медленность там может много откуда взяться. Да даже тупо проверка всех таблиц таймзон, dst-переходов, а ещё можно придумать логику хождения в бд для проверки праздников и т.п. — внезапно и не такой уж скучный пример.

S>Ну, это ещё более хороший пример — потому, что чем дальше вы навешиваете на эту lastFriday стейтфул-сложность, тем дороже и хуже будет обходиться его тестирование.
Логично, чем больше логики, тем больше тестов. Стейт тут не при чём.

S>Теперь он у вас зависит от "источника инфы про таймзоны", "бд со списком праздников", етк. Чтобы протестировать собственно логику всего этого contrivance вам придётся скормить ему чёртову гору моков, и шансы на то, что вы всё это верно замокаете, падают по экспоненте.

Если сложность конкретно CalendarLogic будет выходить за рамки разумного, то начнём резать части логики в отдельные компоненты.

S>·>Ожидания разработчика берутся не из пустого места, а из фиксации поведения боевой системы. Ну по крайней мере если использовать моки правильно, а не так как Pauel думает их используют.

S>Это какие-то абстрактные рассуждения, простите. Получается, для написания мока вам надо сначала провести все нужные тесты с боевой системой, а уже потом что-то там мокать.
Не для написания мока, а для написания теста. Как и у тебя. Ешё раз — мок средство доставки тестовых данных, а не сами данные. Именно данные нужно проводить в соответствие с реальностью. Даже вообще я не очень понимаю, что за процесс такой "написание мока".

S>Не у всех есть эта роскошь — боевая система может быть недоступна (вам дали спецификацию от сервиса, который будет выпущен в марте 2024), или медленно работать (и вы будете дожидаться тестового покрытия неделями и месяцами). И некоторые аспекты поведения вы можете на боевой системе просто не увидеть — в силу банальных причин. Из самого простого — может так оказаться, что в вашей боевой системе случайно ни разу не встетился пользователь с именем, в котором есть не-Latin1 символы. Нет никакого поведения, которое вы могли бы "зафиксировать" — только слова разработчиков "боевой системы" о том, как это должно работать.

S>А потом такой пользователь заводится — и всё, оказывается, что где-то там внутри разработчик ограничил длину байтами, а не code point-ами. Упс.
И? Ты тоже рассуждаешь об абстрактных проблемах программирования вместо того чтобы сравнивать "моки vs немоки".

S>·>При этом он меня обвинил в том, что мои тесты проверяют как написано, а не ожидания пользователей. А бревно в глазу не замечаете — пользователям-то похрен какая-то там эквивалентность у вас, структурная или не очень.

S>Совершенно верно. Но тут речь идёт несколько о другом аспекте. Если мы "мокаем репозиторий", то тестируем "как написано" — неважно, то ли мы следим за тем, что наш код в каком порядке вызывает, то ли запрашиваем из него список "операций", которые будем вызывать мы. Но вот что касается сложных запросов — тут разница не в стоимости поддержания тестов, а в стоимости их исполнения.
Мокаем это значит определяем контракт. И ожидаем, что он выполняется реальным репо. А реальный репо тестируется и-тестом с субд, на то, что он этот же контракт выполняет. Цель всего это действа — отпилить быстрые легковесные тесты логики от тяжелых тестов интеграции яп с субд.

S>·>И накой сдалась эта ваша AST вашим пользователям? А что если завтра понадобится заменить вашу AST на очередной TLA, то придётся переписывать все тесты, рискуя всё сломать.

S>Это вы сейчас аргументируете в пользу отказа от юнит-тестирования, я правильно понял?
S>Потому что разница, собственно, примерно такая: вот у нас есть цепочка зависимостей A->B->C->D, где A — это у нас БЛ, B — это query-часть ORM, С — это генератор SQL-запросов, а D — это БД с тестовыми данными.
S>И вы типа говорите "мы хотим тестировать всю цепочку, чтобы сэкономить на поддержании тестов в случае замены компонентов B/C".
Не очень понял чем B от C отличается. Но суть моего тезиса в том, что просто сгенерированный SQL особо не нужен. В тесте его нужно сгенерировать (не важно как), загнать туда параметры, выполнить, ассертить результаты. См. мой примерчик выше с "save/find". И это делать с бизнес-сущностями, а не абстрактными sql-текстами и именами функций маппинга. А потом, ещё отдельные тесты для бизнес-логики, которые используют контракт репо.

S>Это разумная идея, которая будет хорошо работать в тех случаях, когда компоненты B/C подлежат частой замене; при этом у вас легко поддерживать как D, так и тесты результатов к ней. Например, запросы у вас сводятся к GetObjectById(), который вы вчера делали при помощи метода "репозитория" в стиле Фаулера тридцатилетней давности с зашитым внутри "select * from Objects where id = ?", а сегодня вы переехали на JOOQ или Hibernate, но всё остальное осталось неизменным.

S>Но вот когда начинаются широко разветвлённые пути исполнения в вашем построении запросов, этот подход утонет из-за размера таблиц истинности. Просто возможность указать поле, по которому сортировать, из списка в десяток, потребует от вас довольно-таки длинного набора записей в тестовую таблицу — иначе ваш тест будет некорректным. Нельзя просто добавить пару записей, которые отличаются одним полем, попросить отсортировать по нему, и убедиться, что вторая идёт после первой. Очень может оказаться, что в запросе — косяк, просто он не проявляется на этих данных. Вам нужен такой набор записей, который для каждого из заданных 10 полей будет идти в своём, определённом порядке.
Если ты это уже знаешь, то в чём проблема это загнать через save в тестовые данные? Никто ж не запрещает использовать больше 2 записей. Тут просто желательно делать поменьше записей, чтобы человекам было проще разобраться в сценарии.

S>Как только начинается вариативность в критериях отбора, всё становится ещё веселее. Вылезает та самая экспонента, о которой вам твердит Pauel.

S>И не надо думать, что это касается только data warehouse, проблемы которых можно закрыть, просто передав их в другой отдел — "мы вообще не будем писать никакие отчёты на Java, пусть там аналитики самостоятельно пердолятся в своём Tableau".
S>Запросто может оказаться, что во вполне себе OLTP-коде навёрнута развесистая логика вроде "если запрошенная лицензия ещё не проэкспайрилась, то находим SKU для её renewal и order date будет датой экспирации; если проэкспайрилась меньше месяца назад — берём SKU для renewal, но дата будет сегодняшим днём; иначе берём SKU для новой покупки, дата ордера будет сегодняшняя, но дата следующего обновления округляется до целого месяца вверх", и поверх этого всякие "если продукт снят с продажи — поискать продукт следуюшей версии в таблице преемственности; если нет новой версии — поискать продукт в списке преемственности; если лицензия учитывает платформу — поискать продукт с такой же платформой; если не нашёлся или лицензия была anyplatform — поискать продукт anyplatform", и такой ботвы — шесть страниц со сносками, примечаниями, и исключениями.
S>Вот эта вот вся штука — одна функция типа "найди мне код продукта для продления лицензии", один входной параметр, один выходной параметр.
Ну как видишь эта самая фунция бизнес-логики состоит из меньших шагов бизнес логики с кучей "если-то". И это всё бизнес-логика. Вот и бей на части.
И где же тут эта ваша структурная эквивалентность? Где метапрограммирование? Где буквальный текст sql? И имя функции fn1?

S>Если вы захотите покрыть всё это при помощи заботливо приготовленной тестовой таблицы (точнее, пяти таблиц) и некоторого набора запросов — удачи.

Ну так еште слона по частям. В одной части "находим SKU для её renewal и order date", "поискать продукт в списке преемственности", "поискать продукт с такой же платформой" и т.п. Это всё бизнес-кейсы для тестов разных методов репы.
А потом из этого собирается цельный бизнес-метод дёргающий в нужном порядке моки предыдущих.

S>Обратите внимание — тут как раз юмор в том, что "ожидания пользователя" — это как раз то, что "если вот такой поиск не удался, то будем использовать вот такой". А не так, что "да похрен, как там SQL написан — главное, чтобы она возвращала что должна". Нет никакой возможности даже сформулировать "что должна" в терминах конечных данных. Например, потому, что прайслиста за июнь 2024 ещё не существует в природе, и какие там будут данные — а хрен его знает. Какие-то продукты будут закрыты вовсе, какие-то смигрированы на другие платформы и так далее.

S>И вот такого как правило в реальных системах — дохрена и больше. Иногда разработчики просто делают вид, что этого ужаса не существует — ну, такое отрицание реальности. Дескать, "да что там может пойти не так — я же прогоняю тесты с настоящим постгре, и даже запихиваю туда тестовые данные". А потом выясняется, что QA-шники в тестовых данных позабивали всяких очевидных "острых случаев" вроде пользователей с именем AAAAAAAAAAAAAAA, а потом кто-то в реальной системе пытается продлить подписку, за которую оформлен возврат, и получается непредвиденный результат.
А что предлагается-то взамен, я не понял.

S>·>Именно! Против этих всех ручных пассов я и возражаю. Верю, что писать запросики в консоли и копипастить в код было ещё ок 30 лет назад... но пора сказать стоп! Ровно так же можно подключаться из тестов к какому хошь движку и напрямую _автоматически_ выполнять запросы и _автоматически_ ассертить результаты чем вручную копипастить туда-сюда из консоли в код и проверять глазками, похож ли результат ли на правду.

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

S>·>А если sqlite (есть кстати ещё h2 которая неплохо умеет притворяться популярными субд) чем-то не устраивает, то запустить с точностью до бита идентичный проду постре что локально, что в CI-пайплайне для прогона тестов — дело минут в современном мире докеров.

S>Минут????? Ну круто — вы только что предложили перейти от тестов, которые исполняются миллисекунды, к тестам длиной в минуты.
Минуты на разворачивание тестового окружения. Тесты потом пачками гоняются быстро.

S>И у нас ещё будет большой вопрос про согласованность тестов и данных.

S>Потому что есть два подхода:
S>1. Берём контейнер с СУБД, маунтим образ тестовой БД, прогоняем тесты и ассерты
S>2. Берём контейнер с СУБД, создаём пустую БД, накатываем в неё тестовые данные, прогоняем тесты и ассерты.
S>Первый вариант — быстрее, но рискованнее: наши ассерты должны соответствовать содержимому тестовой БД. Можно запросто сломать пачку тестов, просто добавив пару записей. И вернуться к тому, с чего начинали — глазками просматривать различия между ожиданиями и реальностью, и править расхождения.
S>Второй вариант — надёжнее, т.к. параметры и ожидания лежат рядом друг с другом, и скорее соответствуют друг другу.
S>Но он и заметно медленнее — потому, что нам нужен какой-никакой размер тестовых данных, а коммит в СУБД работает сильно медленнее, чем восстановление файла из образа.
S>Ну, и есть трудности с параллельным исполнением. Не, я понимаю, что никто нам не мешает поднять одновременно полсотни контейнеров, в кажлом из которых постгре обрабатывает свой изолированный тест.
S>Но и ресурсов на это нужно примерно в бесконечность раз больше, чем для прогона unit-тестов, проверяющих факты вроде "если у лицензии не задана платформа, то её не будем учитывать, а если задана — то будем".
Мы делали примерно так: "открываем транзакцию, суём данные, проверяем результаты запросов, откатываем транзакцию". Подавляющее число тестов можно уложить в такое.

S>·>Тут у вас вся ваша функциональщина и вылазит боком, т.к. кеш — это состояние, даже хуже того — шаред, по определению. И вся пюрешность улетучивается.

S>Никуда она не улетучивается. У нас — 90% пюрешного кода обмазаны 10% императивной грязи; а у вас — все 100% кода состоят из грязи, и даже для тестирования банальных вещей вроде арифметики приходится туда и сюда просовывать моки.
Повторюсь: "Если у нас конечная цель иметь именно nextFriday()". Про арифметику ты загнул. Я не отрицаю полезность пюре, но злоупотреблять тоже не стоит.

S>·>Приходится нехило прыгать по монадам и прочим страшным словам. А в шарпах-ts где нет нормальной чистоты, только и остаётся отстреливать себе ноги.

S>
S>Признаюсь честно — сам я на практике (в пет-проджектах) тяготею к написанию тестов именно в вашем стиле; по ряду причин. Основная из которых — непреодолимая привлекательность: сходу понятно, что именно писать. Куда проще проверить, что запрос вида "2*2+2" вернёт 6, чем сидеть и формализовывать "умножение матриц у нас должно конвертироваться вот в такой вот набор скалярных умножений, а сложение — вот в такой".
S>Но и ловится такими тестами далеко не всё. Недавно у меня случился баг как раз такого типа — вроде и возвращалось всё правильно; а поменял размеры тестовых массивов — и упс! оказалось, в одном месте при переходе от скаляров к векторам менялся знак.
Именно что "тестовых массивов". Причём тут моки и пюре? Догадался бы заранее протестировать правильные массивы, поймал бы сразу. Вне зависимости от моков.

S>·>Суть в том, что тест от прода отличается ровно одной строчкой кода, в этом и цель всего этого. И строчка эта если и меняется, то раз в никогда. И если требует изменения — это становится явным и очевидным, требует более аккуратной валидации релиза. У вас же эта вся грязь будет в каждом методе каждого контроллера.

S>Если захотим — да. А если мы увидим в этом проблему, то "вся грязь" будет выписана в виде грязной ФВП и применена единообразно. Вы же не думаете, что агрегация — это прерогатива рич-ООП?
S>Нет ровно никакой проблемы взять и добавить к чистой функции nextFriday(timestamp) грязную функцию nextFriday()=>nextFriday(Time.now()), и заменить вызовы повсеместно.
Так и обратное — на столько же верно.

S>Но это будет осознанным шагом, а не следствием случайного дизайна под давлением "ну мы хотели просто повызывать Console.WriteLine и DateTime.Now, но тим лид велел сделать юнит-тесты через моки".

Это ортогональная проблема. Console и DateTime.Now — глобальные переменные, синглтоны. И с этим борятся через DI.

S>Да нельзя, нельзя заглянуть. Вот вы только что написали код с багом, мы уже пять постов это обсуждаем, а вы, похоже, даже не поняли, что там баг.

Нету никакого бага. А вот что у тебя, что у Pauel — баги были.

S>·>Тесты, которые тестируют такой модуль, конечно, тяжелее чем типичные юнит-тесты, но они всё равно достаточно быстрые, т.к. никакого IO нет, всё внутри языка.

S>И тем не менее — тут умножили в 2 раза, там в 2 раза — так и набегает "тесты исполняются несколько часов".
Как я понял подобные тесты у Pauel будут требовать поднятия всего сразу. Т.е. там не в 2 раза, а в 2к раза, как минимум.

S>·>Это ортогональная проблема. В тестировании пюрешек ровно эта же проблема тоже есть, для параметров. Тебе приходится верить, что твои тестовые параметры изображают реальные значения. Ещё раз, моки отличаются от параметров лишь способом доставки значений. Реальность данных относится именно к самим значениям, а не способу их передачи.

S>Ну, в некоторых изолированных случаях — да.
S>В более интересных случаях, когда "мокировать" приходится более сложный набор взаимосвязей, чем 1 вход и 1 выход, распиливание на pure-куски позволяет нам упростить тестирование.
Я не понял как этот пример относится к моей цитате выше о "Реальность данных относится именно к самим значениям". Ну распилил ты, ну пюре, круто чё... и чё?.. как тестировал для значения "2", так и тестируешь. А надо было ещё протестировать и для внезапного курса зимбабвийских долларов 2_000_000_000. Упс.

S>·>А откуда в request появится ArrivalTime? И как все эти места протестировать? Учти, у нас 100500 различных видов request.

S>Как откуда? Из общей инфраструктуры. У нас система типов построена так, что в любом request есть ArrivalTime, а его генерация делается примерно в одном месте, которое обеспечивает приём запроса ещё до его роутинга.
Даже если для половины из них никакой ArrivalTime не нужен, вы всё равно его вычисляете, проставляете и протаскиваете через весь стек? Да ещё и инфраструктур может быть несколько, где-то через rest, где-то через FIX где-то через mq, в разных форматах.
И далее лавай заглядывать, откуда в этих инфрах берётся ArrivalTime и как оно тестируется?
Ну это ладно, что timestamp какой-то, а бывает и более интересные данные, которые не настолько общие чтобы стоило запихать везде.

S>·>Ы?

S>Там, где вы "старый" код, который обращался к instantSource, заменили на вызов "новой" nextFriday(instantSource.now()).
Может заменили (точнее авмтоматически отрефакторили), а может и оставили как есть. Для платёжки может логика и чуть отличаться, по-другому таймзоны или ещё чего работать.

S>·>А ты попробуй. Над мемоизацией посмеялись, над статической глобальной переменной _cache поплакали. Ещё идеи остались?

S>Вы серьёзно? Давайте вы для начала напишете корректный код кэширования, пусть даже и не самый быстрый в общем случае.
У меня он был вполне корректным, но, соглашусь, не универсальным, а подразумевающим определённое поведение зависимостей.

S>>>Вот именно об этом я и говорю. Инстант не наступил, а nextFriday уже должна возвращать новое значение. Упс.

S>·>Шозабред? Этот инстант логически эквивалентен твоему _cache.nextFriday. В любом случае, это всё элементарно покрывается юнит-тестами (ага, с моками), даже если я и налажал код в браузере правильно написать.
S>Ну, всё верно. Именно поэтому у меня там не одна пятница, а две, и код возвращает закешированное значение только в том случае, если instant попадает в их диапазон
А первая пятница у меня лежала в волатильной переменной как текущее значение для возврата до момента наступления следующей пятницы.

S>>>Так у вас и тесты ничего не покажут.

S>·>Шозабред.
S>Никакого бреда. Вы же не догадались, что ваш instantSource может возвращать время в обратном порядке — значит и тесты у вас будут только прямой порядок тестировать
Ну мой instantSource не может время в обратном порядке возвращать. Я это знаю, в реале он привязывается к системным часам, про которые я знаю, что они монотонны.

S>·>Это ты наверное имеешь в виду, если wall clock поменяет таймзону. Только это невозможно в моём случае по дизайну. Или ты не понимаешь что такое instant. Это конретная точка на линии физического времени, а не показания на циферблате часов типа "01:00 пятница" для человеков.

S>Тогда вы, наверное, неверно решили задачу. Потому что человека интересовала следующая пятница по wall time, а не абстрактная точка на линии "физического времени". Если бы речь щла о физическом времени, то вся эта ботва со следующей пятницей сводилась бы к паре делений и умножений.
Соглашусь, я явно не озвучивал этот момент. Имхо очевидно было... Конечно, пятница — это человеческая конструкция, требует правил календаря, таймзоны и т.п. А instant — это физическая штука. Да, CalendarLogic должен содержать как-то инфу о календарях, не особо важно как, таймзону инжектить в конструктор, например. Или просто использовать Clock класс, который по сути InstantSource+tz.
Более того, система типов (по крайней мере в java.time) не позволит поставить таймер на "01:00 пятницы", ибо бессмысленно. Код с такой ошибкой тупо не скомпилится.

S>>>Значит возьмёт и поменяет вызов вашего компонента Б на какую-нибудь ерунду.

S>·>А если в твоей функции джун поменяет вызов на какую-нибудь ерунду?
S>В какой именно? У меня нет двух разных calendarLogic,
Тогда я вопрос не понял "поменяет вызов вашего компонента Б на какую-нибудь ерунду". Что на что поменяет и с какого бодуна?

S>>>Так-то и в ФП ваш вариант элементарно реализуется, потому что способ закинуть замыкание в таймер есть более-менее везде.

S>·>Как?
S>В смысле "как"?
А таймер где? У тебя при cache miss будет latency spike.

S>Так и пишем:

S>Здесь у нас на каждый вызов Cache() возвращается новая функция со своим кэшем, независимым от результатов других вызовов. АФАИК, в джаве это работает так же.
Т.е. по сути класс с приватным полем и конструктором, та же ж, вид сбоку. Вот только вместо осмысленных имён типов некий Func.
Более того, ты как-то забыл, что теперь тебе придётся пройтись по всему коду, где используется nextFriday(x), понять в каждом конкретном случае что значение этого x идёт именно из instant source (а ведь это вовсе не означает, что стоит ровно nextFriday(instantSource.now()), а может протаскиваться через несколько слоёв. Потом тебе надо будет заменить все эти вызовы на обёрку Cache, ещё как-то позаботиться, что эти общие места должны шарить один инстанс Cache, иначе игра и не стоит свеч.
В моём же случае эта информация уже известна из сигнатуры nextFriday и самого CalendarLogic — всё в одном месте.

S>>>С удовольствием. Пришлите ссылку на публичную SLA, в которой есть такие требования, я почитаю.

S>·>Было дело в lmax. Насчёт публичных доков — не знаю.
S>Ну, LMAX — парни известные. Но если почитать их Terms of Business, то слово latency там упомянуто всего лишь трижды, и ни в одном из случаев там не фигурируют никакие цифры. https://www.lmax.com/documents/LMAXGlobal-eu-Terms-of-Business.pdf
Ну это их selling point. В рекламе обещают 300us mean time и 4ms для 99.99. Или что-то в этом духе.

S>Наверняка вы с лёгкостью найдёте каких-то других парней (да хоть вашего работодателя), где есть SLA c параметрами латентности, да ещё и не процентилях, а в виде ограничения сверху.

S>Я в такое не очень верю — компания, которая такое рисует, будет либо в качестве штрафов писать что-нибудь типа скидки на следующую оплату услуг, либо очень быстро вылетит из бизнеса.
Тут прямая конкуренция за latency. Чем она лучше, тем market maker может делать меньше спред. Чем меньше спред, тем привлекательнее цена. Чем привлекательнее цена, тем выше шанс, что сделка состоится именно на этой бирже. Каждая сделка — копеечка в прибыль биржи.
Если market maker видит скачки latency, это означает, что он расширит спред, и сделки будут идти на других биржах.

S>·>Ясен пень. Но это ещё и означает, что такие фривольности с глобальными кешами просто недопустимы. Т.к. промахи могут быть недетерминированными и все эти бенчмарки их ловят негарантированно.

S>Вопрос к автору бенчмарков
Бенчмарки — не панацея.

S>·>Как в примере со _слишком_ старой платёжкой. Обычно платёжки не очень старые и даже прогон месячной прод-нагрузки может ничего не выявить, а случайный залётный дятел попадается редко, но метко.

S>Расскажите, как в вашем подходе решается эта проблема. Как вы собираетесь поймать "клиента со слишком старой платёжкой"?
Не надо его ловить. Надо контролировать какие процессы происходят в latency sensitive пути на уровне дизайна.

S>·>Требоваться может везде (или почти везде), в этом и проблема. Суть примера была в том, что "всё приложение" использует не общий "nextFriday(x)", а конкретный "nextFriday(Time.Now)" во многих местах (см. моё начальное "Если у нас конечная цель иметь именно nextFriday()"). Поэтому этот общий код у меня и вынесен в одно место, которое безопасно кешировать, т.к. известен этот самый x. У тебя — неизвестно, в этом и беда. Напомню: ЧПФ.

S>На это уже ответил выше — ЧПФ придумали не в ООП
Да я знаю. Просто перевожу на другой язык. Обсуждаемая в начале топика статья — там как раз было про это — DI/CI это ЧПФ. Просто пытаюсь донести мысль — что если мы заменяем класс на лямбду — меняем лишь как код выражен на ЯП, но это не делает никакой волшебной магии.

S>>>Нет такого факта, я вам в который раз повторяю. В вашем instantSource нет никакой гарантии, что следующий вызов now() не вернёт момент времени раньше, чем предыдущий.

S>·>Это не так. В худшем случае, я могу тупо найти все те одно-два места использования _конструктора_ CalendarLogic удостовериться что там может быть и какими свойствами обладает. Ещё раз. ЧПФ.
S>Вы повторяете это как мантру. ЧПФ вам тут никак не поможет — нет такой системы типов, которая бы позволила вам записать монотонность стейтфул-функции.
Это ты куда-то не в ту степь рванул. Надо уж начинать с того, что нет такой системы типов, для записи, что nextFriday возвращает хотя бы пятницу.
ЧПФ мне помогает тем, что я имею один кусок кода в котором происходит композиция Ф и одного из её парамов.

S>>>Всё правильно — весь стейт торчит на самом верху, и его мало. А глубокие вызовы все pure, и позволяют тестироваться по табличкам безо всяких моков.

S>·>Может я не понял, что ты называешь самым верхом, но это у вас — метод контроллера, коих в типичном приложении сотни. У же меня этим верхом является один на всех composition root.
S>Если нас не устраивает высота "метода контроллера", то мы запихиваем то, что вы называете "composition root" туда, откуда эти контроллеры вызываются.
Ну тогда об чём спор. У вас всё то же самое, как и в 00х, как я и говорил, просто переложенное на синтаксис js. Да моки в примере у Фаулера мы тоже нашли.

S>>>Просто будет локальная переменная в замыкании.

S>·>Как эта локальная переменная будет шариться между контроллерами?
S>Как захотим — так и будет. Через указатель на функцию
Ок, теплее. А как теперь с тестами дела обстоят? В тестах этим указатель будет на мок, верно?

S>>>Так и какой смысл подменять параметр типа instant на параметр типа instantSource, который вы хотите позвать ровно один раз в рамках экземпляра бизнес-логики? С какой целью вы это делаете?

S>·>В случае trade date, instant — это физическая точка во времени, для железки. А для ордера нужны календарные показания, для трейдеров. CalendarLogic эту логику и обеспечивает.
S>Ну так этой логике нужен ровно один instant, который физическая точка во времени. А не возможность получить неограниченное количество этих инстантов, продолжая вызывать недетерминистическую timeSource.now().
Можно, но в бизнес как правило логике нигде не нужен по факту физический инстант сам по себе, нужен календарный.

S>>>Ну и прекрасно — тогда у нас есть глобальная информация о том, с какой биржей мы работаем, внутри messageHandler.

S>·>Что значит глобальная? Приложение может подключаться не нескольким биржам одновременно, разными сокетами.
S>Ну так наверное у них и хэндлеры будут разными. Вы же как-то должны понимать, с какой из бирж работаете.
Да, разными. В т.ч. по разным аспектам разными. Разные протоколы, таймзоны и т.п. Но все они используют один и тот же класс CalendarLogic.

S>>>Где у вас это выражено в коде? Как у вас срабатывают таймеры, когда время идёт "назад"?

S>·> Время не ходит назад. Это физически невозможно.
S>При replay — ещё как идёт. Вот только что был понедельник, и тут поехал replay четверга. И ещё есть много случаев.
Не понял как. replay нужен для воспроизведения происходившего в проде. В проде время назад не ходит.

S>Вы пытаетесь искать ключи под фонарём: "а вот у меня instant source — он другой, я про него всё знаю". Нет, нихрена вы не знаете, потому что у вас instantSource — это интерфейс.

Ясен пень. А твой код будет правильно работать, если nextFriday вдруг четверг возвратит? Есть же контракт...

S>Императивные инварианты — это вообще такая штука, которую крайне сложно выразить в терминах системы типов. Банальнейшие вещи вроде "делать socket.Read() нельзя до socket.Open() и после socket.Close()" в терминах ООП невыразимы.

А в какой они выразимы? В rust для такого нужен borrow checker.

S>А в реальных системах встречаются ещё и чудесные вещи типа "после того, как сделал var u = service.CreateUser(...), нужно выждать не менее 5 минут перед service.UpdateUser(u, ...)", с которыми совсем всё плохо.

Тут и borrw checker не поможет.

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

S>Поэтому беспокоиться нужно не о том, что джун заменит request.ArrivalTime на Time.now() (в конце концов, это можно отловить через styleCop и прочие радости жизни) — беспокоиться надо о том, как покрыть все ожидания адекватными тестами. В том числе и все вот эти вот ожидания по поводу валидных трансформаций состояния.
Именно. И моки тут — в помощь.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[70]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 04.02.24 22:12
Оценка: +2
Здравствуйте, Sinclair, Вы писали:

Б>>Я так и не понял, как протестировать код, завязанный на внешние системы. Ответ от Pauel если и был, то размазался где-то среди кучи сообщений.

Б>>Полагаю, что работа с внешними зависимостями будут закрыты функциями, которые будут переданы в функцию do_logic. Аналогично для моков зависимости будут закрыты интерфейсом (группа функций), и он будет передан в do_logic. Т.е. с точки зрения тестирования разницы нет — в обоих случаях подменяется поведение внешней системы — передается функция возвращающая нужное для теста значение.
S>Для внешних систем у нас вариантов немного — либо моки, либо интеграционное тестирование.
+1

S>При этом я предпочитаю интеграцию — потому, что она даёт нам гораздо больше уверенности в результатах.

В каком-то смысле согласен... но это очень быстро выходит из под контроля. Начали с системы, где 5 сценариев и запустить целиком, прогнать 5 тестов — заняло минуту. Потом ВНЕЗАПНО через пару лет оказывается что сценариев уже 5000 и тестов ждать приходится часами, да ещё они как-то через раз падают, т.к. все лазят в одно и то же тестовое окружение и иногда друг-другу мешают... Но менять это уже поздно, ведь можно что-то сломать.

S>Даже "эмулятор" продакшн системы на стороне партнёра часто даёт неверные результаты.

S>У интеграции есть ровно один недостаток — она дорогая. Но обычно это приемлемая стоимость, с учётом того, что интеграционных тестов гораздо меньше. В моей практике обычно основная сложность (в которой можно напороть) спрятана именно в бизнес логике на нашей стороне; а взаимодействие со внешними системами — это ограниченное количество сценариев. И нам собственно нужно оттестировать как раз не саму логику, а расхождения между нашими ожиданиями от внешней системы и её реализацией.
Ну вот самый тупой кейс — system clock — это и есть взаимодействие с внешней системой. И никакими вменяемыми интеграционными тестами не покрывается.

S>Вот, кстати, когда мы работали с CREST API от Microsoft, нами был напилен набор интеграционных тестов, которые гонялись в каждом acceptance. И мы чаще всего ловили там не глюки нашего софта (которые были отловлены юнит-тестами), а регрессию со стороны Microsoft .

S>Было всего несколько случаев, когда поведение API было не таким, как мы ожидали, и это не было признано багом.
Это называется conformance tests. У нас был специальный набор тестов, который тестировал внешние системы на соответствие нашим ожиданиям. Иными словами, в наших моках "мы думаем" что система работает так. И вот это самое "мы думаем" — покрывается conformance тестами.

S>Но я могу себе представить ситуацию, когда 100% интеграционное тестирование недоступно или настолько дорого, чтобы оправдать написание моков. Тут вы совершенно правы — принцип остаётся ровно тем же самым: мы будем передавать в do_logic какие-то функции, эмулирующие поведение внешней системы.

Ваша ситуация когда мы тестируем наш продукт подключаясь к внешней системе — работает тоже только в простейшем случае, когда у вас мало таких внешних систем.
И это означает, что если таких систем много, что мы можем пройти acceptance для нашего проекта только в момент когда _все_ внешние тестовые системы работают.
Если хоть что-то из внешних систем сейчас лежит, вы не можете ничего зарелизить. При росте количества систем вероятность что все они будут работать одновременно быстро стремится к нулю.
Поэтому, мочим всё внешнее и делаем релиз независимым. Делаем conformance tests и убеждаемся что наши моки всё ещё адекватны. Conformance tests можно гонять независимо от наших релизов.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[69]: Что такое Dependency Rejection
От: Sinclair Россия https://github.com/evilguest/
Дата: 05.02.24 02:41
Оценка:
Здравствуйте, ·, Вы писали:

·>composition root — это технический, "подвальный" код. Конкретно инжекция системного источника времени — одна строчка кода, которая за жизнь проекта трогается практически ни разу. И тестируется относительно просто — приложение скомпилировалось, стартовало, даёт зелёный healthcheck — значит работает.

В ФП можно аналогично.
·>У тебя же now() лежит в коде бизнес-логики (как я понял из объяснений Pauel). Будет стоять в каждом втором методе контроллеров, т.е. в сотнях мест и код этот постоянно меняется.
По умолчанию — да. YAGNI в полный рост. По мере необходимости — оборудуем функции контекстом, в который запихиваем всю повторно используемую грязь.

·>Если бы такое писать на хаскеле... А так это всё лирика. На практике же — понадобился кеш и полезло static по ногам стрелять...

Никакой лирики, никакой стрельбы.

·>В сигнатуре nextFriday() это собсвенно негде даже отражать, в этом и суть. А в CalendarLogic — отразить можно. Опять же — это одно место на всё приложение.

В теории — да. А на практике вы об этом даже не задумались. Потому что это неявные связи

S>>Ну, это ещё более хороший пример — потому, что чем дальше вы навешиваете на эту lastFriday стейтфул-сложность, тем дороже и хуже будет обходиться его тестирование.

·>Логично, чем больше логики, тем больше тестов. Стейт тут не при чём.
Вполне себе при чём. Вся сложность — именно в том, что у вас там добавляется дюжина stateful-компонентов. И результат зависит не от параметров функции, а от состояния этих компонентов. Которое ещё и меняться может во время исполнения вашего кода логики.

·>Если сложность конкретно CalendarLogic будет выходить за рамки разумного, то начнём резать части логики в отдельные компоненты.

И они по-прежнему останутся stateful.

·>Не для написания мока, а для написания теста. Как и у тебя. Ешё раз — мок средство доставки тестовых данных, а не сами данные. Именно данные нужно проводить в соответствие с реальностью. Даже вообще я не очень понимаю, что за процесс такой "написание мока".

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

·>И? Ты тоже рассуждаешь об абстрактных проблемах программирования вместо того чтобы сравнивать "моки vs немоки".

Это не абстрактные проблемы программирования. Это то, на что мы налетали, когда переходили от тестирования с "моками" и "стабами" к интеграционному тестированию.

·>Мокаем это значит определяем контракт. И ожидаем, что он выполняется реальным репо. А реальный репо тестируется и-тестом с субд, на то, что он этот же контракт выполняет. Цель всего это действа — отпилить быстрые легковесные тесты логики от тяжелых тестов интеграции яп с субд.

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

S>>Потому что разница, собственно, примерно такая: вот у нас есть цепочка зависимостей A->B->C->D, где A — это у нас БЛ, B — это query-часть ORM, С — это генератор SQL-запросов, а D — это БД с тестовыми данными.

S>>И вы типа говорите "мы хотим тестировать всю цепочку, чтобы сэкономить на поддержании тестов в случае замены компонентов B/C".
·>Не очень понял чем B от C отличается.
B — это та часть, которая отвечает за построение запроса. В Hibernate это Criteria API; в Linq — это собственно IQueryable и его методы. C нам нужен тогда, когда запрос полностью сформирован и можно отправлять его в СУБД на исполнение.
·>Но суть моего тезиса в том, что просто сгенерированный SQL особо не нужен. В тесте его нужно сгенерировать (не важно как), загнать туда параметры, выполнить, ассертить результаты.
Это вам кажется, что SQL не особо нужен. Так-то и now() не особо нужен — нужна реализация пользовательского сценария. Но получить полное покрытие всех путей исполнения в таком обобщающем подходе невозможно, поэтому мы делим решение на части и каждую тестируем отдельно.
Вы, получается, тестируете не только свою БЛ, но и заодно тестируете репо, а также саму СУБД. То, о чём говорит Pauel — это доведение идеи "давайте проверим, что мы вызываем правильные методы repo в правильном порядке" до логического завершения: если у нас в роли репо выступает сама СУБД, то мы проверяем ровно то, что наша логика вызывает правильные запросы в правильном порядке.
Если в роли репо выступает нормальный ОРМ, то мы проверяем корректность построения ОРМ-запроса.

·>Если ты это уже знаешь, то в чём проблема это загнать через save в тестовые данные?

Проблема в экспоненциальности. Вот вы сходу можете сказать, сколько должно быть записей в тестовом наборе, который позволит протестировать корректность сортировки по каждому из 10 критериев?
·>Никто ж не запрещает использовать больше 2 записей. Тут просто желательно делать поменьше записей, чтобы человекам было проще разобраться в сценарии.
Отож.

·>Ну как видишь эта самая фунция бизнес-логики состоит из меньших шагов бизнес логики с кучей "если-то". И это всё бизнес-логика. Вот и бей на части.

Вот мы и будем бить на части.
·>И где же тут эта ваша структурная эквивалентность?
А ровно там, где мы будем проверять, что в IQueryable был добавлен нужный критерий.

·>Где метапрограммирование? Где буквальный текст sql? И имя функции fn1?

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

·>Ну так еште слона по частям. В одной части "находим SKU для её renewal и order date", "поискать продукт в списке преемственности", "поискать продукт с такой же платформой" и т.п. Это всё бизнес-кейсы для тестов разных методов репы.

Ну, всё верно. Просто вы это себе видите методами репы, а мы — структурой запроса. У вас получается, что отделение репы от БЛ — фикция, т.к. при любом изменении требований нужно менять обе части решения. Это — канонический критерий того, что разбиение на компоненты выполнено неправильно.

·>А потом из этого собирается цельный бизнес-метод дёргающий в нужном порядке моки предыдущих.



·>А что предлагается-то взамен, я не понял.

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

S>>В мало-мальски реалистичном случае вы вспотеете автоматически ассертить результаты запроса к данным.

·>Если вспотею делать это автоматически, то если такое придется делать вручную, то просто сдохну. Да ещё перед каждым релизом, как минимум!
Ну отчего же. Если всё правильно сделано, то перед релизом достаточно убедиться, что ничего не отломано. Примерно как у вас в начале поста — если завелось и лампочки зелёные, то скорее всего и всё остальное тоже сработает.

·>Минуты на разворачивание тестового окружения. Тесты потом пачками гоняются быстро.

Опять-таки с чем сравнивать. Проверки структуры запросов — это микросекунды. Тесты против СУБД — секунды.

·>Мы делали примерно так: "открываем транзакцию, суём данные, проверяем результаты запросов, откатываем транзакцию". Подавляющее число тестов можно уложить в такое.

Можно, но это медленно. Возвращаюсь к вопросу о том, сколько записей нужно для проверки сортировки. Предположим, вам не нужно проверять сложные случаи вроде того, что после "А.Иванов" идёт "А. Иванов", а не "А.Петров".

·>Повторюсь: "Если у нас конечная цель иметь именно nextFriday()". Про арифметику ты загнул. Я не отрицаю полезность пюре, но злоупотреблять тоже не стоит.

Ну почему же загнул. Собственно, nextFriday — это арифметика чистой воды.

·>Именно что "тестовых массивов". Причём тут моки и пюре? Догадался бы заранее протестировать правильные массивы, поймал бы сразу. Вне зависимости от моков.

При том, что если бы я тестировал не конечный результат, а построение SIMD-кода по скалярному, то поймал бы это сразу. Ещё до того, как придуман первый тестовый массив.


S>>Нет ровно никакой проблемы взять и добавить к чистой функции nextFriday(timestamp) грязную функцию nextFriday()=>nextFriday(Time.now()), и заменить вызовы повсеместно.

·>Так и обратное — на столько же верно.
Ну, так зачем же вы сопротивляетесь?

·>Это ортогональная проблема. Console и DateTime.Now — глобальные переменные, синглтоны. И с этим борятся через DI.

Math.Sqrt() — тоже синглтон. Будете бороться через DI?

·>Нету никакого бага. А вот что у тебя, что у Pauel — баги были.

Есть. nextFriday должна вычисляться по wall time, и она так и делала, пока ей в аргументы отдавали now(). А вы то ли заменили её на вычисление следующей пятницы по "физическому времени" (кстати, а в какой тайм зоне?), то ли оставили как есть, но с багом при смене тайм зоны пользователя.
А баги, что у меня, что у Pauel — это нефункциональные параметры. Вы попытались задним числом изменить условия забега, чтобы выиграть, но так не получится. Да, бывают ситуации, в которых нефункциональные требования тоже повышаются до обязательных; но не бывает ситуаций, когда ради перформанса можно пожертвовать функциональностью.

·>Как я понял подобные тесты у Pauel будут требовать поднятия всего сразу. Т.е. там не в 2 раза, а в 2к раза, как минимум.

Да, но их будет мало.

·>Я не понял как этот пример относится к моей цитате выше о "Реальность данных относится именно к самим значениям". Ну распилил ты, ну пюре, круто чё... и чё?

И то, что я таким образом сокращаю объём данных, необходимых для тестирования.

·>Даже если для половины из них никакой ArrivalTime не нужен, вы всё равно его вычисляете, проставляете и протаскиваете через весь стек?

Конечно. Вычисление нам всё равно понадобится — мы же в логи его пишем.
А "протаскивание" стоит примерно 0, потому что речь идёт просто об ещё одном поле в структуре, указатель на которую лежит в RAX.

·>Да ещё и инфраструктур может быть несколько, где-то через rest, где-то через FIX где-то через mq, в разных форматах.

И в каждой это будет вычисляться по своему. Делов-то.
·>И далее лавай заглядывать, откуда в этих инфрах берётся ArrivalTime и как оно тестируется?
Откуда-то берётся. Тестируется — известно как: каждый вид канала обслуживается своим кусочком; естественно он обложен тестами по самое не балуйся.
В рамках разработки БЛ об этом думать нет никакой необходимости. Точно так же, как нет нужды проверять способность Oracle исполнять SQL, или способность ASP.NET корректно достать запрос из http.sys и дороутить его до кода контроллера.
·>Ну это ладно, что timestamp какой-то, а бывает и более интересные данные, которые не настолько общие чтобы стоило запихать везде.
Предлагайте варианты — посмотрим что можно сделать.

·>Может заменили (точнее авмтоматически отрефакторили), а может и оставили как есть. Для платёжки может логика и чуть отличаться, по-другому таймзоны или ещё чего работать.

То, что вы рассказываете — верный путь к деградации качества.

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



S>>Ну, всё верно. Именно поэтому у меня там не одна пятница, а две, и код возвращает закешированное значение только в том случае, если instant попадает в их диапазон

·>А первая пятница у меня лежала в волатильной переменной как текущее значение для возврата до момента наступления следующей пятницы.
Ну, всё верно. Поэтому она непригодна для обработки ситуации, когда now() выдаёт значение раньше, чем "нынешняя пятница".

·>Ну мой instantSource не может время в обратном порядке возвращать. Я это знаю, в реале он привязывается к системным часам, про которые я знаю, что они монотонны.

А исходная now(), которую вы решили заменить на instantSource, могла.

·>Соглашусь, я явно не озвучивал этот момент. Имхо очевидно было... Конечно, пятница — это человеческая конструкция, требует правил календаря, таймзоны и т.п. А instant — это физическая штука. Да, CalendarLogic должен содержать как-то инфу о календарях, не особо важно как, таймзону инжектить в конструктор, например. Или просто использовать Clock класс, который по сути InstantSource+tz.

Ну, то есть вы провели некорректную замену в рамках DI.
·>Более того, система типов (по крайней мере в java.time) не позволит поставить таймер на "01:00 пятницы", ибо бессмысленно. Код с такой ошибкой тупо не скомпилится.
Ну, то есть пока что у нас нет рабочего кода, который бы делал то, что ожидает пользователь.

·>Тогда я вопрос не понял "поменяет вызов вашего компонента Б на какую-нибудь ерунду". Что на что поменяет и с какого бодуна?

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

S>>В смысле "как"?

·>А таймер где? У тебя при cache miss будет latency spike.
А, это я протупил. Ну точно так же:
    public static Func<int, int> Cache(Func<int, int> func)
    {
        int c = 42;
    var timer = new Timer(()=>{c = new Random().Next()}, null, TimeSpan.FromDays(7), TimeSpan.FromDays(7));
        return (i) => (c >= i) ? c : (c = func(i));
    }

·>Т.е. по сути класс с приватным полем и конструктором, та же ж, вид сбоку. Вот только вместо осмысленных имён типов некий Func.
"Осмысленные имена типов" часто переоценивают.

·>Более того, ты как-то забыл, что теперь тебе придётся пройтись по всему коду, где используется nextFriday(x), понять в каждом конкретном случае что значение этого x идёт именно из instant source (а ведь это вовсе не означает, что стоит ровно nextFriday(instantSource.now()), а может протаскиваться через несколько слоёв.

Ничего подобного мне не потребуется.
Напомню, что performance optimization всегда начинается не с гениальной идеи, а с бенчмарка.
У нас есть N мест, которые не устраивают по производительности. Идти мы будем по этим местам, а не по "местам применения функции".

·>Ну это их selling point. В рекламе обещают 300us mean time и 4ms для 99.99. Или что-то в этом духе.

Хотелось бы посмотреть на эту рекламу — хоть она-то публично доступна?
А 99.99 означает, что мы запросто можем себе позволить 1 т.н. spike каждые 10000 запросов. С чем вы спорите-то?

·>Если market maker видит скачки latency, это означает, что он расширит спред, и сделки будут идти на других биржах.

У market maker — точно такой же измеритель на его конце. И он не "скачки latency" анализирует, а те самые 99%, 99.9%, 99.99%. Если он смотрит усреднённую latency, то это вообще ни о чём — mean там сгладит примерно всё.

·>Бенчмарки — не панацея.

Простите, но я не знаю другого способа контролировать performance. Ну, точнее, знаю один — это Ada. Но на ней почему-то практически никто не пишет. То есть либо спроса нет, либо в использовании запредельно сложно.

S>>·>Как в примере со _слишком_ старой платёжкой. Обычно платёжки не очень старые и даже прогон месячной прод-нагрузки может ничего не выявить, а случайный залётный дятел попадается редко, но метко.

S>>Расскажите, как в вашем подходе решается эта проблема. Как вы собираетесь поймать "клиента со слишком старой платёжкой"?
·>Не надо его ловить. Надо контролировать какие процессы происходят в latency sensitive пути на уровне дизайна.
Простите, не понимаю. Как вы на уровне дизайна запретите клиенту приходить со старой платёжкой?

·>Да я знаю. Просто перевожу на другой язык. Обсуждаемая в начале топика статья — там как раз было про это — DI/CI это ЧПФ. Просто пытаюсь донести мысль — что если мы заменяем класс на лямбду — меняем лишь как код выражен на ЯП, но это не делает никакой волшебной магии.

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

·>Это ты куда-то не в ту степь рванул. Надо уж начинать с того, что нет такой системы типов, для записи, что nextFriday возвращает хотя бы пятницу.

·>ЧПФ мне помогает тем, что я имею один кусок кода в котором происходит композиция Ф и одного из её парамов.
Это прекрасно. И ничто не мешает вам перейти от "чистой, но неэффективной" nextFridayFrom(x) к "грязной, но эффективной" nextFridayFromNow(). При этом вы воспользуетесь как преимуществом того, что чистая nextFridayFrom(x)у вас уже оттестирована и ведёт себя гарантированно корректно, так и возможностью полагаться на поведение вашего источника времени.

·>Ну тогда об чём спор. У вас всё то же самое, как и в 00х, как я и говорил, просто переложенное на синтаксис js. Да моки в примере у Фаулера мы тоже нашли.



·>Ок, теплее. А как теперь с тестами дела обстоят? В тестах этим указатель будет на мок, верно?

Может быть и мок придётся сделать

S>>Ну так этой логике нужен ровно один instant, который физическая точка во времени. А не возможность получить неограниченное количество этих инстантов, продолжая вызывать недетерминистическую timeSource.now().

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

S>>Ну так наверное у них и хэндлеры будут разными. Вы же как-то должны понимать, с какой из бирж работаете.

·>Да, разными. В т.ч. по разным аспектам разными. Разные протоколы, таймзоны и т.п. Но все они используют один и тот же класс CalendarLogic.
А экземпляр у этого класса один или разные?

S>>При replay — ещё как идёт. Вот только что был понедельник, и тут поехал replay четверга. И ещё есть много случаев.

·>Не понял как. replay нужен для воспроизведения происходившего в проде. В проде время назад не ходит.
Вот именно так и ходит — вот у нас понедельник, мы запускаем replay четверга.

·>Ясен пень. А твой код будет правильно работать, если nextFriday вдруг четверг возвратит? Есть же контракт...

Ну, какой день возвращает nextFriday, проверить можно. Если задуриться, можно даже это статически доказать
Ещё Хоар придумал способ убедиться, что некая функция f (пусть даже и императивно определённая) возвращает то, что обещано контрактом.
А вот способ убедиться, что результаты последовательных вызовов методов, тем более разных, связаны какими-то соотношениями, мне неизвестен.

·>А в какой они выразимы? В rust для такого нужен borrow checker.

Rust, афаик, позволяет выражать очень ограниченное подмножество контрактов. Каким способом он предлагает решить описанную задачу? Подозреваю, что будет что-то изоморфное ФП.
Вообще, большинство доказательств строилось как раз для ФП. Как раз переход к чистым функциям позволяет нам усилить контракты так, чтобы иметь предсказуемость поведения.

S>>А в реальных системах встречаются ещё и чудесные вещи типа "после того, как сделал var u = service.CreateUser(...), нужно выждать не менее 5 минут перед service.UpdateUser(u, ...)", с которыми совсем всё плохо.

·>Тут и borrw checker не поможет.
Ну вот и я о чём.

S>>Поэтому беспокоиться нужно не о том, что джун заменит request.ArrivalTime на Time.now() (в конце концов, это можно отловить через styleCop и прочие радости жизни) — беспокоиться надо о том, как покрыть все ожидания адекватными тестами. В том числе и все вот эти вот ожидания по поводу валидных трансформаций состояния.

·>Именно. И моки тут — в помощь.
Главное, чтобы они не превращались в самоцель .
Там, где можно обойтись без моков, лучше обойтись без моков. Ну, если уж припёрло, тогда можно и моки впилить. Но не раньше
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[70]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 05.02.24 13:01
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>·>composition root — это технический, "подвальный" код. Конкретно инжекция системного источника времени — одна строчка кода, которая за жизнь проекта трогается практически ни разу. И тестируется относительно просто — приложение скомпилировалось, стартовало, даёт зелёный healthcheck — значит работает.

S>В ФП можно аналогично.
Можно, конечно. Но тогда будет тестирование с моками.

S>·>У тебя же now() лежит в коде бизнес-логики (как я понял из объяснений Pauel). Будет стоять в каждом втором методе контроллеров, т.е. в сотнях мест и код этот постоянно меняется.

S>По умолчанию — да. YAGNI в полный рост. По мере необходимости — оборудуем функции контекстом, в который запихиваем всю повторно используемую грязь.
Угу, и для тестирования ВНЕЗАПНО начинают требоваться моки.

S>·>Если бы такое писать на хаскеле... А так это всё лирика. На практике же — понадобился кеш и полезло static по ногам стрелять...

S>Никакой лирики, никакой стрельбы.
Ну вы оба себе и стрельнули.

S>·>В сигнатуре nextFriday() это собсвенно негде даже отражать, в этом и суть. А в CalendarLogic — отразить можно. Опять же — это одно место на всё приложение.

S>В теории — да. А на практике вы об этом даже не задумались. Потому что это неявные связи
Мысли мои читаешь — о чём я думаю, о чём не думаю. Что Pauel телепат, что ты.

S>·>Логично, чем больше логики, тем больше тестов. Стейт тут не при чём.

S>Вполне себе при чём. Вся сложность — именно в том, что у вас там добавляется дюжина stateful-компонентов. И результат зависит не от параметров функции, а от состояния этих компонентов. Которое ещё и меняться может во время исполнения вашего кода логики.
Welcome to real life.

S>·>Если сложность конкретно CalendarLogic будет выходить за рамки разумного, то начнём резать части логики в отдельные компоненты.

S>И они по-прежнему останутся stateful.
На это ещё я два месяца назад ответил: "внутри CalendarLogic отрефакторить nextFriday() так, чтобы оно звало какой-нибудь static nextFriday(now) который можно потестирвать как пюрешку". Это всё мелкие тривиальности, скукота детсадовская.

S>·>Не для написания мока, а для написания теста. Как и у тебя. Ешё раз — мок средство доставки тестовых данных, а не сами данные. Именно данные нужно проводить в соответствие с реальностью. Даже вообще я не очень понимаю, что за процесс такой "написание мока".

S>Во времена оные мок был просто альтернативной реализацией того же интерфейса, что и "настоящий" объект. И писался, естественно, вручную.
S>То, что у вас сейчас есть возможность описывать мок декларативно — это деталь реализации.
В "оные" времена это какие? Мы тут статью 2023 года читали, оно и есть: альтернативная реализациия того же интерфейса. Появилсь что-то новее?

S>·>И? Ты тоже рассуждаешь об абстрактных проблемах программирования вместо того чтобы сравнивать "моки vs немоки".

S>Это не абстрактные проблемы программирования. Это то, на что мы налетали, когда переходили от тестирования с "моками" и "стабами" к интеграционному тестированию.
Халва-халва. "У нас не моки, у нас альтернативная реализация".

S>·>Мокаем это значит определяем контракт. И ожидаем, что он выполняется реальным репо. А реальный репо тестируется и-тестом с субд, на то, что он этот же контракт выполняет. Цель всего это действа — отпилить быстрые легковесные тесты логики от тяжелых тестов интеграции яп с субд.

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

S>>>И вы типа говорите "мы хотим тестировать всю цепочку, чтобы сэкономить на поддержании тестов в случае замены компонентов B/C".

S>·>Не очень понял чем B от C отличается.
S>B — это та часть, которая отвечает за построение запроса. В Hibernate это Criteria API; в Linq — это собственно IQueryable и его методы. C нам нужен тогда, когда запрос полностью сформирован и можно отправлять его в СУБД на исполнение.
Т.е. если нам придётя немного переделать какие-то методы с linq/criteria/etc на чего-то другое — всё посыпется, и тестов нет, т.к. ютесты все завязаны на конкретную реализацию и бесполезны. А ваших интеграционных тестов значительно меньше, следовательно все эти разные вариации не — тупо не покрыты.

S>·>Но суть моего тезиса в том, что просто сгенерированный SQL особо не нужен. В тесте его нужно сгенерировать (не важно как), загнать туда параметры, выполнить, ассертить результаты.

S>Это вам кажется, что SQL не особо нужен. Так-то и now() не особо нужен — нужна реализация пользовательского сценария. Но получить полное покрытие всех путей исполнения в таком обобщающем подходе невозможно, поэтому мы делим решение на части и каждую тестируем отдельно.
now() отражает момент времени. Он и в описании бизнес-сценариев напрямую фигурирует "в момент когда пришел запрос, проверяем trade date". SQL-запросов я никогда в бизнес-требованиях не видел. Так что твоя аналогия в топку.

S>Вы, получается, тестируете не только свою БЛ, но и заодно тестируете репо, а также саму СУБД. То, о чём говорит Pauel — это доведение идеи "давайте проверим, что мы вызываем правильные методы repo в правильном порядке" до логического завершения: если у нас в роли репо выступает сама СУБД, то мы проверяем ровно то, что наша логика вызывает правильные запросы в правильном порядке.

S>Если в роли репо выступает нормальный ОРМ, то мы проверяем корректность построения ОРМ-запроса.
Неверно. Я тестрирую какие данные мой репо возвращает в сооветствующих бизнес-сценариях. Что там внутре как-то собирается запрос, параметризуется, где-то как-то выполняется, парсится резалтсет — это всё неинтересные с тз теста детали.

S>·>Если ты это уже знаешь, то в чём проблема это загнать через save в тестовые данные?

S>Проблема в экспоненциальности. Вот вы сходу можете сказать, сколько должно быть записей в тестовом наборе, который позволит протестировать корректность сортировки по каждому из 10 критериев?
Проблема в том, что ты словоблудишь. Ровно та же экспоненциальность будет и в твоём процессе с прогонкой вручную написанных запросов на неких (каких?) данных тестовой базы. Вот только программно можно обрабатыать больше деталей, чем ты способен анализировать вручную глазками.

S>·>И где же тут эта ваша структурная эквивалентность?

S>А ровно там, где мы будем проверять, что в IQueryable был добавлен нужный критерий.
А как ты будешь убеждаться, что он нужный? А то что он всё ещё нужный спустя код после пачки исправлений соседнего кода? Каждый раз вручную?

S>·>Где метапрограммирование? Где буквальный текст sql? И имя функции fn1?

S>Буквальный текст SQL остался там, где мы напрямую лезем в базу из бизнес-логики.
И в коде тестов.

S>·>Ну так еште слона по частям. В одной части "находим SKU для её renewal и order date", "поискать продукт в списке преемственности", "поискать продукт с такой же платформой" и т.п. Это всё бизнес-кейсы для тестов разных методов репы.

S>Ну, всё верно. Просто вы это себе видите методами репы, а мы — структурой запроса. У вас получается, что отделение репы от БЛ — фикция, т.к. при любом изменении требований нужно менять обе части решения. Это — канонический критерий того, что разбиение на компоненты выполнено неправильно.
Не понял зачем обе. Если меняется логика как именно надо "искать продукт в списке преемственности", то меняется только код репы.

S>·>А потом из этого собирается цельный бизнес-метод дёргающий в нужном порядке моки предыдущих.

S>
Ровно по описанному тобой бизнес-требованию. Раз так требуется, так и проверяем. Ты же будешь проверять, что имя у функции fn1, а не fn2 в каком порядке идут запятые в sql-тексте.

S>·>А что предлагается-то взамен, я не понял.

S>Взамен предлагается проверять запросы проверкой запросов, а не их результатов.
Чё? Проверять "как в коде написано"?

S>А согласованность всех частей — проверять интеграционными тестами.

Не сможешь. Т.к. комбинаторно рванут варианты согласования частей.

S>>>В мало-мальски реалистичном случае вы вспотеете автоматически ассертить результаты запроса к данным.

S>·>Если вспотею делать это автоматически, то если такое придется делать вручную, то просто сдохну. Да ещё перед каждым релизом, как минимум!
S>Ну отчего же. Если всё правильно сделано, то перед релизом достаточно убедиться, что ничего не отломано. Примерно как у вас в начале поста — если завелось и лампочки зелёные, то скорее всего и всё остальное тоже сработает.
Как убедитья?

S>·>Минуты на разворачивание тестового окружения. Тесты потом пачками гоняются быстро.

S>Опять-таки с чем сравнивать. Проверки структуры запросов — это микросекунды. Тесты против СУБД — секунды.
А толку? С таким же успехом можно проверять, что все запросы начинаются со слова "select" — жутко полезно, ага, ну чтобы sellect не пролез.

S>·>Мы делали примерно так: "открываем транзакцию, суём данные, проверяем результаты запросов, откатываем транзакцию". Подавляющее число тестов можно уложить в такое.

S>Можно, но это медленно. Возвращаюсь к вопросу о том, сколько записей нужно для проверки сортировки. Предположим, вам не нужно проверять сложные случаи вроде того, что после "А.Иванов" идёт "А. Иванов", а не "А.Петров".
Не больше (скорее всего на порядки меньше), чем в твоей базе по которой ты ручками свои запросы проверяешь.

S>·>Повторюсь: "Если у нас конечная цель иметь именно nextFriday()". Про арифметику ты загнул. Я не отрицаю полезность пюре, но злоупотреблять тоже не стоит.

S>Ну почему же загнул. Собственно, nextFriday — это арифметика чистой воды.
Я не отрицаю полезность пюре.

S>·>Именно что "тестовых массивов". Причём тут моки и пюре? Догадался бы заранее протестировать правильные массивы, поймал бы сразу. Вне зависимости от моков.

S>При том, что если бы я тестировал не конечный результат, а построение SIMD-кода по скалярному, то поймал бы это сразу. Ещё до того, как придуман первый тестовый массив.
Не знаю, я не понял суть проблемы.

S>>>Нет ровно никакой проблемы взять и добавить к чистой функции nextFriday(timestamp) грязную функцию nextFriday()=>nextFriday(Time.now()), и заменить вызовы повсеместно.

S>·>Так и обратное — на столько же верно.
S>Ну, так зачем же вы сопротивляетесь?
Я рассказываю когда это работает плохо.

S>·>Это ортогональная проблема. Console и DateTime.Now — глобальные переменные, синглтоны. И с этим борятся через DI.

S>Math.Sqrt() — тоже синглтон. Будете бороться через DI?
Ты юродствуешь или правда не догоняешь? Загляни внутрь реализаций и поищи глобальные переменны там и там. И вспомни что такое синглтон, перечитай лекции которые ты типа читал, у тебя в памяти провал.

S>·>Нету никакого бага. А вот что у тебя, что у Pauel — баги были.

S>Есть. nextFriday должна вычисляться по wall time, и она так и делала, пока ей в аргументы отдавали now(). А вы то ли заменили её на вычисление следующей пятницы по "физическому времени" (кстати, а в какой тайм зоне?), то ли оставили как есть, но с багом при смене тайм зоны пользователя.
Бред какой-то.

S>А баги, что у меня, что у Pauel — это нефункциональные параметры.

Оппа! А кеш это именно что нефункциональный параметр и есть. Внезапно! Читаем Pauel: Вот появилось нефункциональное требование "перформанс" и мы видим.

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

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

S>Да, бывают ситуации, в которых нефункциональные требования тоже повышаются до обязательных; но не бывает ситуаций, когда ради перформанса можно пожертвовать функциональностью.

Ну кешами мериться Pauel первым надумал...
И видишь какая интересная история вышла.
Трагикомедия "Функциональная Клоунада".
Акт первый. Действующее лицо Pauel: Прикручивает мемоизацию за минуту, выкатил в прод, тесты все зелёные, ну что-то вроде пока улучшения перформанса не очень заметно... надо ещё подождать для сбора статистики... хм.. гхм... Ой! Оно по OOM навернулось чего-то!!! ААА!! Синлкер, помоги!!
Акт второй. Вбегает Синклер: Ну вот же: Static cache! Ой... а перформанс всё ещё сосет.
Акт третий. Заходит заказчик с мешком патефонных иголок.
Акт четвёртый. Синклер и Pauel перелопачивают все свои контроллеры и учатся писать моки.
The End.

S>·>Как я понял подобные тесты у Pauel будут требовать поднятия всего сразу. Т.е. там не в 2 раза, а в 2к раза, как минимум.

S>Да, но их будет мало.
Т.е. не будет покрывать все эти ваши комбинации 10и типов сортировки.

S>·>Я не понял как этот пример относится к моей цитате выше о "Реальность данных относится именно к самим значениям". Ну распилил ты, ну пюре, круто чё... и чё?

S>И то, что я таким образом сокращаю объём данных, необходимых для тестирования.
Не понял как. Как было одно единственное данное "2", так и осталось. Ты в один раз сократил? Хвалю.

S>·>Даже если для половины из них никакой ArrivalTime не нужен, вы всё равно его вычисляете, проставляете и протаскиваете через весь стек?

S>Конечно. Вычисление нам всё равно понадобится — мы же в логи его пишем.
S>А "протаскивание" стоит примерно 0, потому что речь идёт просто об ещё одном поле в структуре, указатель на которую лежит в RAX.
Угу. Именно, что "примерно".

S>·>Да ещё и инфраструктур может быть несколько, где-то через rest, где-то через FIX где-то через mq, в разных форматах.

S>И в каждой это будет вычисляться по своему. Делов-то.
И тестироваться, внезапно, моками.

S>·>Может заменили (точнее авмтоматически отрефакторили), а может и оставили как есть. Для платёжки может логика и чуть отличаться, по-другому таймзоны или ещё чего работать.

S>То, что вы рассказываете — верный путь к деградации качества.
Баланс всегда — риск vs выигриш.

S>·>У меня он был вполне корректным, но, соглашусь, не универсальным, а подразумевающим определённое поведение зависимостей.

S>
Определённое поведение зависимостей вполне общеизвестно, я думал очевидно, ошибся.

S>>>Ну, всё верно. Именно поэтому у меня там не одна пятница, а две, и код возвращает закешированное значение только в том случае, если instant попадает в их диапазон

S>·>А первая пятница у меня лежала в волатильной переменной как текущее значение для возврата до момента наступления следующей пятницы.
S>Ну, всё верно. Поэтому она непригодна для обработки ситуации, когда now() выдаёт значение раньше, чем "нынешняя пятница".
Оно не может так себя вести, по крайней мере в проде.

S>·>Ну мой instantSource не может время в обратном порядке возвращать. Я это знаю, в реале он привязывается к системным часам, про которые я знаю, что они монотонны.

S>А исходная now(), которую вы решили заменить на instantSource, могла.
Ну это проблема вашего DateTime.Now(), а не конкретно моего кода. В моём коде такой проблемы нет.

S>·>Соглашусь, я явно не озвучивал этот момент. Имхо очевидно было... Конечно, пятница — это человеческая конструкция, требует правил календаря, таймзоны и т.п. А instant — это физическая штука. Да, CalendarLogic должен содержать как-то инфу о календарях, не особо важно как, таймзону инжектить в конструктор, например. Или просто использовать Clock класс, который по сути InstantSource+tz.

S>Ну, то есть вы провели некорректную замену в рамках DI.
Замену чего на что?

S>·>Более того, система типов (по крайней мере в java.time) не позволит поставить таймер на "01:00 пятницы", ибо бессмысленно. Код с такой ошибкой тупо не скомпилится.

S>Ну, то есть пока что у нас нет рабочего кода, который бы делал то, что ожидает пользователь.
Я его в браузере набирал, конечно, он не работает. Думаю там даже полно синтаксических ошибок. . Я лишь идею пытался продемонстрировать, что нужное значение обновляется по таймеру, а не вычисляется из переданных аргументов. В случае с event sourcing подходом — было бы просто событие, генерируемое таймером в поток сообщений "наступила пятница", которое в процессорах обновит это поле в CalendarLogic.

S>·>Тогда я вопрос не понял "поменяет вызов вашего компонента Б на какую-нибудь ерунду". Что на что поменяет и с какого бодуна?

S>Ну, вот вы только что рассказали, что хотите по-разному вычислять nextFriday "для платёжек" и для "всего остального", в том числе — по разному работать с временными зонами.
Ясен пень. Платёжка должна обрабатваться по правилам когда она была создана. Если платёжка прошлогодняя, у неё могли бы быть другие правила расчёта таймзон, вон недавно в России меняли правила dst.

S>Это как раз подход типичного джуна — ему дали багу про таймзоны в платёжке, он починил в одном месте, но не починил в другом. В итоге имеем тонкое расхождение в логике, которое будет заметно далеко не сразу.

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

S>>>В смысле "как"?

S>·>А таймер где? У тебя при cache miss будет latency spike.
S>А, это я протупил. Ну точно так же:
S> var timer = new Timer(()=>{c = new Random().Next()}, null, TimeSpan.FromDays(7), TimeSpan.FromDays(7));
Уже лучше. И как ты это будешь тестировать? Тест будет делать Sleep(7, Days)?

S>·>Более того, ты как-то забыл, что теперь тебе придётся пройтись по всему коду, где используется nextFriday(x), понять в каждом конкретном случае что значение этого x идёт именно из instant source (а ведь это вовсе не означает, что стоит ровно nextFriday(instantSource.now()), а может протаскиваться через несколько слоёв.

S>Ничего подобного мне не потребуется.
S>Напомню, что performance optimization всегда начинается не с гениальной идеи, а с бенчмарка.
S>У нас есть N мест, которые не устраивают по производительности. Идти мы будем по этим местам, а не по "местам применения функции".
Напоминаю, что "Если у нас конечная цель иметь именно nextFriday()".

S>·>Ну это их selling point. В рекламе обещают 300us mean time и 4ms для 99.99. Или что-то в этом духе.

S>Хотелось бы посмотреть на эту рекламу — хоть она-то публично доступна?
Не знаю, я там давно не работаю. Вот презентация какая-то, там цифири какие-то есть. https://www.infoq.com/presentations/lmax-trading-architecture/

S>А 99.99 означает, что мы запросто можем себе позволить 1 т.н. spike каждые 10000 запросов. С чем вы спорите-то?

Ни о чём не спорю. Рассказываю, что на спайки более 10ms создавали тикеты и расследовали.
Скажем, обращение к ZoneInfo в jdk может порождать чтение ресурсов с информацией о таймзонах, т.е. чтение файла, т.е. сискол, блокировку и т.п.
Сравни это с intrinsic Math.sqrt, который превращается в одну асм-инструкцию или около того, который ты с какого-то бодуна решил инжектить. Вот такая, гы, арифметика.

S>·>Если market maker видит скачки latency, это означает, что он расширит спред, и сделки будут идти на других биржах.

S>У market maker — точно такой же измеритель на его конце. И он не "скачки latency" анализирует, а те самые 99%, 99.9%, 99.99%. Если он смотрит усреднённую latency, то это вообще ни о чём — mean там сгладит примерно всё.
market maker не интерсуется mean. Его интересуют именно спайки. Спайк означает, что его опубликованная цена засевшая дольше чем надо могла прыгнуть в нетуда и совершится невыгодная сделка.

S>·>Бенчмарки — не панацея.

S>Простите, но я не знаю другого способа контролировать performance. Ну, точнее, знаю один — это Ada. Но на ней почему-то практически никто не пишет. То есть либо спроса нет, либо в использовании запредельно сложно.
Бенчмарки это тесты... они не тестируют неизвестное, а для регрессии по сути.

S>·>Не надо его ловить. Надо контролировать какие процессы происходят в latency sensitive пути на уровне дизайна.

S>Простите, не понимаю. Как вы на уровне дизайна запретите клиенту приходить со старой платёжкой?
По дизайну со старыми платёжками ходят в другое место. У тебя проблема была в том, что твой static cache оказывает влияние на всё приложение.

S>·>Да я знаю. Просто перевожу на другой язык. Обсуждаемая в начале топика статья — там как раз было про это — DI/CI это ЧПФ. Просто пытаюсь донести мысль — что если мы заменяем класс на лямбду — меняем лишь как код выражен на ЯП, но это не делает никакой волшебной магии.

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

S>·>Это ты куда-то не в ту степь рванул. Надо уж начинать с того, что нет такой системы типов, для записи, что nextFriday возвращает хотя бы пятницу.

S>·>ЧПФ мне помогает тем, что я имею один кусок кода в котором происходит композиция Ф и одного из её парамов.
S>Это прекрасно. И ничто не мешает вам перейти от "чистой, но неэффективной" nextFridayFrom(x) к "грязной, но эффективной" nextFridayFromNow(). При этом вы воспользуетесь как преимуществом того, что чистая nextFridayFrom(x)у вас уже оттестирована и ведёт себя гарантированно корректно, так и возможностью полагаться на поведение вашего источника времени.
Ну я и не спорил об этом. Вопрос был в целесообразности. В nextFriday(x) будет пол строчки кода. Больше места займёт обвязка сигнатуры самой функции. Насколько целесообразно вводить это всё при условии что цель всё равно иметь везде конкренто nextFridayFromNow и покрывать тестами надо и её тоже.

S>·>Ок, теплее. А как теперь с тестами дела обстоят? В тестах этим указатель будет на мок, верно?

S>Может быть и мок придётся сделать
ЧТД. Эту я мысль я и пытался донести. Можно сворачивать обсуждение.

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

Я ничего не ломал. Возможно я не рассказал очевидные для меня детали: инстант это Instant. Что такое "календарный инстант" — мне неведомо. А "пятница" — это из календаря уже, ясен пень.
Полагаю путаница пошла из того, что в c# нет вменяемого time api и там всё намешано без разбору.

S>Но речь-то не об этом — а ровно о том, что бизнес-логике нужен не источник инстантов, а сам инстант. Календарный.

Для этого я и делаю логику преобразования инстантов в календарные данные в классе CalendarLogic, который зависит от источкика инстантов.

S>>>Ну так наверное у них и хэндлеры будут разными. Вы же как-то должны понимать, с какой из бирж работаете.

S>·>Да, разными. В т.ч. по разным аспектам разными. Разные протоколы, таймзоны и т.п. Но все они используют один и тот же класс CalendarLogic.
S>А экземпляр у этого класса один или разные?
Ну в общем случае разные, скроее всего.

S>>>При replay — ещё как идёт. Вот только что был понедельник, и тут поехал replay четверга. И ещё есть много случаев.

S>·>Не понял как. replay нужен для воспроизведения происходившего в проде. В проде время назад не ходит.
S>Вот именно так и ходит — вот у нас понедельник, мы запускаем replay четверга.
event sourcing так не работает.

S>·>Ясен пень. А твой код будет правильно работать, если nextFriday вдруг четверг возвратит? Есть же контракт...

S>Ну, какой день возвращает nextFriday, проверить можно. Если задуриться, можно даже это статически доказать
Круто, конечно, но ни ява, ни шарп, ни ts такое, скорее всего не понятнут. А если и потянут, то это будет в лучшем случае академический этюд, чем используемый практически код.

S>·>А в какой они выразимы? В rust для такого нужен borrow checker.

S>Rust, афаик, позволяет выражать очень ограниченное подмножество контрактов. Каким способом он предлагает решить описанную задачу? Подозреваю, что будет что-то изоморфное ФП.
Не знаю, лень думать, да и оффтоп.

S>Вообще, большинство доказательств строилось как раз для ФП. Как раз переход к чистым функциям позволяет нам усилить контракты так, чтобы иметь предсказуемость поведения.

Неясно как это относится к обсуждаемомому тут источнику системного времени.

S>Ну вот и я о чём.

Неясно к чему эти все возражения, что видите ли после пятницы теоретически может наступать четверг. Нет, не может на практике, по тому что мы так договорились в нашем коде (т.к. это нахрен не надо с т.з. бизнеса), а не потому что система типов это нам доказала.

S>Там, где можно обойтись без моков, лучше обойтись без моков. Ну, если уж припёрло, тогда можно и моки впилить. Но не раньше

Ну просто под этим флагом вы почему-то проносите какие-то сомнительные практики как ручное написанипе и проверка запросов и т.п.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[71]: Что такое Dependency Rejection
От: Sinclair Россия https://github.com/evilguest/
Дата: 06.02.24 06:09
Оценка:
Здравствуйте, ·, Вы писали:
S>>В ФП можно аналогично.
·>Можно, конечно. Но тогда будет тестирование с моками.
Необязательно. "Просмотр 1 строчки глазами" != "тестирование с моками".

·>Угу, и для тестирования ВНЕЗАПНО начинают требоваться моки.

Или мы обходимся integration tests.

·>Ну вы оба себе и стрельнули.

Пока что нет. Вы усердно пытаетесь изобрести окружение, в котором наш код бы что-то сломал, но пока успехи сомнительны.

·>Мысли мои читаешь — о чём я думаю, о чём не думаю. Что Pauel телепат, что ты.

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

S>>Вполне себе при чём. Вся сложность — именно в том, что у вас там добавляется дюжина stateful-компонентов. И результат зависит не от параметров функции, а от состояния этих компонентов. Которое ещё и меняться может во время исполнения вашего кода логики.

·>Welcome to real life.
Ну, где-то это real life, а где-то — трудности, искусственно созданные самому себе.

·>На это ещё я два месяца назад ответил: "внутри CalendarLogic отрефакторить nextFriday() так, чтобы оно звало какой-нибудь static nextFriday(now) который можно потестирвать как пюрешку". Это всё мелкие тривиальности, скукота детсадовская.



·>В "оные" времена это какие?

Лет двадцать назад — как раз когда в моду вошло плотное покрытие тестированием, а основным методом декомпозиции кода было всё вот это вот ООП, и DDD с рич-моделью, где каждая ерунда реализовывалась как stateful object.

·>Халва-халва. "У нас не моки, у нас альтернативная реализация".



·>Смысл в отпиливании медленного от быстрого.

Этого недостаточно для хорошего дизайна

·>Т.е. если нам придётя немного переделать какие-то методы с linq/criteria/etc на чего-то другое — всё посыпется, и тестов нет, т.к. ютесты все завязаны на конкретную реализацию и бесполезны.

Это не "немного переделать". Это смена дизайна, сопоставимая с переездом из SQL в NoSQL. Понятно, что вашей реальности смена ORM — это частое явление, а изменения BL — редкое. Но у меня (и у Pauel, и у примерно всех проектов, с которыми я имел дело с 1999 года) всё строго наоборот.

·>А ваших интеграционных тестов значительно меньше, следовательно все эти разные вариации не — тупо не покрыты.

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

·>now() отражает момент времени. Он и в описании бизнес-сценариев напрямую фигурирует "в момент когда пришел запрос, проверяем trade date". SQL-запросов я никогда в бизнес-требованиях не видел. Так что твоя аналогия в топку.

Это вы уже задним числом требования меняете. Начиналось всё с nextFriday, который никак не может быть привязан к физическому времени, т.к. в физическом времени никаких пятниц нет, а есть только продолжительности.

·>Неверно. Я тестрирую какие данные мой репо возвращает в сооветствующих бизнес-сценариях. Что там внутре как-то собирается запрос, параметризуется, где-то как-то выполняется, парсится резалтсет — это всё неинтересные с тз теста детали.


·>Проблема в том, что ты словоблудишь. Ровно та же экспоненциальность будет и в твоём процессе с прогонкой вручную написанных запросов на неких (каких?) данных тестовой базы.
Нет такой проблемы. В моём процессе никакой экспоненциальности не будет — будет проверено, что если в параметрах функции сказано "сортировать по фамилии", то в запрос уедет "order by LastName". Всё. Нет никакой "тестовой базы". СУБД совершенно точно умеет делать order by LastName, поэтому проверять, что она там не вернёт данные в неверном порядке (какие бы они ни были), нам не надо.
А вы уже оценили количество строк, которые вам потребуется вставлять в тестовую таблицу, чтобы получить аналогичную гарантию?

·>А как ты будешь убеждаться, что он нужный? А то что он всё ещё нужный спустя код после пачки исправлений соседнего кода? Каждый раз вручную?

По-моему, кто-то начал словоблудить.

·>И в коде тестов.

Повторюсь: если мы лезем в базу напрямую из бизнес-логики, в стиле 1994, то да, код тестов может содержать прямой SQL.
Если мы пишем на чём-то новее 2005, то SQL проверять необходимости нет — какой-нибудь linq2db его генерирует заведомо корректно.

S>>Ну, всё верно. Просто вы это себе видите методами репы, а мы — структурой запроса. У вас получается, что отделение репы от БЛ — фикция, т.к. при любом изменении требований нужно менять обе части решения. Это — канонический критерий того, что разбиение на компоненты выполнено неправильно.

·>Не понял зачем обе. Если меняется логика как именно надо "искать продукт в списке преемственности", то меняется только код репы.
Ну нет конечно. У вас "репа" — это интерфейс с пачкой специализированных методов, которые отражают ровно обсуждённые этапы исполнения бизнес-логики. И вот эта бизнес-логика будет меняться чаще всего — в стиле "раньше у нас была разница между сетевыми лицензиями и индивидуальными, но с 2021 её уже нет". Поэтому список пунктов "в каком порядке что искать" будет меняться — а с ним и набор методов репы.
Или я неверно понял вашу идею? Как выглядят сигнатуры методов репы которые достают из БД нужные мне данные?

S>>·>А потом из этого собирается цельный бизнес-метод дёргающий в нужном порядке моки предыдущих.

Ну, то есть всё-таки три метода на три этапа бизнес-сценария. Сценарий поменялся — будет не три, а четыре метода, с другими аргументами.
Переписали репо, переписали тесты репы, переписали тесты бизнес-логики, которые вызывают замоканную репу. Где экономим?
S>>
·>Ровно по описанному тобой бизнес-требованию. Раз так требуется, так и проверяем. Ты же будешь проверять, что имя у функции fn1, а не fn2 в каком порядке идут запятые в sql-тексте.
Нет, зачем. Проверю, что для соответствующих веток логики в запрос добавляются соответствующие критерии.
Запятые в тексте расставляет конвертер AST запроса в SQL. Заодно у нас есть гарантия отсутствия опечаток в ключевых словах и идентификаторах.

S>>Взамен предлагается проверять запросы проверкой запросов, а не их результатов.

·>Чё? Проверять "как в коде написано"?
Почти, но не совсем.

S>>А согласованность всех частей — проверять интеграционными тестами.

·>Не сможешь. Т.к. комбинаторно рванут варианты согласования частей.
Ну так интеграционным тестам не нужно покрывать 100% сценариев.
Грубо говоря, если у меня вдруг разъехалось определение entity в коде с именем колонки в базе, то никакой юнит-тест это не поймает.
Зато первый же запрос в базу, где задействована эта entity, упадёт. Мне не нужно гонять все сотни разных вариантов запросов с десятками тысяч строк в базе, чтобы это обнаружить.


S>>Ну отчего же. Если всё правильно сделано, то перед релизом достаточно убедиться, что ничего не отломано. Примерно как у вас в начале поста — если завелось и лампочки зелёные, то скорее всего и всё остальное тоже сработает.

·>Как убедитья?
интеграционными тестами
S>>Опять-таки с чем сравнивать. Проверки структуры запросов — это микросекунды. Тесты против СУБД — секунды.
·>А толку? С таким же успехом можно проверять, что все запросы начинаются со слова "select" — жутко полезно, ага, ну чтобы sellect не пролез.
Какой конкретно тип бага вы боитесь упустить?

·>Не больше (скорее всего на порядки меньше), чем в твоей базе по которой ты ручками свои запросы проверяешь.



·>Я не отрицаю полезность пюре.


·>Не знаю, я не понял суть проблемы.

Суть проблемы очень простая — мы по скалярному коду вида a=x*b+y порождаем оптимизированный SIMD код.
Задача изоморфна компилятору. То есть на входе — текст на каком-то языке высокого уровня, на выходе — программа на другом языке.
Ваш подход (который у меня и применён) — "давайте проверим, что функция вычисления квадратного корня после компиляции компилятором реально вычисляет квадратный корень". Берём исходник, компилируем его, и начинаем серию запусков, проверяя, что для 4 функция вернёт 2, а для 9 — 3.
Проблема этого подхода — в том, что без заглядывания в целевой код невозможно понять, сколько и каких тестов для вот этой программы запустить.
Более конструктивный способ проверять компилятор — убеждаться, что для функции вычисления квадратного корня порождается код с FSQRT. Тестировать, что FSQRT корректно вычисляет квадратный корень, нам не нужно.
Этот тест сломается, если мы захотим оборудовать компилятор оптимизатором, который вставляет табличку для часто встречающихся аргументов — но оно как раз хорошо: после оптимизации нам надо будет переписать тест; зато теперь у нас будет уверенность, что оптимизация случайно не отпадёт после какого-нибудь рефакторинга или доработки. А вот тест, который проверяет результаты на 4 и 9, ничего не обнаружит.

Вот примерно так же устроена и бизнес-логика поверх RDBMS. Мне важно не то, что там вернёт select person.name from person order by person.name — мне важно, чтобы в моём запросе корректно был выбран критерий сортировки.

·>Я рассказываю когда это работает плохо.

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

S>>Math.Sqrt() — тоже синглтон. Будете бороться через DI?

·> Ты юродствуешь или правда не догоняешь? Загляни внутрь реализаций и поищи глобальные переменны там и там.
Посмотрел. Нет глобальных переменных ни там, ни там.
Дальше что?
·>И вспомни что такое синглтон, перечитай лекции которые ты типа читал, у тебя в памяти провал.
Ближе к делу, меньше лирики.

·>Бред какой-то.

Понятно.

·>Оппа! А кеш это именно что нефункциональный параметр и есть. Внезапно! Читаем Pauel: Вот появилось нефункциональное требование "перформанс" и мы видим.

Ну так я про него и говорю. Самые тяжёлые последствия от кривой реализация кэша — потеря нефункциональных требований.

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



S>>Да, но их будет мало.

·>Т.е. не будет покрывать все эти ваши комбинации 10и типов сортировки.
А этого и не надо. Зачем? Хватит одной "комбинации". Остальные заведомо работают корректно — проверено дешёвыми юнит-тестами.
Это примерно как проверять URL партнёрского API — если парочка вызовов обработана корректно, то и остальные заработают. А если мы накосячили в конфигурации, то упадёт первый же вызов.

S>>И то, что я таким образом сокращаю объём данных, необходимых для тестирования.

·>Не понял как. Как было одно единственное данное "2", так и осталось. Ты в один раз сократил? Хвалю.
А чего тут не понимать? У меня было x=y*z+w. Если каждый из трёх параметров принимает N значений, то мне нужно проверить N^3 сочетаний.
Если я распилю формулу на сложение и умножение, то я смогу протестировать сложение отдельно (N^2 значений), умножение отдельно (N^2 значений).
И ещё 1 (один) тест, который проверяет, что функция AddMul является корректной комбинацией сложения и умножения. Это если я не доверяю своей способности увидеть это глазами.

·>Угу. Именно, что "примерно".


·>И тестироваться, внезапно, моками.
Или нет. Потому что у меня, возможно, не будет грязной функции process(..., time: Future<DateTime>), которая внутри будет вызывать грязную time.Value.
У меня будет pure функция process(request), в которую я буду запихивать request-ы с различным ArrivalTime безо всяких моков.

·>Баланс всегда — риск vs выигриш.

Отож.

·>Оно не может так себя вести, по крайней мере в проде.



·>Ну это проблема вашего DateTime.Now(), а не конкретно моего кода. В моём коде такой проблемы нет.

Вы лёгким манием руки поменяли одну проблему на другую.

·>Замену чего на что?

now() на instantSource.instant().

>>Это как раз подход типичного джуна — ему дали багу про таймзоны в платёжке, он починил в одном месте, но не починил в другом. В итоге имеем тонкое расхождение в логике, которое будет заметно далеко не сразу.

·>Вот у тебя как раз подход джуна, который совершенно не понимает все тонкости календарей.

·>Уже лучше. И как ты это будешь тестировать? Тест будет делать Sleep(7, Days)?

Поскольку этот код заведомо некорректен, не очень важно чем его тестировать.

·>Напоминаю, что "Если у нас конечная цель иметь именно nextFriday()".

Я не могу себе представить ситуацию, в которой у нас возникает такая цель. Непонятно, какую проблему она решает, и как эта проблема вообще возникла.

·>Не знаю, я там давно не работаю. Вот презентация какая-то, там цифири какие-то есть. https://www.infoq.com/presentations/lmax-trading-architecture/

Ну, ок. Есть, значит. 4ms max, в 8 раз больше 99.99%, в 25 раз больше 99%. Сколько стоит промах мимо кэша nextFriday?

·>Ни о чём не спорю. Рассказываю, что на спайки более 10ms создавали тикеты и расследовали.

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

·>Сравни это с intrinsic Math.sqrt, который превращается в одну асм-инструкцию или около того, который ты с какого-то бодуна решил инжектить. Вот такая, гы, арифметика.

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

·>market maker не интерсуется mean. Его интересуют именно спайки. Спайк означает, что его опубликованная цена засевшая дольше чем надо могла прыгнуть в нетуда и совершится невыгодная сделка.

Ну, хорошо, коли так.

·>Бенчмарки это тесты... они не тестируют неизвестное, а для регрессии по сути.

И как вы предлагаете тестировать неизвестное?

·>По дизайну со старыми платёжками ходят в другое место. У тебя проблема была в том, что твой static cache оказывает влияние на всё приложение.

Нет, это у вас такая проблема, потому что единый composition root.
А у нас такая проблема появится только если мы руками её создадим.

·>Угу. Но это не избавляет от необходимости моков, т.к. от грязного кода практически невозможно избавиться.

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

·>Ну я и не спорил об этом. Вопрос был в целесообразности. В nextFriday(x) будет пол строчки кода.

Наоборот — это в nextFriday() будет полстрочки кода, с вызовом nextFriday(now()).
А в nextFriday вся вот эта тяжёлая артиллерия — с загрузкой timeZoneInfo и прочим.

·>Больше места займёт обвязка сигнатуры самой функции. Насколько целесообразно вводить это всё при условии что цель всё равно иметь везде конкренто nextFridayFromNow и покрывать тестами надо и её тоже.

Нет такой цели. Вы её выдумали для оправдания stateful дизайна.
Если посмотреть ту самую презентацию LMAX, там чёрным по белому напиcано "time sourced from events". Слайд 34, "System must be deterministic".
Господь вас упаси использовать какие-то там instantSource. Добро пожаловать в мир низкой латентности

·>ЧТД. Эту я мысль я и пытался донести. Можно сворачивать обсуждение.

Можно.

·>Я ничего не ломал. Возможно я не рассказал очевидные для меня детали: инстант это Instant. Что такое "календарный инстант" — мне неведомо. А "пятница" — это из календаря уже, ясен пень.

Дело же не в устройстве АПИ. А в том, что у меня сейчас "следующая пятница" — это через неделю; а если я поменяю таймзону — следующая пятница станет наступать через час. Да, это календарь, а не инстант, но задача формулировалась в терминах календаря. Инстант придумали именно вы, причём в рамках решения своих архитектурных задач, на которые пользователям наплевать. Пользователя очень удивит такая штука, что "следующая пятница" вдруг вместо полуночи по его локальным часам показывает 22 часа следующего четверга.

·>Для этого я и делаю логику преобразования инстантов в календарные данные в классе CalendarLogic, который зависит от источкика инстантов.

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

·>event sourcing так не работает.


·>Круто, конечно, но ни ява, ни шарп, ни ts такое, скорее всего не понятнут. А если и потянут, то это будет в лучшем случае академический этюд, чем используемый практически код.

Шарп — потянет. Но с оговорками. Про TS ничего не могу сказать, не знаю, что у них там с верификацией.

·>Неясно как это относится к обсуждаемомому тут источнику системного времени.

Это относится к контрактам, на которые вы собираетесь положиться.

·>Неясно к чему эти все возражения, что видите ли после пятницы теоретически может наступать четверг. Нет, не может на практике, по тому что мы так договорились в нашем коде (т.к. это нахрен не надо с т.з. бизнеса), а не потому что система типов это нам доказала.

Ваши договорённости не выражены в коде, и не стоят примерно ничего.

·>Ну просто под этим флагом вы почему-то проносите какие-то сомнительные практики как ручное написанипе и проверка запросов и т.п.

Это были простые примеры на пальцах, призванные помочь вам понять принцип.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[72]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 06.02.24 17:04
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>>>В ФП можно аналогично.

S>·>Можно, конечно. Но тогда будет тестирование с моками.
S>Необязательно. "Просмотр 1 строчки глазами" != "тестирование с моками".
Одной строчки в каждом методе контроллеров?

S>·>Угу, и для тестирования ВНЕЗАПНО начинают требоваться моки.

S>Или мы обходимся integration tests.
Вы не можете через integration tests протестировать системные часы.

S>·>Ну вы оба себе и стрельнули.

S>Пока что нет. Вы усердно пытаетесь изобрести окружение, в котором наш код бы что-то сломал, но пока успехи сомнительны.
Pauel поставил задачу сделать кеш. И вы оба не справились.

S>·>Мысли мои читаешь — о чём я думаю, о чём не думаю. Что Pauel телепат, что ты.

S>Зачем мысли? Я читаю код. В нём всё написано. Когда собеседник предлагает вариант кода с ошибкой — это уже что-то говорит.
Это говорит о том, что ты не понял код. Ошибки там нет. Следующая пятница календаря — соответствует определённой точке физического времени. Вот в эту точку и ставится таймер для обновления значения в кеше. Поставить таймер на некую "пятница 11:00" теоретически невозможно.

S>А когда он эту ошибку отказывается чинить даже тогда, когда на неё восемь раз укажут — тут уже никакая телепатия не нужна

Нечего чинить.

S>·>В "оные" времена это какие?

S>Лет двадцать назад — как раз когда в моду вошло плотное покрытие тестированием, а основным методом декомпозиции кода было всё вот это вот ООП, и DDD с рич-моделью, где каждая ерунда реализовывалась как stateful object.
Похрен что было 20 лет назад. Я послал ссылку на ровно такой же код от 2023 года. Ты скромно сделал вид что этого не было.

S>·>Смысл в отпиливании медленного от быстрого.

S>Этого недостаточно для хорошего дизайна
А дальше идёт вкусовщина, субъективщина и веяния моды. Хорошеметр дизайна ещё не изобрели. А медленное от быстрого можно отличить по секундомеру.

S>·>Т.е. если нам придётя немного переделать какие-то методы с linq/criteria/etc на чего-то другое — всё посыпется, и тестов нет, т.к. ютесты все завязаны на конкретную реализацию и бесполезны.

S>Это не "немного переделать". Это смена дизайна, сопоставимая с переездом из SQL в NoSQL. Понятно, что вашей реальности смена ORM — это частое явление, а изменения BL — редкое. Но у меня (и у Pauel, и у примерно всех проектов, с которыми я имел дело с 1999 года) всё строго наоборот.
Потому что проблемы бывают именно когда надо менять код по серьёзному, и тут правильно написанные тесты и приносят реальную пользу. А у вас тесты похоже "шоб було, красиво жеж".

S>·>А ваших интеграционных тестов значительно меньше, следовательно все эти разные вариации не — тупо не покрыты.

S>Ну да, есть такой момент — когда мы режем компоненты определённым образом, то при смене архитектуры одного из них придётся переделывать тесты смежных с ним систем.
Это означает, что вы переделываете без тестов вообще. Старые тесты ломаются полностью и новый код пишется с нуля, без проверки всё ещё работает.

S>·>now() отражает момент времени. Он и в описании бизнес-сценариев напрямую фигурирует "в момент когда пришел запрос, проверяем trade date". SQL-запросов я никогда в бизнес-требованиях не видел. Так что твоя аналогия в топку.

S>Это вы уже задним числом требования меняете. Начиналось всё с nextFriday, который никак не может быть привязан к физическому времени, т.к. в физическом времени никаких пятниц нет, а есть только продолжительности.
Физическое время + календарные правила => пятница.

S>·>Проблема в том, что ты словоблудишь. Ровно та же экспоненциальность будет и в твоём процессе с прогонкой вручную написанных запросов на неких (каких?) данных тестовой базы.

S>Нет такой проблемы. В моём процессе никакой экспоненциальности не будет — будет проверено, что если в параметрах функции сказано "сортировать по фамилии", то в запрос уедет "order by LastName". Всё.
А если будет "сортировать по убыванию", то уедет ошибка копипасты "order by LastName asc" и бага обнаружится только в проде по звоку от клиентов.
И это самый простейший очевидный случай. Если будет код чуть посложнее, то перепутать < и >, + или - очень даже запросто. Не знаю о других, но я путаю if-else раз в неделю, не реже.

S>Нет никакой "тестовой базы". СУБД совершенно точно умеет делать order by LastName, поэтому проверять, что она там не вернёт данные в неверном порядке (какие бы они ни были), нам не надо.

Надо проверять не то, что order by выполняется базой верно, а то что в бизнес сценарии "вывести клиентов по фамилии" генерит верный список.

S>А вы уже оценили количество строк, которые вам потребуется вставлять в тестовую таблицу, чтобы получить аналогичную гарантию?

Оценка от балды — десяток строк.

S>·>А как ты будешь убеждаться, что он нужный? А то что он всё ещё нужный спустя код после пачки исправлений соседнего кода? Каждый раз вручную?

S>По-моему, кто-то начал словоблудить.
Нет, я задаю конкретный вопрос.

S>·>И в коде тестов.

S>Повторюсь: если мы лезем в базу напрямую из бизнес-логики, в стиле 1994, то да, код тестов может содержать прямой SQL.
S>Если мы пишем на чём-то новее 2005, то SQL проверять необходимости нет — какой-нибудь linq2db его генерирует заведомо корректно.
Я ссылаюсь на кусок кода с "pattern" который мне показал Pauel. Вопросы к нему какого года этот код.

S>·>Не понял зачем обе. Если меняется логика как именно надо "искать продукт в списке преемственности", то меняется только код репы.

S>Ну нет конечно. У вас "репа" — это интерфейс с пачкой специализированных методов, которые отражают ровно обсуждённые этапы исполнения бизнес-логики. И вот эта бизнес-логика будет меняться чаще всего — в стиле "раньше у нас была разница между сетевыми лицензиями и индивидуальными, но с 2021 её уже нет". Поэтому список пунктов "в каком порядке что искать" будет меняться — а с ним и набор методов репы.
Т.е. при изменении бизнес-логики надо будет менять и тесты. Всё ок. Что не так?

S>Или я неверно понял вашу идею? Как выглядят сигнатуры методов репы которые достают из БД нужные мне данные?

Ну как слышится, так и пишется "находим SKU для её renewal" => "Sku find(License lic)". Честно говоря я не очень в теме этой предментой области и не очень понимаю что такое sku, renewal и т.п.

S>>>·>А потом из этого собирается цельный бизнес-метод дёргающий в нужном порядке моки предыдущих.

S>Ну, то есть всё-таки три метода на три этапа бизнес-сценария. Сценарий поменялся — будет не три, а четыре метода, с другими аргументами.
S>Переписали репо, переписали тесты репы, переписали тесты бизнес-логики, которые вызывают замоканную репу. Где экономим?
В тестировании бизнес-логики. Вот этот весь твой шестистраничный процесс с пачкой "если-то" будет покрыт десятками тестов, которые быстро пролетают за микросекунды.

S>>>

S>·>Ровно по описанному тобой бизнес-требованию. Раз так требуется, так и проверяем. Ты же будешь проверять, что имя у функции fn1, а не fn2 в каком порядке идут запятые в sql-тексте.
S>Нет, зачем. Проверю, что для соответствующих веток логики в запрос добавляются соответствующие критерии.
А как ты убедишься, что добавленные критерии написаны правильно? Условие "лицензия ещё не проэкспайрилась" у тебя преобразуется в код "licence.date > todayDate" — это правильное условие? Может должно быть "<"? Или вообще "<="? Или "licence.expiryDate"? Всё компилится, но результаты — неверные.
А покрывается это условие одной строчкой в бд и тремя ассертами, что-то вроде:
var licence = TestLicence.withExpiry("2024-06-25");
save(licence);
assertThat(isExpired(licence, "2024-06-24")).isFalse();
assertThat(isExpired(licence, "2024-06-25")).isFalse();
assertThat(isExpired(licence, "2024-06-26")).isTrue();

Такой код можно хоть заказчиу в чатике написать и спросить — верно ли.

S>Запятые в тексте расставляет конвертер AST запроса в SQL. Заодно у нас есть гарантия отсутствия опечаток в ключевых словах и идентификаторах.

Вау, круто. А толку...

S>>>Взамен предлагается проверять запросы проверкой запросов, а не их результатов.

S>·>Чё? Проверять "как в коде написано"?
S>Почти, но не совсем.
Совсем.

S>>>А согласованность всех частей — проверять интеграционными тестами.

S>·>Не сможешь. Т.к. комбинаторно рванут варианты согласования частей.
S>Ну так интеграционным тестам не нужно покрывать 100% сценариев.
Вам — нужно. Просто вы не можете, т.к. дизайн слишком хороший.

S>Грубо говоря, если у меня вдруг разъехалось определение entity в коде с именем колонки в базе, то никакой юнит-тест это не поймает.

S>Зато первый же запрос в базу, где задействована эта entity, упадёт. Мне не нужно гонять все сотни разных вариантов запросов с десятками тысяч строк в базе, чтобы это обнаружить.
Или не упадёт, т.к. копипаста и данное берётся не из той колонки.

S>>>Опять-таки с чем сравнивать. Проверки структуры запросов — это микросекунды. Тесты против СУБД — секунды.

S>·>А толку? С таким же успехом можно проверять, что все запросы начинаются со слова "select" — жутко полезно, ага, ну чтобы sellect не пролез.
S>Какой конкретно тип бага вы боитесь упустить?
Написанный код не соответствует ожиданиям пользователя.

S>·>Не знаю, я не понял суть проблемы.

S>Суть проблемы очень простая — мы по скалярному коду вида a=x*b+y порождаем оптимизированный SIMD код.
S>Задача изоморфна компилятору. То есть на входе — текст на каком-то языке высокого уровня, на выходе — программа на другом языке.
Т.е. ваша бизнес-домен — это компилятор. Результат компилятора — код. Тогда логично, тестировать что код порождается верный. Тут возражений нет.

S>Ваш подход (который у меня и применён) — "давайте проверим, что функция вычисления квадратного корня после компиляции компилятором реально вычисляет квадратный корень". Берём исходник, компилируем его, и начинаем серию запусков, проверяя, что для 4 функция вернёт 2, а для 9 — 3.

Логично. Т.к. наша бизнес-задача — это компилятор, а не квадратный корень.

S>Вот примерно так же устроена и бизнес-логика поверх RDBMS. Мне важно не то, что там вернёт select person.name from person order by person.name — мне важно, чтобы в моём запросе корректно был выбран критерий сортировки.

Ок, если вы пишете библиотеку orm или подобное, то согласен полностью, всё верно делаете.

S>>>Math.Sqrt() — тоже синглтон. Будете бороться через DI?

S>·> Ты юродствуешь или правда не догоняешь? Загляни внутрь реализаций и поищи глобальные переменны там и там.
S>Посмотрел. Нет глобальных переменных ни там, ни там.
S>Дальше что?
Ты троллишь что-ли?

S>Ближе к делу, меньше лирики.

rdtsc

S>·>Оппа! А кеш это именно что нефункциональный параметр и есть. Внезапно! Читаем Pauel: Вот появилось нефункциональное требование "перформанс" и мы видим.

S>Ну так я про него и говорю. Самые тяжёлые последствия от кривой реализация кэша — потеря нефункциональных требований.
В смысле? Задача была поставлена "нужно нефункциональное требование!". И как решение "не шмогли нефункциональные требования, ну и чё, зато вроде работает"? Троллите что-ли?!

S>>>Да, но их будет мало.

S>·>Т.е. не будет покрывать все эти ваши комбинации 10и типов сортировки.
S>А этого и не надо. Зачем? Хватит одной "комбинации". Остальные заведомо работают корректно — проверено дешёвыми юнит-тестами.
Нихрена не проверено. Проверено, что "works as coded".

S>Это примерно как проверять URL партнёрского API — если парочка вызовов обработана корректно, то и остальные заработают. А если мы накосячили в конфигурации, то упадёт первый же вызов.

Зависит от кода. Если у вас каждый вызов к базовому урлу аппендит разные строчки с именами разных ресурсов, то их надо валидировать как-то, обязательно. Либо по их swagger-like схеме с валидированной версией, либо conformance tests, если схемы нет.

S>>>И то, что я таким образом сокращаю объём данных, необходимых для тестирования.

S>·>Не понял как. Как было одно единственное данное "2", так и осталось. Ты в один раз сократил? Хвалю.
S>А чего тут не понимать? У меня было x=y*z+w. Если каждый из трёх параметров принимает N значений, то мне нужно проверить N^3 сочетаний.
S>Если я распилю формулу на сложение и умножение, то я смогу протестировать сложение отдельно (N^2 значений), умножение отдельно (N^2 значений).
S>И ещё 1 (один) тест, который проверяет, что функция AddMul является корректной комбинацией сложения и умножения. Это если я не доверяю своей способности увидеть это глазами.
Я не знаю о чём ты тут говоришь. Тут речь шла о твоём конкретном коде с GetItems и тестом со значением данного "2". Ты видимо лажанулся, сейчас ужом извиваешься подменяя тему.

S>·>И тестироваться, внезапно, моками.

S>Или нет. Потому что у меня, возможно, не будет грязной функции process(..., time: Future<DateTime>), которая внутри будет вызывать грязную time.Value.
S>У меня будет pure функция process(request), в которую я буду запихивать request-ы с различным ArrivalTime безо всяких моков.



S>·>Оно не может так себя вести, по крайней мере в проде.

S>
Да. Бизнес требует наличия PTP на прод серверах и монотонный rtc источник.

S>·>Ну это проблема вашего DateTime.Now(), а не конкретно моего кода. В моём коде такой проблемы нет.

S> Вы лёгким манием руки поменяли одну проблему на другую.
S>·>Замену чего на что?
S>now() на instantSource.instant().
Ты видимо дальше носа не видишь. Загляни внутро now(). Там несколько синглтонов — локаль, календарь, таймзона, источник времени.
В этом и был мой поинт — нельзя использовать Now в коде логики, который синглтон синглтонами погоняющий. А надо явно испоьзовать DI и инжектить источник времени в composition root.

S>·>Уже лучше. И как ты это будешь тестировать? Тест будет делать Sleep(7, Days)?

S>Поскольку этот код заведомо некорректен, не очень важно чем его тестировать.
Т.е. ты корректный код написать не в состоянии?..

S>·>Напоминаю, что "Если у нас конечная цель иметь именно nextFriday()".

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

S>·>Не знаю, я там давно не работаю. Вот презентация какая-то, там цифири какие-то есть. https://www.infoq.com/presentations/lmax-trading-architecture/

S>Ну, ок. Есть, значит. 4ms max, в 8 раз больше 99.99%, в 25 раз больше 99%. Сколько стоит промах мимо кэша nextFriday?
Я уже говорил — сискол и чтение из файла. В low latency path такое недопустимо. Чтение волатильной переменной отличается от обращения к файлу на 3-4 порядка афаир.

S>·>Ни о чём не спорю. Рассказываю, что на спайки более 10ms создавали тикеты и расследовали.

S>Ну, значит нужно строить процесс разработки так, чтобы таких спайков не было. То есть — всё профилировать, бенчмаркать, и вставлять это в приёмочные тесты.
Ну так и было. Но это не даёт гарантию. Нужно ещё и правильно дизайнить всё.

S>·>Сравни это с intrinsic Math.sqrt, который превращается в одну асм-инструкцию или около того, который ты с какого-то бодуна решил инжектить. Вот такая, гы, арифметика.

S>Вот это уже ближе к делу. Не в сигнлтоне дело, выходит.
Синглтонность — это про тестируемость, а не про перформанс.

S>Кстати, инжектить может иметь смысл — потому, что математика математике рознь. И где-то может вместо FSQRT мы будем использовать табличную аппроксимацию .

Ну это экзотика какая-то... чтобы требовалось на ходу менять алгоритм sqrt. Но теоретически да, можно и инжектить.

S>·>Бенчмарки это тесты... они не тестируют неизвестное, а для регрессии по сути.

S>И как вы предлагаете тестировать неизвестное?
Я такого не предлагаю.

S>·>По дизайну со старыми платёжками ходят в другое место. У тебя проблема была в том, что твой static cache оказывает влияние на всё приложение.

S>Нет, это у вас такая проблема, потому что единый composition root.
У нас такой проблемы не было.

S>А у нас такая проблема появится только если мы руками её создадим.

У тебя такую проблему ты сам показал в своём коде со "static _cache". У меня бы спазм пальцев возник при наборе такого кода, а ты с ходу выдал "решение", функциональщик блин. Кстати, и мемоизация — это тоже фактически глобальный кеш и отжор памяти, который надо как-то контролировать.

S>·>Угу. Но это не избавляет от необходимости моков, т.к. от грязного кода практически невозможно избавиться.

S>Моки перестают быть необходимостью. Их с одной стороны подпирают дешёвые юнит-тесты пюрешки, а с другой — полноценные интеграционные тесты взаимодействий.
S>В итоге, ниша для моков сжимается до околонулевой. Потому что они недостаточно дешёвые по сравнению с юнитами, и недостаточно релевантные по сравнению с интеграцией.
Ок. Для проекта класса сложности "хоумпейдж" — верю.

S>·>Ну я и не спорил об этом. Вопрос был в целесообразности. В nextFriday(x) будет пол строчки кода.

S>Наоборот — это в nextFriday() будет полстрочки кода, с вызовом nextFriday(now()).
S>А в nextFriday вся вот эта тяжёлая артиллерия — с загрузкой timeZoneInfo и прочим.
Да пофиг, совершенно неважно. Это будет всё неинтересные внутренности.

S>·>Больше места займёт обвязка сигнатуры самой функции. Насколько целесообразно вводить это всё при условии что цель всё равно иметь везде конкренто nextFridayFromNow и покрывать тестами надо и её тоже.

S>Нет такой цели. Вы её выдумали для оправдания stateful дизайна.
S>Если посмотреть ту самую презентацию LMAX, там чёрным по белому напиcано "time sourced from events". Слайд 34, "System must be deterministic".
S>Господь вас упаси использовать какие-то там instantSource. Добро пожаловать в мир низкой латентности
Был интерфейс TimeSource или типа того. Использовался кастомный, т.к. нужны были long-наносекунды, т.к. милисекундной точности не хватает.
И да, в зависимости от каждого конкретного участка кода, либо инжект TimeSource, либо передача через параметры методов, либо внутри каких-то request-like объектов. Нигде никакого Time.Now(), ясен пень.

S>·>Я ничего не ломал. Возможно я не рассказал очевидные для меня детали: инстант это Instant. Что такое "календарный инстант" — мне неведомо. А "пятница" — это из календаря уже, ясен пень.

S>Дело же не в устройстве АПИ. А в том, что у меня сейчас "следующая пятница" — это через неделю; а если я поменяю таймзону — следующая пятница станет наступать через час. Да, это календарь, а не инстант, но задача формулировалась в терминах календаря.
Следующая пятница это "nextFriday()", а вся кухня с таймзонами и календарями это CalendarLogic, объяснял же уже. Например, в решении с таймером, если будет логика динамически менять таймзону, то будет соответсвующая логика переставить таймер на другой момент времени.

S>Инстант придумали именно вы, причём в рамках решения своих архитектурных задач, на которые пользователям наплевать. Пользователя очень удивит такая штука, что "следующая пятница" вдруг вместо полуночи по его локальным часам показывает 22 часа следующего четверга.

Просто ты, видимо, не понимаешь, что происходит внутри типичного Time.now(). Я ошибся, подумав, что это всем очевидно.

S>·>Для этого я и делаю логику преобразования инстантов в календарные данные в классе CalendarLogic, который зависит от источкика инстантов.

S>При этом кэш у вас об этом ничего не знает. Не-кэшированное вычисление nextFriday берёт текущую тайм-зону; кэш возвращает значение, рассчитанное по тайм-зоне в момент популяции кэша
S>И вы продолжаете утверждать, что у вас нет баги.
Верно, нет бага.


S>·>Неясно как это относится к обсуждаемомому тут источнику системного времени.

S>Это относится к контрактам, на которые вы собираетесь положиться.
Если контракты не проверяются системой типов ЯП, это ещё не значит, что они не выполняются и на них нельзя полагаться.

S>·>Неясно к чему эти все возражения, что видите ли после пятницы теоретически может наступать четверг. Нет, не может на практике, по тому что мы так договорились в нашем коде (т.к. это нахрен не надо с т.з. бизнеса), а не потому что система типов это нам доказала.

S>Ваши договорённости не выражены в коде, и не стоят примерно ничего.
Они выражены в бизнес-требованиях.

S>·>Ну просто под этим флагом вы почему-то проносите какие-то сомнительные практики как ручное написанипе и проверка запросов и т.п.

S>Это были простые примеры на пальцах, призванные помочь вам понять принцип.
Плохой принцип. Я считаю, что лучше иметь автоматические тесты с моками, или даже неспешные тесты с субд, чем полагаться на ручную проверку запросов.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[73]: Что такое Dependency Rejection
От: Sinclair Россия https://github.com/evilguest/
Дата: 07.02.24 03:12
Оценка:
Здравствуйте, ·, Вы писали:

·>Одной строчки в каждом методе контроллеров?

Или в методе, который готовит входы для "методов контроллеров".

·>Вы не можете через integration tests протестировать системные часы.

Могу, но с ограничениями.

·>Pauel поставил задачу сделать кеш. И вы оба не справились.



·>Это говорит о том, что ты не понял код. Ошибки там нет. Следующая пятница календаря — соответствует определённой точке физического времени. Вот в эту точку и ставится таймер для обновления значения в кеше. Поставить таймер на некую "пятница 11:00" теоретически невозможно.

Это значит, что таймер непригоден для решения поставленной задачи. Потому, что пользователю нужна именно "пятница", а не то, куда вам там удобно ставить таймер.
·>Нечего чинить.


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



·>Потому что проблемы бывают именно когда надо менять код по серьёзному, и тут правильно написанные тесты и приносят реальную пользу. А у вас тесты похоже "шоб було, красиво жеж".



·>Это означает, что вы переделываете без тестов вообще. Старые тесты ломаются полностью и новый код пишется с нуля, без проверки всё ещё работает.


S>>Это вы уже задним числом требования меняете. Начиналось всё с nextFriday, который никак не может быть привязан к физическому времени, т.к. в физическом времени никаких пятниц нет, а есть только продолжительности.

·> Физическое время + календарные правила => пятница.
Ну, всё верно. Только таймер обновляет вот это вот "Физическое время + календарные правила" невовремя.

·>А если будет "сортировать по убыванию", то уедет ошибка копипасты "order by LastName asc" и бага обнаружится только в проде по звоку от клиентов.

·>И это самый простейший очевидный случай. Если будет код чуть посложнее, то перепутать < и >, + или - очень даже запросто. Не знаю о других, но я путаю if-else раз в неделю, не реже.

S>>Нет никакой "тестовой базы". СУБД совершенно точно умеет делать order by LastName, поэтому проверять, что она там не вернёт данные в неверном порядке (какие бы они ни были), нам не надо.

·>Надо проверять не то, что order by выполняется базой верно, а то что в бизнес сценарии "вывести клиентов по фамилии" генерит верный список.

S>>А вы уже оценили количество строк, которые вам потребуется вставлять в тестовую таблицу, чтобы получить аналогичную гарантию?

·>Оценка от балды — десяток строк.
Да, с сортировками вы правы, это с фильтрацией там экспонента.

·>Нет, я задаю конкретный вопрос.

Исправление соседнего кода, очевидно, не затронет мой код.
·>Я ссылаюсь на кусок кода с "pattern" который мне показал Pauel. Вопросы к нему какого года этот код.
Ну, пусть ответит.

·>Т.е. при изменении бизнес-логики надо будет менять и тесты. Всё ок. Что не так?

Двойной комплект тестов.

·>Ну как слышится, так и пишется "находим SKU для её renewal" => "Sku find(License lic)". Честно говоря я не очень в теме этой предментой области и не очень понимаю что такое sku, renewal и т.п.

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

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

Микросекунд не будет — там "на дне" вызов SQL-запросов. То есть — поднимаем докерный образ, инициалзируем данные, и т.п.
За микросекунды пролетит тот самый код юнит-тестов, проверяющих чистые функции.

·>А как ты убедишься, что добавленные критерии написаны правильно? Условие "лицензия ещё не проэкспайрилась" у тебя преобразуется в код "licence.date > todayDate" — это правильное условие? Может должно быть "<"? Или вообще "<="? Или "licence.expiryDate"? Всё компилится, но результаты — неверные.

Мы всегда можем вычислить полученный предикат на экземпляре DTO — это же чистая функция
Если мы не доверяем собственной способности корректно написать семантический тест.

·>А покрывается это условие одной строчкой в бд и тремя ассертами, что-то вроде:

·>
·>var licence = TestLicence.withExpiry("2024-06-25");
·>save(licence);
·>assertThat(isExpired(licence, "2024-06-24")).isFalse();
·>assertThat(isExpired(licence, "2024-06-25")).isFalse();
·>assertThat(isExpired(licence, "2024-06-26")).isTrue();
·>

Всё точно так же, только save() не нужен. isExpired — это Expression Tree.
·>Такой код можно хоть заказчиу в чатике написать и спросить — верно ли.
Ну, а SQL — это практически 1:1 английский. Его точно так же можно написать заказчику в чатике и спросить — верно ли.

·>Вау, круто. А толку...



·>Совсем.



·>Вам — нужно. Просто вы не можете, т.к. дизайн слишком хороший.



·>Или не упадёт, т.к. копипаста и данное берётся не из той колонки.

Ну, с тем же успехом у вас копипаста будет и в тесте.

S>>Какой конкретно тип бага вы боитесь упустить?

·>Написанный код не соответствует ожиданиям пользователя.
Это общие слова.

·>Т.е. ваша бизнес-домен — это компилятор. Результат компилятора — код. Тогда логично, тестировать что код порождается верный. Тут возражений нет.

Пользователь не видит кода. Пользователь видит результат вычисления функции, которую он предоставил.
·>Логично. Т.к. наша бизнес-задача — это компилятор, а не квадратный корень.

S>>Вот примерно так же устроена и бизнес-логика поверх RDBMS. Мне важно не то, что там вернёт select person.name from person order by person.name — мне важно, чтобы в моём запросе корректно был выбран критерий сортировки.

·>Ок, если вы пишете библиотеку orm или подобное, то согласен полностью, всё верно делаете.

S>>Посмотрел. Нет глобальных переменных ни там, ни там.

S>>Дальше что?
·>Ты троллишь что-ли?
Нет. Я даже дал ссылку на исходник. Посмотрите — там нет никаких глобальных переменных.
S>>Ближе к делу, меньше лирики.
·>rdtsc
Что rdtsc? Нет в now() никаких rdtsc.

·>В смысле? Задача была поставлена "нужно нефункциональное требование!". И как решение "не шмогли нефункциональные требования, ну и чё, зато вроде работает"? Троллите что-ли?!

Почему же "не шмогли"? Покажите бенчмарк.

S>>А этого и не надо. Зачем? Хватит одной "комбинации". Остальные заведомо работают корректно — проверено дешёвыми юнит-тестами.

·>Нихрена не проверено. Проверено, что "works as coded".


·>Зависит от кода. Если у вас каждый вызов к базовому урлу аппендит разные строчки с именами разных ресурсов, то их надо валидировать как-то, обязательно. Либо по их swagger-like схеме с валидированной версией, либо conformance tests, если схемы нет.

Ну вот "по схеме" — это и есть дешёвые юнит-тесты, которые проверяют соответствие строчки спеке.

S>>А чего тут не понимать? У меня было x=y*z+w. Если каждый из трёх параметров принимает N значений, то мне нужно проверить N^3 сочетаний.

S>>Если я распилю формулу на сложение и умножение, то я смогу протестировать сложение отдельно (N^2 значений), умножение отдельно (N^2 значений).
S>>И ещё 1 (один) тест, который проверяет, что функция AddMul является корректной комбинацией сложения и умножения. Это если я не доверяю своей способности увидеть это глазами.
·>Я не знаю о чём ты тут говоришь. Тут речь шла о твоём конкретном коде с GetItems и тестом со значением данного "2". Ты видимо лажанулся, сейчас ужом извиваешься подменяя тему.
Для GetItems — да, всё упирается в тестовые данные. Как я и говорил — в отдельных изолированных случаях поведение грязных функций с моками эквивалентно поведению чистых функций с таблицами истинности.
В более сложных я могу декомпозировать задачу, а с ней и тесты.

·>Да. Бизнес требует наличия PTP на прод серверах и монотонный rtc источник.

Хм. А в презентации — event time sourcing.

·>Ты видимо дальше носа не видишь. Загляни внутро now(). Там несколько синглтонов — локаль, календарь, таймзона, источник времени.

Я заглянул, даже ссылку дал
·>В этом и был мой поинт — нельзя использовать Now в коде логики, который синглтон синглтонами погоняющий. А надо явно испоьзовать DI и инжектить источник времени в composition root.
Первая часть у вас корректна. Вторая — нет. Зачем нам "источник времени", который ещё и не даёт информации о локали, календаре, и тайм-зоне?
Нам нужно отдавать в БЛ момент времени, относительно которого она будет рассчитываться.

·>Т.е. ты корректный код написать не в состоянии?..

Я — в состоянии, но сейчас ваша очередь

·>Как игрушечный пример для обсуждения на форуме.

Ну, тогда и игрушечные ответы подходят.

·>Я уже говорил — сискол и чтение из файла.

Из какого файла?

S>>Ну, значит нужно строить процесс разработки так, чтобы таких спайков не было. То есть — всё профилировать, бенчмаркать, и вставлять это в приёмочные тесты.

·>Ну так и было. Но это не даёт гарантию. Нужно ещё и правильно дизайнить всё.

S>>·>Сравни это с intrinsic Math.sqrt, который превращается в одну асм-инструкцию или около того, который ты с какого-то бодуна решил инжектить. Вот такая, гы, арифметика.

S>>Вот это уже ближе к делу. Не в сигнлтоне дело, выходит.
·>Синглтонность — это про тестируемость, а не про перформанс.

S>>Кстати, инжектить может иметь смысл — потому, что математика математике рознь. И где-то может вместо FSQRT мы будем использовать табличную аппроксимацию .

·>Ну это экзотика какая-то... чтобы требовалось на ходу менять алгоритм sqrt. Но теоретически да, можно и инжектить.
Ну, вы же не собираетесь на ходу менять алгоритм instantSource, который там у вас rdtsc вызывает. Значит — и его можно не инжектить

·>Я такого не предлагаю.

Тогда и говорить не о чем.

·>У нас такой проблемы не было.



S>>А у нас такая проблема появится только если мы руками её создадим.

·>У тебя такую проблему ты сам показал в своём коде со "static _cache". У меня бы спазм пальцев возник при наборе такого кода, а ты с ходу выдал "решение", функциональщик блин. Кстати, и мемоизация — это тоже фактически глобальный кеш и отжор памяти, который надо как-то контролировать.
В разных платформах — по-разному. Где-то это глобальный кэш, где-то — локальный. Можно и контролировать — обычно помимо примитивной memoize есть более подробные версии, с рукоятками по макс. размеру кэша и политике инвалидации.
·>Ок. Для проекта класса сложности "хоумпейдж" — верю.
Хорошо, остановимся на проектах класса сложности "хоумпейдж"

·>Да пофиг, совершенно неважно. Это будет всё неинтересные внутренности.

Ну, ок.

S>>Если посмотреть ту самую презентацию LMAX, там чёрным по белому напиcано "time sourced from events". Слайд 34, "System must be deterministic".

S>>Господь вас упаси использовать какие-то там instantSource. Добро пожаловать в мир низкой латентности
·>Был интерфейс TimeSource или типа того. Использовался кастомный, т.к. нужны были long-наносекунды, т.к. милисекундной точности не хватает.
·>И да, в зависимости от каждого конкретного участка кода, либо инжект TimeSource, либо передача через параметры методов, либо внутри каких-то request-like объектов. Нигде никакого Time.Now(), ясен пень.
Непонятно, зачем вам инжект TimeSource в коде, который обязан брать время из event. Вместе со всеми настройками календаря и прочим — если event был по летнему времени, то и преобразование его в календарную дату тоже будет по летнему времени, даже если в момент его обработки время сменилось на зимнее.

·>Следующая пятница это "nextFriday()", а вся кухня с таймзонами и календарями это CalendarLogic, объяснял же уже. Например, в решении с таймером, если будет логика динамически менять таймзону, то будет соответсвующая логика переставить таймер на другой момент времени.

Логика смены таймзоны уже реализована за вас, на уровне ОС. Вам остаётся только к ней адаптироваться.

·>Просто ты, видимо, не понимаешь, что происходит внутри типичного Time.now(). Я ошибся, подумав, что это всем очевидно.

Наверное, не понимаю. В моём воображении Time.now() берёт Utc-шное время из операционной системы (https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemtimeasfiletime), а потом прибавляет к нему текущее смещение текущей таймзоны. Как вы собрались заменить это на instant — интересная загадка.
Как вы собираетесь "переставлять таймер" — тоже загадка. Я вот сходу не вспомнил надёжного способа задетектить изменение тайм зоны даже для винды; тем более — кроссплатформенным способом.

·>Верно, нет бага.

Угу. Только nextFriday иногда возвращает 23:00 следующего четверга, а так — бага нету. Удачи, чо.

·>Если контракты не проверяются системой типов ЯП, это ещё не значит, что они не выполняются и на них нельзя полагаться.

Хм. В целом я согласен. Но тогда у нас должен быть какой-то другой способ проверить выполнение контрактов, не так ли?

·>Они выражены в бизнес-требованиях.

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

·>Плохой принцип. Я считаю, что лучше иметь автоматические тесты с моками, или даже неспешные тесты с субд, чем полагаться на ручную проверку запросов.

Ну, ок.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[74]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 07.02.24 12:16
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>·>Одной строчки в каждом методе контроллеров?

S>Или в методе, который готовит входы для "методов контроллеров".
Наверное сойдёт для простых случаев, когда такой метод только один и не надо париться о перформансе.

S>·>Вы не можете через integration tests протестировать системные часы.

S>Могу, но с ограничениями.
Как? Невозможно будет отличить время взятое сервером из своего источника от времени пришедшее в запросе от клиента.

S>·> Физическое время + календарные правила => пятница.

S>Ну, всё верно. Только таймер обновляет вот это вот "Физическое время + календарные правила" невовремя.
Бред.

S>>>А вы уже оценили количество строк, которые вам потребуется вставлять в тестовую таблицу, чтобы получить аналогичную гарантию?

S>·>Оценка от балды — десяток строк.
S>Да, с сортировками вы правы, это с фильтрацией там экспонента.
Имхо, то же самое. Просто одни и те же данные можно запрашивать разными фильтрами. Я уже примерный код показывал где-то выше.

S>·>Нет, я задаю конкретный вопрос.

S>Исправление соседнего кода, очевидно, не затронет мой код.
Поля могли немного переместиться, переделаться, они стали работать несколько иначе. Что может повлиять на логику фильтров.

S>·>Т.е. при изменении бизнес-логики надо будет менять и тесты. Всё ок. Что не так?

S>Двойной комплект тестов.
Разные тесты тестируют разные вещи. Первый комплект — бизнес-логика, второй комплект — загрузка/выгрузка данных в бд.

S>·>Ну как слышится, так и пишется "находим SKU для её renewal" => "Sku find(License lic)". Честно говоря я не очень в теме этой предментой области и не очень понимаю что такое sku, renewal и т.п.

S>Не. Sku find(License lic) — это и есть метод бизнес-логики. Внутри там код с множеством ветвлений, потому что способов найти замену одной лицензии на другую — много, правила запутанные и частично противоречивые.
Т.е. у тебя бизнес-логика не отделима от персистенса? Ну хз, может это какой-то особенный проект.

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

S>Микросекунд не будет — там "на дне" вызов SQL-запросов. То есть — поднимаем докерный образ, инициалзируем данные, и т.п.
S>За микросекунды пролетит тот самый код юнит-тестов, проверяющих чистые функции.
За микросекунды летают десятки тестов для ветвистой бизнес-логики. А докер-образ это для тестирования персистенса данных в субд.

S>·>А как ты убедишься, что добавленные критерии написаны правильно? Условие "лицензия ещё не проэкспайрилась" у тебя преобразуется в код "licence.date > todayDate" — это правильное условие? Может должно быть "<"? Или вообще "<="? Или "licence.expiryDate"? Всё компилится, но результаты — неверные.

S>Мы всегда можем вычислить полученный предикат на экземпляре DTO — это же чистая функция
Ну это и есть фактически тестирование на "in-memory" бд. Если это делаете, то спору нет.

S>Если мы не доверяем собственной способности корректно написать семантический тест.

Неужели никогда + и — или подобное не путаете?

S>Всё точно так же, только save() не нужен. isExpired — это Expression Tree.

save нужен для того, чтобы убедиться создаваемая лицензия при записи даты экспирации кладёт всё в нужную колонку бд, в ту же самую, которая потом при чтении в isExpired матчится.
Если вы это не тестируете, то .

S>·>Такой код можно хоть заказчиу в чатике написать и спросить — верно ли.

S>Ну, а SQL — это практически 1:1 английский. Его точно так же можно написать заказчику в чатике и спросить — верно ли.
Только что был expression tree, теперь sql. Если sql генерится из ET, то он может быть довольно хитрым и навороченным, с кучей лишних деталей. Код теста же затачивается под один бизнес-аспект, технические детали реализации запрятаны.

S>·>Или не упадёт, т.к. копипаста и данное берётся не из той колонки.

S>Ну, с тем же успехом у вас копипаста будет и в тесте.
Её там гораздо проще заметить.

S>>>Какой конкретно тип бага вы боитесь упустить?

S>·>Написанный код не соответствует ожиданиям пользователя.
S>Это общие слова.
Лично я не умею выполнять в уме sql запросы. Увидеть ошибку в тексте запроса или даже ET — мне гораздо сложнее, чем на конкретном тестовом примере, показывающем, например, что такой-то конкретный фильтр выбирает эту запись, но не ту. Может это только мои заморочки, конечно...

S>·>Т.е. ваша бизнес-домен — это компилятор. Результат компилятора — код. Тогда логично, тестировать что код порождается верный. Тут возражений нет.

S>Пользователь не видит кода. Пользователь видит результат вычисления функции, которую он предоставил.
Компилятор порождает код для машины, а не для пользователя.

S>>>Посмотрел. Нет глобальных переменных ни там, ни там.

S>>>Дальше что?
S>·>Ты троллишь что-ли?
S>Нет. Я даже дал ссылку на исходник. Посмотрите — там нет никаких глобальных переменных.
В строке 1083 идёт чтение из глобальной переменной TSC или аналога.

S>·>rdtsc

S>Что rdtsc? Нет в now() никаких rdtsc.


S>·>В смысле? Задача была поставлена "нужно нефункциональное требование!". И как решение "не шмогли нефункциональные требования, ну и чё, зато вроде работает"? Троллите что-ли?!

S>Почему же "не шмогли"? Покажите бенчмарк.
Я рассказал. Бенчмарком заморачиваться не буду.

S>·>Зависит от кода. Если у вас каждый вызов к базовому урлу аппендит разные строчки с именами разных ресурсов, то их надо валидировать как-то, обязательно. Либо по их swagger-like схеме с валидированной версией, либо conformance tests, если схемы нет.

S>Ну вот "по схеме" — это и есть дешёвые юнит-тесты, которые проверяют соответствие строчки спеке.
Угу. Если спека есть. И есть возможность верифицировать, что базовый урл работает с той же спекой.

S>·>Я не знаю о чём ты тут говоришь. Тут речь шла о твоём конкретном коде с GetItems и тестом со значением данного "2". Ты видимо лажанулся, сейчас ужом извиваешься подменяя тему.

S>Для GetItems — да, всё упирается в тестовые данные. Как я и говорил — в отдельных изолированных случаях поведение грязных функций с моками эквивалентно поведению чистых функций с таблицами истинности.
Я знаю, я об этом говорил ещё ~2 месяца назад, когда Pauel свои таблицы истинности потерял.

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

И моки тут непричём.

S>·>Да. Бизнес требует наличия PTP на прод серверах и монотонный rtc источник.

S>Хм. А в презентации — event time sourcing.
А думаешь откуда в events берётся time? От святого духа?

S>·>Ты видимо дальше носа не видишь. Загляни внутро now(). Там несколько синглтонов — локаль, календарь, таймзона, источник времени.

S>Я заглянул, даже ссылку дал
Смотришь в книгу, видишь...

S>·>В этом и был мой поинт — нельзя использовать Now в коде логики, который синглтон синглтонами погоняющий. А надо явно испоьзовать DI и инжектить источник времени в composition root.

S>Первая часть у вас корректна. Вторая — нет. Зачем нам "источник времени", который ещё и не даёт информации о локали, календаре, и тайм-зоне?
S>Нам нужно отдавать в БЛ момент времени, относительно которого она будет рассчитываться.
У момента времени нет локали, календаря и тайм-зоны. Это просто число. Обычно unix epoch.

S>·>Т.е. ты корректный код написать не в состоянии?..

S>Я — в состоянии, но сейчас ваша очередь
Я уже давно написал, тебе осталось разобраться.

S>·>Как игрушечный пример для обсуждения на форуме.

S>Ну, тогда и игрушечные ответы подходят.
Ок, но сломанные игрушки — no fun.

S>·>Я уже говорил — сискол и чтение из файла.

S>Из какого файла?
"Скажем, обращение к ZoneInfo в jdk может порождать чтение ресурсов с информацией о таймзонах, т.е. чтение файла, т.е. сискол, блокировку и т.п."

S>>>Кстати, инжектить может иметь смысл — потому, что математика математике рознь. И где-то может вместо FSQRT мы будем использовать табличную аппроксимацию .

S>·>Ну это экзотика какая-то... чтобы требовалось на ходу менять алгоритм sqrt. Но теоретически да, можно и инжектить.
S>Ну, вы же не собираетесь на ходу менять алгоритм instantSource, который там у вас rdtsc вызывает. Значит — и его можно не инжектить
Для replay и для тестов — надо. Менять алгоритм извлечения квадратного корня мне не приходилось.

S>>>А у нас такая проблема появится только если мы руками её создадим.

S>·>У тебя такую проблему ты сам показал в своём коде со "static _cache". У меня бы спазм пальцев возник при наборе такого кода, а ты с ходу выдал "решение", функциональщик блин. Кстати, и мемоизация — это тоже фактически глобальный кеш и отжор памяти, который надо как-то контролировать.
S>В разных платформах — по-разному. Где-то это глобальный кэш, где-то — локальный. Можно и контролировать — обычно помимо примитивной memoize есть более подробные версии, с рукоятками по макс. размеру кэша и политике инвалидации.
Вот только это уже не будет так всё просто как обещано — "пишем всю интеграцию в контроллерах".

S>·>Ок. Для проекта класса сложности "хоумпейдж" — верю.

S>Хорошо, остановимся на проектах класса сложности "хоумпейдж"
Ну там всё работает, хоть левой пяткой пиши, всё равно.

S>·>Был интерфейс TimeSource или типа того. Использовался кастомный, т.к. нужны были long-наносекунды, т.к. милисекундной точности не хватает.

S>·>И да, в зависимости от каждого конкретного участка кода, либо инжект TimeSource, либо передача через параметры методов, либо внутри каких-то request-like объектов. Нигде никакого Time.Now(), ясен пень.
S>Непонятно, зачем вам инжект TimeSource в коде, который обязан брать время из event.

Потому что если это не хоумпейдж, то кода много, большого и разного. Поток ордеров да, он евент, притом генерится из центрального ll сервиса. К этому всему нужны ещё несколько десятков сервисов со своей логикой, со своими реквестами и очередями.

S>Вместе со всеми настройками календаря и прочим — если event был по летнему времени, то и преобразование его в календарную дату тоже будет по летнему времени, даже если в момент его обработки время сменилось на зимнее.

Бред.

S>·>Следующая пятница это "nextFriday()", а вся кухня с таймзонами и календарями это CalendarLogic, объяснял же уже. Например, в решении с таймером, если будет логика динамически менять таймзону, то будет соответсвующая логика переставить таймер на другой момент времени.

S>Логика смены таймзоны уже реализована за вас, на уровне ОС. Вам остаётся только к ней адаптироваться.
ОС — это для десктопных приложений. Серверное приложение имеет свои таймзоны, в зависимости куда ходит. Я же рассказывал, например, что разные коннекшны к разным бирзам имеют разные зоны.
А в процессе разработки адаптироваться к ОС это вообще грабля. Код работающий у девелопера в Лондоне, падает у девелопера в Сиднее.

S>·>Просто ты, видимо, не понимаешь, что происходит внутри типичного Time.now(). Я ошибся, подумав, что это всем очевидно.

S>Наверное, не понимаю. В моём воображении Time.now() берёт Utc-шное время из операционной системы (https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemtimeasfiletime), а потом прибавляет к нему текущее смещение текущей таймзоны. Как вы собрались заменить это на instant — интересная загадка.
Я же написал, не использовать Time.now() с его синглтонами, а напрямую использовать источники времени и таймзоны.

S>Как вы собираетесь "переставлять таймер" — тоже загадка. Я вот сходу не вспомнил надёжного способа задетектить изменение тайм зоны даже для винды; тем более — кроссплатформенным способом.

Что значит "задедектить"? В админке меняем таймзону в настройках биржи и сообщение отправляется соответсвующему сервису, который отменяет таймер и ставит новый.

S>·>Верно, нет бага.

S> Угу. Только nextFriday иногда возвращает 23:00 следующего четверга, а так — бага нету. Удачи, чо.
nextFriday возващает LocalDate, в котором никакого времени нет и быть не должно. Это ты с колокольни корявейшего time api из дотнета и винды говоришь, видимо.

S>·>Если контракты не проверяются системой типов ЯП, это ещё не значит, что они не выполняются и на них нельзя полагаться.

S>Хм. В целом я согласен. Но тогда у нас должен быть какой-то другой способ проверить выполнение контрактов, не так ли?
Ну да.

S>·>Они выражены в бизнес-требованиях.

S>Ну, да. Выражены. Я вот в бизнес-требованиях не вижу никаких упоминаний того, что пятница обязательно идёт после четверга, и никогда не бывает наоборот.
S>А мой жизненный опыт показывает, что это не то что бы сплошь и рядом происходит, но встречается достаточно часто для того, чтобы считать это штатной ситуацией.
Потому что ты календарное время путаешь и физическое.

S>А если у нас будут такие бизнес-требования, то это будет не то, на что мы будем полагаться, а то, что мы должны обеспечить. А если обеспечиваем не мы — то убедиться, что всё сделано так, как требуется.

Обеспечиваем соответствующим железом и настройкой PTP.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[67]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 15.02.24 18:53
Оценка:
Здравствуйте, ·, Вы писали:

S>>Правильный способ отрезания входов-выходов — ровно такой, как настаивает коллега Pauel. Потому что в вашем способе вы их никуда не отрезаете — вы их заменяете плохими аналогами. Ващ подход мы, как и все на рынке, применяли в бою. И неоднократно обожглись о то, что мок не мокает настоящее решение; он мокает ожидания разработчика об этом решении.

·>Ожидания разработчика берутся не из пустого места, а из фиксации поведения боевой системы. Ну по крайней мере если использовать моки правильно, а не так как Pauel думает их используют.

Вы сейчас в чистом виде топите за человеческий фактор. Вроде бы все пишут с оглядкой на эту боевую систему, но вот почему то результат слишком сильно коррелирует с квалификацией. И это большая проблема.
Задача инженера как раз исключать человеческий фактор, а не вводить его
Re[69]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 15.02.24 19:15
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Пример приводил здесь do_logic.

Б>http://rsdn.org/forum/design/8648665.1
Автор: Буравчик
Дата: 10.12.23


Б>Алгоритмы (доменная логика) уже вынесена в чистые функции, и это действительно банально.

Б>Но осталось связать эту логику с данными из внешних систем. Ведь где-то при обработке запроса это должно происходить. Кстати, где?

Б>Полагаю, что работа с внешними зависимостями будут закрыты функциями, которые будут переданы в функцию do_logic. Аналогично для моков зависимости будут закрыты интерфейсом (группа функций), и он будет передан в do_logic. Т.е. с точки зрения тестирования разницы нет — в обоих случаях подменяется поведение внешней системы — передается функция возвращающая нужное для теста значение.


Разница в том, что именно будете мокать. Или низкоуровневые репозитории, или высокоуровневые абстракции.

В вашем случае нужны и интеграционные, и моки. Если беретесь за моки, нужно сделать их максимально надежными и наименее дешевыми. И для этого стоит уйти от моков низкоуровневых зависимостей типа "репозиторий"
Re[67]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 15.02.24 19:38
Оценка:
Здравствуйте, ·, Вы писали:

S>>·>Вот это я не понял. У него в примере тест буквально сравнивал текст sql "select * ...". Как такое поймает это?

S>>Это он просто переводит с рабочего на понятный. Он же пытался вам объяснить, что проверяет структурную эквивалентность. Тут важно понимать, какие компоненты у нас в пайплайне.
·>При этом он меня обвинил в том, что мои тесты проверяют как написано, а не ожидания пользователей.

Я вам привожу общеизвестный факт — моки всех сортов обладают таким вот свойством. И вы показали почему то именно такие тесты, смотрите сами, что пишете.

> А бревно в глазу не замечаете — пользователям-то похрен какая-то там эквивалентность у вас, структурная или не очень.


И точно так же похрен на ваши классы и интерфейсы. Но вы их не стесняетесь использовать, а структурная эквивалентность это ужос-ужос.

S>>Если вы строите текст SQL руками — ну да, тогда будет именно сравнение текстов. Но в наше время так делать неэффективно. Обычно мы порождаем не текст SQL, а его AST. Генерацией текста по AST занимается отдельный компонент (чистая функция), и он покрыт своими тестами — нам их писать не надо.

·>И накой сдалась эта ваша AST вашим пользователям? А что если завтра понадобится заменить вашу AST на очередной TLA, то придётся переписывать все тесты, рискуя всё сломать.

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

Я взял пример для иллюстрации подхода. В другом месте вместо sql будет json-like структура, которая отлично сравнивается и визуализируется — например, при запросах по http или orm.
Вот сегодня сделал тож самое для запросов к http rpc сервису — параметры, retry, api-key, итд. Теперь в тестах сразу видно, что именно уходит на сервис.
Re[69]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 15.02.24 19:44
Оценка:
Здравствуйте, ·, Вы писали:

·>composition root — это технический, "подвальный" код. Конкретно инжекция системного источника времени — одна строчка кода, которая за жизнь проекта трогается практически ни разу. И тестируется относительно просто — приложение скомпилировалось, стартовало, даёт зелёный healthcheck — значит работает.

·>У тебя же now() лежит в коде бизнес-логики (как я понял из объяснений Pauel). Будет стоять в каждом втором методе контроллеров, т.е. в сотнях мест и код этот постоянно меняется.

У вас будет как минимум несколько этих composition root, т.к. юзкейсы разные. А следовательно будут ошибки вида "посмотрел не туда"

·>Ну как видишь эта самая фунция бизнес-логики состоит из меньших шагов бизнес логики с кучей "если-то". И это всё бизнес-логика. Вот и бей на части.

·>И где же тут эта ваша структурная эквивалентность? Где метапрограммирование? Где буквальный текст sql? И имя функции fn1?

Вы почему в моем примере видите только сравнение текста sql, и дальше этого смотреть не хотите. SQL потому, что фильтры сделаны именно так. Соответственно, можно подкинуть дешовых тестов используя структурную эквивалентность.

Соответсвенно, фиксируем тестами свои ожидания, что бы будущие изменения в коде не сломали всё подряд
Re[70]: Что такое Dependency Rejection
От: Буравчик Россия  
Дата: 15.02.24 20:45
Оценка: +1
Здравствуйте, Pauel, Вы писали:

P>Разница в том, что именно будете мокать. Или низкоуровневые репозитории, или высокоуровневые абстракции.


Какая-то вода. Почему ты называешь репозитории низкоуровневыми? Они вполне себе работают с объектами бизнес-логики, и достаточно высокоуровневые.
Разъясни, что ты называешь высокоуровневыми абстракциями.

P>В вашем случае нужны и интеграционные, и моки. Если беретесь за моки, нужно сделать их максимально надежными и наименее дешевыми. И для этого стоит уйти от моков низкоуровневых зависимостей типа "репозиторий"


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

При этом подход с моками не исключает разбивку по функциям. Можно код переразбивать как хочешь и рефакторить, в том числе выделяя бизнес-логику в чистые функции, — тесты от этого не меняются. В отличие от искусственного разбинения на маленькие вспомогательнные функции и их отдельного тестирования.
Best regards, Буравчик
Re[68]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 16.02.24 00:06
Оценка:
Здравствуйте, Pauel, Вы писали:

P>·>При этом он меня обвинил в том, что мои тесты проверяют как написано, а не ожидания пользователей.

P>Я вам привожу общеизвестный факт — моки всех сортов обладают таким вот свойством. И вы показали почему то именно такие тесты, смотрите сами, что пишете.
Давай определимся. В том образцовом коде у Фаулера — моки есть?

>> А бревно в глазу не замечаете — пользователям-то похрен какая-то там эквивалентность у вас, структурная или не очень.

P>И точно так же похрен на ваши классы и интерфейсы. Но вы их не стесняетесь использовать, а структурная эквивалентность это ужос-ужос.
Так я это и говорю, похрен на эквивалентность, и на классы, интерфейсы тоже похрен. Важно чтобы код выражал ожидания пользователя, а не что =="select *". Пользователь может ожидать, что "для такого фильтра вернуть такой-то список штуковин". А у тебя "для такого фильтра верунть такой sql-код и использовать такие-то приватные методы".

P>·>И накой сдалась эта ваша AST вашим пользователям? А что если завтра понадобится заменить вашу AST на очередной TLA, то придётся переписывать все тесты, рискуя всё сломать.

P>А ифы, циклы, классы, экземпляры зачем вашим пользователям? А что если понадобится поменять один класс-интерфейс на другой?
Ровно та же проблема, если понадобится поменять твою структуру на другую.

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

Это ты опять что-то перефантазировал.

P>Я взял пример для иллюстрации подхода. В другом месте вместо sql будет json-like структура, которая отлично сравнивается и визуализируется — например, при запросах по http или orm.

P>Вот сегодня сделал тож самое для запросов к http rpc сервису — параметры, retry, api-key, итд. Теперь в тестах сразу видно, что именно уходит на сервис.
А толку? Ну уходит, а с чего ты решил, что сервис будет с этим ушедшим работать? Будешь тесты в проде гонять?

P>·>У тебя же now() лежит в коде бизнес-логики (как я понял из объяснений Pauel). Будет стоять в каждом втором методе контроллеров, т.е. в сотнях мест и код этот постоянно меняется.

P>У вас будет как минимум несколько этих composition root, т.к. юзкейсы разные. А следовательно будут ошибки вида "посмотрел не туда"
Эти же кусочки composition root будут использоваться и в тестах этих юзкейсов. От тестов моками отделяются "плохие" зависимости. Т.е. выделение "плохих" зависимостей — чисто техническое — медленное (сеть-диск-етс), неконтролируемое (системные штуки типа clock) и т.п. А юзкейсы — это логика, этот же код что в проде, что в тестах — один и тот же.

P>·>Ну как видишь эта самая фунция бизнес-логики состоит из меньших шагов бизнес логики с кучей "если-то". И это всё бизнес-логика. Вот и бей на части.

P>·>И где же тут эта ваша структурная эквивалентность? Где метапрограммирование? Где буквальный текст sql? И имя функции fn1?
P>Вы почему в моем примере видите только сравнение текста sql, и дальше этого смотреть не хотите.
Я вижу ровно то, что ты мне показал. Фантазии и телепатия — не моя стезя.

P>SQL потому, что фильтры сделаны именно так.

Именно — и это отстой. Тестируете детали как сделано, что works as coded.

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

Это плохой критерий для выбора тестов.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[69]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 16.02.24 06:36
Оценка:
Здравствуйте, ·, Вы писали:

P>>Я вам привожу общеизвестный факт — моки всех сортов обладают таким вот свойством. И вы показали почему то именно такие тесты, смотрите сами, что пишете.

·>Давай определимся. В том образцовом коде у Фаулера — моки есть?

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

>>> А бревно в глазу не замечаете — пользователям-то похрен какая-то там эквивалентность у вас, структурная или не очень.

P>>И точно так же похрен на ваши классы и интерфейсы. Но вы их не стесняетесь использовать, а структурная эквивалентность это ужос-ужос.
·>Так я это и говорю, похрен на эквивалентность, и на классы, интерфейсы тоже похрен. Важно чтобы код выражал ожидания пользователя, а не что =="select *". Пользователь может ожидать, что "для такого фильтра вернуть такой-то список штуковин". А у тебя "для такого фильтра верунть такой sql-код и использовать такие-то приватные методы".

Похоже, объяснения Синклера и мои про data complexity вам не помогли. Нету у вас варианта покрыть фильтры тестами на данных. Ради каждого теста, условно, вам надо не 2-3 элемента забрасывать в БД, а кучу данных во все таблицы которые учавствуют прямо или косвенно.
Отсюда ясно, что мелкие тесты фильтров вы будете гонять на бд близкой к боевой. Ну или игнорировать такую проблему делая вид что "и так сойдет"

P>>А ифы, циклы, классы, экземпляры зачем вашим пользователям? А что если понадобится поменять один класс-интерфейс на другой?

·>Ровно та же проблема, если понадобится поменять твою структуру на другую.

Вы снова игнорируете аргументы — глубина и частота правок зависят от количества зависимостей, которое у вас получается конским. У меня же простой маппер. Его править придется только в том случае, если поменяется БЛ. У вас — любое изменение дизайна, например, ради оптимизации.

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

·>Это ты опять что-то перефантазировал.

Смотрите сами:

P>Я вам сказал, что при переходе от одного дизайна к другому моки отваливаются. Если вы мокнули репозиторий в одном варианте, а в другом варианте репозиторий вызывается не там, не так, не тогда — всё, приплыли.
Голословно заявил, да.


P>>Я взял пример для иллюстрации подхода. В другом месте вместо sql будет json-like структура, которая отлично сравнивается и визуализируется — например, при запросах по http или orm.

P>>Вот сегодня сделал тож самое для запросов к http rpc сервису — параметры, retry, api-key, итд. Теперь в тестах сразу видно, что именно уходит на сервис.
·>А толку? Ну уходит, а с чего ты решил, что сервис будет с этим ушедшим работать? Будешь тесты в проде гонять?

1 Задача "работает ли сервис с тем и этим" решается интеграционными тестами.
2 Задача "создаём только такие запросы, которых понятны тому сервису. Это решается простыми дешовыми юнит-тестами.

Это две разные задачи. Вторую можно игнорировать, переложить на интеграционные, если у вас один единственный вариант запроса. А если у вас сложное вычисление — то интеграционными вы вспотеете всё это тестировать. И моки вам точно так же не помогут.

P>>SQL потому, что фильтры сделаны именно так.

·>Именно — и это отстой. Тестируете детали как сделано, что works as coded.

Тестируется вычисление фильтров, которое довольно сложное.

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

·>Это плохой критерий для выбора тестов.

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