Сообщение Re[18]: Что такое Dependency Rejection от 10.12.2023 13:41
Изменено 10.12.2023 13:53 ·
Re[18]: Что такое Dependency Rejection
Здравствуйте, Pauel, Вы писали:
P>>>Например, нам нужно считать токен для стрима, нас интересует ид класса, ид сущность, версия сущность b секрет.
P>·>Не очень ясно в чём идея. У тебя есть библотека которая умеет несколько алгоритмов, апдейт по частям с приведением типов, формирование дайджеста в разных форматах. И ты это оборачиваешь в свою обёртку, которая умеет только одно — конкретный алгоритм упакованный в отдельный метод Token.from, который содержит конкретные параметры и т.п. И что в этом такого особенного? Называется частичное применение функции. И причём тут моки?
P>Это я вам пример привел, для чего вообще люди мокают, когда всё прекрасно тестируется и без моков.
Т.е. проблема не в моках как таковых, а в том, что некоторые люди используют некоторые подходы и пишут корявый код? Это проблема ликбеза, а не моков. Ровно та же проблема есть в любых подходах тестирования.
P>·>Проблема такого подхода в том, что это работает только в тривиальных случаях. Когда в твоём приложении понадобятся много разных комбинаций, то у тебя количество частично применённых функций пойдёт по экспоненте. У тебя получатся ApiTokenAsHex.from, ApiTokenAsBytes.from, ApiTokenAsB64.from, HashedPassword.from, HashedPasswordAsBytes.from и пошло поехало.
P>А зачем вам эти AsHex, AsBytes? Это же просто преставление. У вас есть токен — это значение. Его можно создать парой-тройкой способов, и сохранить так же.
Именно. 3 способа создания x 3 способа представления — уже девять комбинаций. У тебя будет либо девять функций, либо своя обёртка над сторонним api, изоморфная по сложности тому же api.
P> Отсюда ясно, что нет нужды плодить те функции, что вы предложили, а следовательно и тестировать их тоже не нужно.
Нужда идёт из кода, из требований. Если у тебя достаточно сложная система, у тебя будет там все 9 комбинаций, а то и больше.
P>Всех дел — отделить данные от представления, и количество тестов уменьшается.
Количество может быть уменьшено, только если ты какое-то требование перестал тестировать.
P>>>Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.
P>·>Т.е. вынесли кусочек кода в метод и его протестировали отдельно. И? Моки тут причём?
P>Насколько я понимаю, я вам показываю, как разные люди понимают моки. Вы же всё приписываете лично мне.
Что лично тебе я приписал?
P>·>Но если у тебя оно нужно конкретно в одном месте для etag, то это уже оверкилл.
P>Вас всё тянет любое высказывание к абсурду свести. Естетсвенно, что городить вагон абстракций, ради того, что бы вычислить етаг не нужно.
А ты таки предложил новую абстракцию ввести в виде ApiToken.from.
P>При этом и моки тоже не нужны для этого случая.
P>У нас будет функция getEtag(content) которую можно проверить разными значениями.
Именно. Зачем нам тогда нужен ApiToken.from?
P>Тем не менее, пример на моках что я вам показал, он именно отсюда взят.
Пример на то и пример, чтобы показать как использовать моки. А в твоём примере было показано как _не надо_ писать код. Я показал как надо писать код при использовании моков. И мой код был вполне вменяемым. Оправданы ли они в данном случае моки — вопрос другой.
P>>>ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.
P>·>Оборачивать каждую стороннюю либу — т.к. её _возможно_ нужно будет заменить — это оверинжиниринг в большинстве случаев.
P>При чем здесь оборачивание? У вас есть функционал — верификация jwt токена.
P>Объясните внятно, почему замена jsonwebtoken на jose должна сломать тесты?
P>А вот если вы замокали jsonwebtoken, то ваши тесты сломаются. И причина только в том, что вы замокали там где этого не надо делать.
В примере никакого jwt токена не было. Я не знаю ты о чём, у тебя где-то в голове какая-то более широкая картина, но к сожалению, я не очень хороший телепат.
P>>>а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)
P>·>А какая разница? Ведь эту конструкцию надо будет тоже каким-то тестом покрыть.
P>Для этого есть интеграционный тест
Жуть. Т.е. ты не можешь что-то протестировать быстрыми тестами, приходится плодить медленные интеграционные и тривиальные ошибки обнаруживать только после запуска всего приложения. Спасибо, не хочу.
P>>>Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.
P>·>То что ты логику в коде куда-то переносишь, не означает, что её вдруг можно не тестировать.
P>Там где логика — там юнит-тесты, без моков, а там где интеграция — там интеграционные тесты, тоже без моков.
Слишком тяжелый подход. Только на мелких приложениях работает.
P>>>Вот-вот, еще чтото надо куда то инжектить, вызывать, проверять, а вызвалось ли замоканое нужное количество раз, в нужной последовательности, итд итд итд
P>·>Это надо не для моков как таковых (если validateThing — тривиальна, то зачем её вообще мочить? Всегда можно запихать реальную имплементацию), а для разделения компонент и независимого их тестирования.
P>А задизайнить компоненты, что бы они не зависели друг от друга?
Это как? То что зависимости неявные — это не означает, что их нет.
P>·>Я тебе уже написал когда это _надо_, но ты как-то тихо проигнорировал: Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.
P>Это ваш выбор дизайна — на самом низу стека вы вдруг вызываете чтение из сети-базы-итд, или начинаете жечь cpu.
P>А если этого не делать, то и моки не нужны.
Ага-ага. Но тогда нужны ещё более медленные и неуклюжие интеграционные тесты, много.
P>>>Итого — вы прибили тесты к реализации
P>·>В js принято валить всё в Global и сплошные синглтоны? DI не слышали?
P>Зачем global? DI можно по разному использовать — речь же об этом. Посмотрите framework as a detail или clean architecture, сразу станет понятнее.
require это нифига не DI, это SL.
P>Вместо выстраивания глубокой иерархии, где на самом низу стека идет чтение из бд, делаем иначе — вычисляем, что читать из бд, следующим шагом выполняем чтение.
P>Итого — самое важное тестируется плотно юнит-тестами, а остальное — интеграционными
P>С вашими моками покрытие будет хуже, кода больше, а интеграционные всё равно нужны
Интеграционные тесты тестируют интеграцию крупных, тяжелых компонент. А у тебя они тестируют всю мелкую функциональность.
P>>>На мой взгляд это не нужно — самый минимум это ожидания вызывающей стороны. Всё остальное это если время некуда девать.
P>·>Так пиши ожидания когда они известны, если ты их знаешь. Анализ вайтбокс, покрытие кода — именно поиск неизвестных ожиданий.
P>·>"Мы тут написали все ожидания по спеке, но вот в тот else почему-то ни разу не попали ни разу. Разбираемся почему."
P>С моками чаще получается так — моками некто заставил попасть в этот else, а на проде этот else срабатывает совсем в другом контексте, и все валится именно там где покрыто тестом.
Очень вряд-ли, но допустим даже что-то вальнулось в проде. Изучив логи прода, ты с моками сможешь быстро воспроизвести сценарий и получить быстрый красный тест, который можно спокойно отлаживать и фиксить, без всякой интеграции.
P>·>Это только для tryAcceptComposition. А таких врапперов придётся написать под каждый метод контроллера. И на каждый пару тестов.
P>Именно! И никакие моки этого не отменяют!
Они отменяют необходимость написания тучи этих самых xxxComposition, нет кода — нечего тестировать.
P>>>Во вторых — моки не избавляют от необхидимости интеграционных тестов, в т.ч. запуска на реальной субд
P>·>Такой тест может быть один на всю систему. Запустилось, соединения установлены, версии совпадают, пинги идут. Готово.
P>Если у вас система из одного единственного роута и без зависимостей входящих-исходящих, то так и будет. А если роутов много, и есть зависимости, то надо интеграционными тестами покрыть весь входящий и исходящий трафик.
Не весь, а только покрывающий интеграцию компонент.
P>>>Вот я и говорю — вы сделали в бл зависимость от бд, а потом в тестах изолируетесь от нее. Можно ж и иначе пойти — изолироваться на этапе дизайна, тогда ничего мокать не надо
P>·>В бл зависимость от репы.
P>А следовательно и от бд. Например, вечная проблема с репозиториями, как туда фильтры передавать и всякие параметры, что бы не плодить сотни методов. Сменили эту механику — и весь код где прокидываются фильтры, издох — начинай переписывать тесты.
Это какая-то специфичная проблема в твоих проектах, я не в курсе.
P>·>Гораздо больше, чем динамическая.
P>Этого недостаточно. Нужен внятный интеграционный тест, его задача — следить что подсистема с правильными зависимостями соответствует ожиданиям
Задача инеграционного теста — тестировать места взаимодействия компонент, а не сценарии поведения.
P>>>тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория
P>·>А какие типы у объектов getUser/updateUser? И примерная реализация всех методов?
P>getUser — чистая функция, выдает объект-реквест, например select * from users where id=? + параметры + трансформации. UpdateUser — чистая функция, вычисляет все что нужно для операции. execute — принимает объект-реквест, возвращает трансформированый результат.
Как ассертить такую функцию в юнит-тесте? Как проверить, что текст запроса хотя бы синтаксически корректен? И как ассертить что параметры куда надо положены и транформация верна?
И далее, ну допустим ты как-то написал кучу тестов что getUser выглядит правильно. А дальше? Как убедиться, что настоящая субд будет работать с getUser именно так, как ты описал в ожиданиях теста репы?
>>> const updateUser = repository.updateUser(oldUser, newUser);
P>·>А если я случайно напишу
P>·>const updateUser = repository.updateUser(newUser, oldUser);
P>·>где что когда упадёт?
P>Упадет интеграционный тест, тест вида "выполним операцию и проверим, что состояние юзера соответствует ожиданиям".
Плохо, это функциональная бизнес-логика, а не интеграция. Это должно ловиться юнит-тестом, а не интеграционным.
P>·>А как контролиоруется что newUser возвращённый из бл обработается так как мы задумали в репе?
P>Метод репозитория тоже тестируется юнитами, без моков — его задача сгенерировать правильный реквест.
P>Проблему может вызвать ветвление в контроллере — он должен быть линейным.
Т.е. нужно писать интеграционный тест как минимум для каждого метода контроллера. Это слишком дохрена.
P>·>У тебя какое-то странное понимание моков. Цель мока не чтобы максимально точно описать поведение мокируемого, а чтобы описать различные возможные сценарии поведения мокируемого, на которые мы реагируем различным способом в sut.
P>Я вам пишу про разные кейсы из того что вижу, а вы все пытаетесь причину во мне найти
Т.е. ты пытаешься своё "Я так вижу" за какой-то объективный факт.
P>·>Т.е. при тестировании контроллера совершенно неважно, что именно делает реальный modifyUser. Важно какие варианты результата он может вернуть и что на них реагируем в sut в соответствии с ожиданиями.
P>А моки вам зачем для этого?
Чтобы описать варианты результата что может вернуть modifyUser.
P>>>А как это QA делают? Строчка требований — список тестов, где она фигурирует.
P>·>А как узнать, что требования покрывают как можно больше возможных сценариев, а не только те, о которых не забыли подумать?
P>Если мы очем то забыли, то никакой кавередж не спасет. Например, если вы забыли, что вам могут приходить данные разной природы, то всё, приплыли.
Это наверное для js актуально, где всё есть Object и где угодно может быть что угодно. При статической типизации вариантов разной природы не так уж много. Если modifyUser возвращает boolean, то тут только два варианта данных. А вот в js оно внезапно может выдать "FileNotFound" — и приехали.
P>>>Это я так трохи пошутил — идея в том, что нам всего то нужен один тест верхнего уровня. он у нас тупой как доска — буквально как http реквест-респонс
P>·>Ага. Но он, в лучшем случае, покроет только один xxxComposition. А их будет на каждый метод контроллера.
P>Это в любом случае так — e2e тесты должны покрыть весь входящий и исходящий трафик.
Вот тут тебе и аукнется цикломатическая сложность. Чтобы покрыть весть трафик — надо будет выполнить все возможные пути выполнения. Что просто дофига и очень долго.
P>>>Много где завезли, в джава коде я такое много где видел. А еще есть сравнение логов — если у вас вдруг появится лишний аудит, он обязательно сломает выхлоп. Это тоже блекбокс
P>·>Это означает, что небольшое изменение в формате аудита ломает все тесты с образцами логов. Вот и представь, добавление небольшой штучки в аудит поломает все твои "несколько штук" тестов на каждый роут.
P>Похоже, вы незнакомы с acceptance driven тестированием. В данном случае нам нужно просмотреть логи, и сделать коммит, если всё хорошо.
Это для игрушечных проектов ещё может только заработать. В более менее реальном проекте этих логов будут десятки тысяч. И просмотреть всё просто нереально. После тысячной строки диффа ты задолбаешься смотреть дальеш и тупо всё закоммитишь, незаметив особый случай в тысячепервой строке.
P>·>Именно. А должно быть несколько штук на весь сервис. Роутов может быть сотни.
P>Тесты верхнего уровня в любом случае должны покрывать весь апи. Вы же своими моками пытаетесь искусственно уровень тестирования понизить.
Нет. Интеграционные тесты должны протестировать интеграцию, а не весь апи.
P>>>Например, ваш бл-сервис неявно зависит от бд-клиента, через репозиторий. А это значит, смена бд сначала сломает ваш репозиторий, а это сломает ваш бл сервис.
P>·>Не понял, что значит "сломает"? Что за такая "смена бд"?
P>Как ваш репозиторий фильтры принимает ?
Не очень знаком с этой терминологией.
P>·>Зачем? В тесте контрллера используется бл, поэтому моки только для него. И что творится в репе с т.з. контроллера — совершенно неважно.
P>Если у вас полная изоляция, то так и будет. А если неполная, что чаще всего, то будет всё сломано.
P>Собственно — что бы добиться этой самой хорошей изоляции в коде девелопера надо тренировать очень долго.
Если изоляция нужна, можно использовать интерфейсы.
P>>> прямых — одна, а непрямых — все дочерние от того же репозитория
P>·>Неясно как "непрямые" зависимости влияют на _дизайн_ класса. Он их вообще никак не видит. Никак.
P>Я вот регулярно вижу такое — контроллер вызывает сервис, передает тому объекты http request response. А вот сервис, чья задача это бл, вычитывает всё из req, res, лезет в базу пополам с репозиторием, и еще собственно бл считает, плюс кеширование итд.
P>То есть, типичный разработчик не умеет пользоваться абстракциями и изоляцией, а пишет прямолинейный низкоуровневый код. И у него нет ни единого шанса протестировать такое чудовище кроме как моками.
Ну ликбез же.
P>>>Ничего странного — вы как то прошли мимо clean architecture.
P>·>Я как-то мимо rest и ui прошел в принципе... если есть какая ссылка "clean architecture for morons", давай.
P>https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Похоже это просто N-ая итерация очередной вариации самой правильной организации трёх слоёв типичного веб-приложения. MVC, MVP, MVI, MVVM, MVVM-C, VIPER, и ещё куча...
Причём тут моки — неясно.
Я вообще в последние N лет редко занимаюсь UI/web. У меня всякая поточка, очереди, сообщения, low/zero gc и прочее. Как там этот getUser сделать с zero gc — тот ещё вопрос.
Моки же общий инструмент, от деталей архитектуры зависит мало.
P>>>Я не знаю, в чем вы проблему видите. В жээс аннотация это обычный код, который инструментируется, и будет светиться или красным, или зеленым
P>·>В js вроде вообще нет аннотаций. Не знаю что именно ты зовёшь аннотацией тогда.
P>
А что за версия яп? Ну не важно. Это ещё можно простить, это просто маппинг урлов на объекты при отсутствии схемы REST. Как туда запихать аудит? Как аудит-аннотация будет описывать что, куда, когда и как аудитить?
P>·>И это всё юнит-тестится как ты обещал?! expext(Metadata.from(ControllerX, 'tryAccept')).metadata.to.match({...})?
P>Смотря что — валидатор, это юнит-тест. А вот чтото нужна или нет авторизация, это можно переопределить конфигом или переменными окружения.
P>>>Зачем? Есть трассировка и правило — результат аудита можно сравнивать
P>·>Не понял к чему ты это говоришь. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?
P>Кое что надо и в самом методе вызывать, без этого работать не будет.
Именно! И накой тогда аннотация нужна для этого?
P> Видите нужные строчки — аудит работает, выхлоп совпадает с предыдущим забегом — точно все отлично. Не совпадает — точно есть проблемы.
Строчки где видим? Аудит может сообщения куда-нибудь в сокет слать или в бд отдельную писать.
И валидировать, что для данных сценариев записываются нужная инфа в аудит?
Суть в том, что детали аудита каждой бизнес-операции можно покрывать туевой хучей отдельных юнит-тестов с моками на каждое возможное ожидание. И тут аннотации будут только мешаться. Интеграционный тест же можно написать один — что хоть какая-то бизнес-операция записала хоть какой-то аудит.
P>>>Например, нам нужно считать токен для стрима, нас интересует ид класса, ид сущность, версия сущность b секрет.
P>·>Не очень ясно в чём идея. У тебя есть библотека которая умеет несколько алгоритмов, апдейт по частям с приведением типов, формирование дайджеста в разных форматах. И ты это оборачиваешь в свою обёртку, которая умеет только одно — конкретный алгоритм упакованный в отдельный метод Token.from, который содержит конкретные параметры и т.п. И что в этом такого особенного? Называется частичное применение функции. И причём тут моки?
P>Это я вам пример привел, для чего вообще люди мокают, когда всё прекрасно тестируется и без моков.
Т.е. проблема не в моках как таковых, а в том, что некоторые люди используют некоторые подходы и пишут корявый код? Это проблема ликбеза, а не моков. Ровно та же проблема есть в любых подходах тестирования.
P>·>Проблема такого подхода в том, что это работает только в тривиальных случаях. Когда в твоём приложении понадобятся много разных комбинаций, то у тебя количество частично применённых функций пойдёт по экспоненте. У тебя получатся ApiTokenAsHex.from, ApiTokenAsBytes.from, ApiTokenAsB64.from, HashedPassword.from, HashedPasswordAsBytes.from и пошло поехало.
P>А зачем вам эти AsHex, AsBytes? Это же просто преставление. У вас есть токен — это значение. Его можно создать парой-тройкой способов, и сохранить так же.
Именно. 3 способа создания x 3 способа представления — уже девять комбинаций. У тебя будет либо девять функций, либо своя обёртка над сторонним api, изоморфная по сложности тому же api.
P> Отсюда ясно, что нет нужды плодить те функции, что вы предложили, а следовательно и тестировать их тоже не нужно.
Нужда идёт из кода, из требований. Если у тебя достаточно сложная система, у тебя будет там все 9 комбинаций, а то и больше.
P>Всех дел — отделить данные от представления, и количество тестов уменьшается.
Количество может быть уменьшено, только если ты какое-то требование перестал тестировать.
P>>>Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.
P>·>Т.е. вынесли кусочек кода в метод и его протестировали отдельно. И? Моки тут причём?
P>Насколько я понимаю, я вам показываю, как разные люди понимают моки. Вы же всё приписываете лично мне.
Что лично тебе я приписал?
P>·>Но если у тебя оно нужно конкретно в одном месте для etag, то это уже оверкилл.
P>Вас всё тянет любое высказывание к абсурду свести. Естетсвенно, что городить вагон абстракций, ради того, что бы вычислить етаг не нужно.
А ты таки предложил новую абстракцию ввести в виде ApiToken.from.
P>При этом и моки тоже не нужны для этого случая.
P>У нас будет функция getEtag(content) которую можно проверить разными значениями.
Именно. Зачем нам тогда нужен ApiToken.from?
P>Тем не менее, пример на моках что я вам показал, он именно отсюда взят.
Пример на то и пример, чтобы показать как использовать моки. А в твоём примере было показано как _не надо_ писать код. Я показал как надо писать код при использовании моков. И мой код был вполне вменяемым. Оправданы ли они в данном случае моки — вопрос другой.
P>>>ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.
P>·>Оборачивать каждую стороннюю либу — т.к. её _возможно_ нужно будет заменить — это оверинжиниринг в большинстве случаев.
P>При чем здесь оборачивание? У вас есть функционал — верификация jwt токена.
P>Объясните внятно, почему замена jsonwebtoken на jose должна сломать тесты?
P>А вот если вы замокали jsonwebtoken, то ваши тесты сломаются. И причина только в том, что вы замокали там где этого не надо делать.
В примере никакого jwt токена не было. Я не знаю ты о чём, у тебя где-то в голове какая-то более широкая картина, но к сожалению, я не очень хороший телепат.
P>>>а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)
P>·>А какая разница? Ведь эту конструкцию надо будет тоже каким-то тестом покрыть.
P>Для этого есть интеграционный тест
Жуть. Т.е. ты не можешь что-то протестировать быстрыми тестами, приходится плодить медленные интеграционные и тривиальные ошибки обнаруживать только после запуска всего приложения. Спасибо, не хочу.
P>>>Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.
P>·>То что ты логику в коде куда-то переносишь, не означает, что её вдруг можно не тестировать.
P>Там где логика — там юнит-тесты, без моков, а там где интеграция — там интеграционные тесты, тоже без моков.
Слишком тяжелый подход. Только на мелких приложениях работает.
P>>>Вот-вот, еще чтото надо куда то инжектить, вызывать, проверять, а вызвалось ли замоканое нужное количество раз, в нужной последовательности, итд итд итд
P>·>Это надо не для моков как таковых (если validateThing — тривиальна, то зачем её вообще мочить? Всегда можно запихать реальную имплементацию), а для разделения компонент и независимого их тестирования.
P>А задизайнить компоненты, что бы они не зависели друг от друга?
Это как? То что зависимости неявные — это не означает, что их нет.
P>·>Я тебе уже написал когда это _надо_, но ты как-то тихо проигнорировал: Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.
P>Это ваш выбор дизайна — на самом низу стека вы вдруг вызываете чтение из сети-базы-итд, или начинаете жечь cpu.
P>А если этого не делать, то и моки не нужны.
Ага-ага. Но тогда нужны ещё более медленные и неуклюжие интеграционные тесты, много.
P>>>Итого — вы прибили тесты к реализации
P>·>В js принято валить всё в Global и сплошные синглтоны? DI не слышали?
P>Зачем global? DI можно по разному использовать — речь же об этом. Посмотрите framework as a detail или clean architecture, сразу станет понятнее.
require это нифига не DI, это SL.
P>Вместо выстраивания глубокой иерархии, где на самом низу стека идет чтение из бд, делаем иначе — вычисляем, что читать из бд, следующим шагом выполняем чтение.
P>Итого — самое важное тестируется плотно юнит-тестами, а остальное — интеграционными
P>С вашими моками покрытие будет хуже, кода больше, а интеграционные всё равно нужны
Интеграционные тесты тестируют интеграцию крупных, тяжелых компонент. А у тебя они тестируют всю мелкую функциональность.
P>>>На мой взгляд это не нужно — самый минимум это ожидания вызывающей стороны. Всё остальное это если время некуда девать.
P>·>Так пиши ожидания когда они известны, если ты их знаешь. Анализ вайтбокс, покрытие кода — именно поиск неизвестных ожиданий.
P>·>"Мы тут написали все ожидания по спеке, но вот в тот else почему-то ни разу не попали ни разу. Разбираемся почему."
P>С моками чаще получается так — моками некто заставил попасть в этот else, а на проде этот else срабатывает совсем в другом контексте, и все валится именно там где покрыто тестом.
Очень вряд-ли, но допустим даже что-то вальнулось в проде. Изучив логи прода, ты с моками сможешь быстро воспроизвести сценарий и получить быстрый красный тест, который можно спокойно отлаживать и фиксить, без всякой интеграции.
P>·>Это только для tryAcceptComposition. А таких врапперов придётся написать под каждый метод контроллера. И на каждый пару тестов.
P>Именно! И никакие моки этого не отменяют!
Они отменяют необходимость написания тучи этих самых xxxComposition, нет кода — нечего тестировать.
P>>>Во вторых — моки не избавляют от необхидимости интеграционных тестов, в т.ч. запуска на реальной субд
P>·>Такой тест может быть один на всю систему. Запустилось, соединения установлены, версии совпадают, пинги идут. Готово.
P>Если у вас система из одного единственного роута и без зависимостей входящих-исходящих, то так и будет. А если роутов много, и есть зависимости, то надо интеграционными тестами покрыть весь входящий и исходящий трафик.
Не весь, а только покрывающий интеграцию компонент.
P>>>Вот я и говорю — вы сделали в бл зависимость от бд, а потом в тестах изолируетесь от нее. Можно ж и иначе пойти — изолироваться на этапе дизайна, тогда ничего мокать не надо
P>·>В бл зависимость от репы.
P>А следовательно и от бд. Например, вечная проблема с репозиториями, как туда фильтры передавать и всякие параметры, что бы не плодить сотни методов. Сменили эту механику — и весь код где прокидываются фильтры, издох — начинай переписывать тесты.
Это какая-то специфичная проблема в твоих проектах, я не в курсе.
P>·>Гораздо больше, чем динамическая.
P>Этого недостаточно. Нужен внятный интеграционный тест, его задача — следить что подсистема с правильными зависимостями соответствует ожиданиям
Задача инеграционного теста — тестировать места взаимодействия компонент, а не сценарии поведения.
P>>>тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория
P>·>А какие типы у объектов getUser/updateUser? И примерная реализация всех методов?
P>getUser — чистая функция, выдает объект-реквест, например select * from users where id=? + параметры + трансформации. UpdateUser — чистая функция, вычисляет все что нужно для операции. execute — принимает объект-реквест, возвращает трансформированый результат.
Как ассертить такую функцию в юнит-тесте? Как проверить, что текст запроса хотя бы синтаксически корректен? И как ассертить что параметры куда надо положены и транформация верна?
И далее, ну допустим ты как-то написал кучу тестов что getUser выглядит правильно. А дальше? Как убедиться, что настоящая субд будет работать с getUser именно так, как ты описал в ожиданиях теста репы?
>>> const updateUser = repository.updateUser(oldUser, newUser);
P>·>А если я случайно напишу
P>·>const updateUser = repository.updateUser(newUser, oldUser);
P>·>где что когда упадёт?
P>Упадет интеграционный тест, тест вида "выполним операцию и проверим, что состояние юзера соответствует ожиданиям".
Плохо, это функциональная бизнес-логика, а не интеграция. Это должно ловиться юнит-тестом, а не интеграционным.
P>·>А как контролиоруется что newUser возвращённый из бл обработается так как мы задумали в репе?
P>Метод репозитория тоже тестируется юнитами, без моков — его задача сгенерировать правильный реквест.
P>Проблему может вызвать ветвление в контроллере — он должен быть линейным.
Т.е. нужно писать интеграционный тест как минимум для каждого метода контроллера. Это слишком дохрена.
P>·>У тебя какое-то странное понимание моков. Цель мока не чтобы максимально точно описать поведение мокируемого, а чтобы описать различные возможные сценарии поведения мокируемого, на которые мы реагируем различным способом в sut.
P>Я вам пишу про разные кейсы из того что вижу, а вы все пытаетесь причину во мне найти
Т.е. ты пытаешься своё "Я так вижу" за какой-то объективный факт.
P>·>Т.е. при тестировании контроллера совершенно неважно, что именно делает реальный modifyUser. Важно какие варианты результата он может вернуть и что на них реагируем в sut в соответствии с ожиданиями.
P>А моки вам зачем для этого?
Чтобы описать варианты результата что может вернуть modifyUser.
P>>>А как это QA делают? Строчка требований — список тестов, где она фигурирует.
P>·>А как узнать, что требования покрывают как можно больше возможных сценариев, а не только те, о которых не забыли подумать?
P>Если мы очем то забыли, то никакой кавередж не спасет. Например, если вы забыли, что вам могут приходить данные разной природы, то всё, приплыли.
Это наверное для js актуально, где всё есть Object и где угодно может быть что угодно. При статической типизации вариантов разной природы не так уж много. Если modifyUser возвращает boolean, то тут только два варианта данных. А вот в js оно внезапно может выдать "FileNotFound" — и приехали.
P>>>Это я так трохи пошутил — идея в том, что нам всего то нужен один тест верхнего уровня. он у нас тупой как доска — буквально как http реквест-респонс
P>·>Ага. Но он, в лучшем случае, покроет только один xxxComposition. А их будет на каждый метод контроллера.
P>Это в любом случае так — e2e тесты должны покрыть весь входящий и исходящий трафик.
Вот тут тебе и аукнется цикломатическая сложность. Чтобы покрыть весть трафик — надо будет выполнить все возможные пути выполнения. Что просто дофига и очень долго.
P>>>Много где завезли, в джава коде я такое много где видел. А еще есть сравнение логов — если у вас вдруг появится лишний аудит, он обязательно сломает выхлоп. Это тоже блекбокс
P>·>Это означает, что небольшое изменение в формате аудита ломает все тесты с образцами логов. Вот и представь, добавление небольшой штучки в аудит поломает все твои "несколько штук" тестов на каждый роут.
P>Похоже, вы незнакомы с acceptance driven тестированием. В данном случае нам нужно просмотреть логи, и сделать коммит, если всё хорошо.
Это для игрушечных проектов ещё может только заработать. В более менее реальном проекте этих логов будут десятки тысяч. И просмотреть всё просто нереально. После тысячной строки диффа ты задолбаешься смотреть дальеш и тупо всё закоммитишь, незаметив особый случай в тысячепервой строке.
P>·>Именно. А должно быть несколько штук на весь сервис. Роутов может быть сотни.
P>Тесты верхнего уровня в любом случае должны покрывать весь апи. Вы же своими моками пытаетесь искусственно уровень тестирования понизить.
Нет. Интеграционные тесты должны протестировать интеграцию, а не весь апи.
P>>>Например, ваш бл-сервис неявно зависит от бд-клиента, через репозиторий. А это значит, смена бд сначала сломает ваш репозиторий, а это сломает ваш бл сервис.
P>·>Не понял, что значит "сломает"? Что за такая "смена бд"?
P>Как ваш репозиторий фильтры принимает ?
Не очень знаком с этой терминологией.
P>·>Зачем? В тесте контрллера используется бл, поэтому моки только для него. И что творится в репе с т.з. контроллера — совершенно неважно.
P>Если у вас полная изоляция, то так и будет. А если неполная, что чаще всего, то будет всё сломано.
P>Собственно — что бы добиться этой самой хорошей изоляции в коде девелопера надо тренировать очень долго.
Если изоляция нужна, можно использовать интерфейсы.
P>>> прямых — одна, а непрямых — все дочерние от того же репозитория
P>·>Неясно как "непрямые" зависимости влияют на _дизайн_ класса. Он их вообще никак не видит. Никак.
P>Я вот регулярно вижу такое — контроллер вызывает сервис, передает тому объекты http request response. А вот сервис, чья задача это бл, вычитывает всё из req, res, лезет в базу пополам с репозиторием, и еще собственно бл считает, плюс кеширование итд.
P>То есть, типичный разработчик не умеет пользоваться абстракциями и изоляцией, а пишет прямолинейный низкоуровневый код. И у него нет ни единого шанса протестировать такое чудовище кроме как моками.
Ну ликбез же.
P>>>Ничего странного — вы как то прошли мимо clean architecture.
P>·>Я как-то мимо rest и ui прошел в принципе... если есть какая ссылка "clean architecture for morons", давай.
P>https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Похоже это просто N-ая итерация очередной вариации самой правильной организации трёх слоёв типичного веб-приложения. MVC, MVP, MVI, MVVM, MVVM-C, VIPER, и ещё куча...
Причём тут моки — неясно.
Я вообще в последние N лет редко занимаюсь UI/web. У меня всякая поточка, очереди, сообщения, low/zero gc и прочее. Как там этот getUser сделать с zero gc — тот ещё вопрос.
Моки же общий инструмент, от деталей архитектуры зависит мало.
P>>>Я не знаю, в чем вы проблему видите. В жээс аннотация это обычный код, который инструментируется, и будет светиться или красным, или зеленым
P>·>В js вроде вообще нет аннотаций. Не знаю что именно ты зовёшь аннотацией тогда.
P>
P> @Operation({method: 'PATCH', route:'/users', summary: 'modify user', responseType: User, })
P> modify(@Req() req: RawBodyRequest<User>): Promise<User> {
P> ...
P> }
P>
А что за версия яп? Ну не важно. Это ещё можно простить, это просто маппинг урлов на объекты при отсутствии схемы REST. Как туда запихать аудит? Как аудит-аннотация будет описывать что, куда, когда и как аудитить?
P>·>И это всё юнит-тестится как ты обещал?! expext(Metadata.from(ControllerX, 'tryAccept')).metadata.to.match({...})?
P>Смотря что — валидатор, это юнит-тест. А вот чтото нужна или нет авторизация, это можно переопределить конфигом или переменными окружения.
P>>>Зачем? Есть трассировка и правило — результат аудита можно сравнивать
P>·>Не понял к чему ты это говоришь. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?
P>Кое что надо и в самом методе вызывать, без этого работать не будет.
Именно! И накой тогда аннотация нужна для этого?
P> Видите нужные строчки — аудит работает, выхлоп совпадает с предыдущим забегом — точно все отлично. Не совпадает — точно есть проблемы.
Строчки где видим? Аудит может сообщения куда-нибудь в сокет слать или в бд отдельную писать.
И валидировать, что для данных сценариев записываются нужная инфа в аудит?
Суть в том, что детали аудита каждой бизнес-операции можно покрывать туевой хучей отдельных юнит-тестов с моками на каждое возможное ожидание. И тут аннотации будут только мешаться. Интеграционный тест же можно написать один — что хоть какая-то бизнес-операция записала хоть какой-то аудит.
Re[18]: Что такое Dependency Rejection
Здравствуйте, Pauel, Вы писали:
P>>>Например, нам нужно считать токен для стрима, нас интересует ид класса, ид сущность, версия сущность b секрет.
P>·>Не очень ясно в чём идея. У тебя есть библотека которая умеет несколько алгоритмов, апдейт по частям с приведением типов, формирование дайджеста в разных форматах. И ты это оборачиваешь в свою обёртку, которая умеет только одно — конкретный алгоритм упакованный в отдельный метод Token.from, который содержит конкретные параметры и т.п. И что в этом такого особенного? Называется частичное применение функции. И причём тут моки?
P>Это я вам пример привел, для чего вообще люди мокают, когда всё прекрасно тестируется и без моков.
Т.е. проблема не в моках как таковых, а в том, что некоторые люди используют некоторые подходы и пишут корявый код? Это проблема ликбеза, а не моков. Ровно та же проблема есть в любых подходах тестирования.
P>·>Проблема такого подхода в том, что это работает только в тривиальных случаях. Когда в твоём приложении понадобятся много разных комбинаций, то у тебя количество частично применённых функций пойдёт по экспоненте. У тебя получатся ApiTokenAsHex.from, ApiTokenAsBytes.from, ApiTokenAsB64.from, HashedPassword.from, HashedPasswordAsBytes.from и пошло поехало.
P>А зачем вам эти AsHex, AsBytes? Это же просто преставление. У вас есть токен — это значение. Его можно создать парой-тройкой способов, и сохранить так же.
Именно. 3 способа создания x 3 способа представления — уже девять комбинаций. У тебя будет либо девять функций, либо своя обёртка над сторонним api, изоморфная по сложности тому же api.
P> Отсюда ясно, что нет нужды плодить те функции, что вы предложили, а следовательно и тестировать их тоже не нужно.
Нужда идёт из кода, из требований. Если у тебя достаточно сложная система, у тебя будет там все 9 комбинаций, а то и больше.
P>Всех дел — отделить данные от представления, и количество тестов уменьшается.
Количество может быть уменьшено, только если ты какое-то требование перестал тестировать.
P>>>Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.
P>·>Т.е. вынесли кусочек кода в метод и его протестировали отдельно. И? Моки тут причём?
P>Насколько я понимаю, я вам показываю, как разные люди понимают моки. Вы же всё приписываете лично мне.
Что лично тебе я приписал?
P>·>Но если у тебя оно нужно конкретно в одном месте для etag, то это уже оверкилл.
P>Вас всё тянет любое высказывание к абсурду свести. Естетсвенно, что городить вагон абстракций, ради того, что бы вычислить етаг не нужно.
А ты таки предложил новую абстракцию ввести в виде ApiToken.from.
P>При этом и моки тоже не нужны для этого случая.
P>У нас будет функция getEtag(content) которую можно проверить разными значениями.
Именно. Зачем нам тогда нужен ApiToken.from?
P>Тем не менее, пример на моках что я вам показал, он именно отсюда взят.
Пример на то и пример, чтобы показать как использовать моки. А в твоём примере было показано как _не надо_ писать код. Я показал как надо писать код при использовании моков. И мой код был вполне вменяемым. Оправданы ли они в данном случае моки — вопрос другой.
P>>>ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.
P>·>Оборачивать каждую стороннюю либу — т.к. её _возможно_ нужно будет заменить — это оверинжиниринг в большинстве случаев.
P>При чем здесь оборачивание? У вас есть функционал — верификация jwt токена.
P>Объясните внятно, почему замена jsonwebtoken на jose должна сломать тесты?
P>А вот если вы замокали jsonwebtoken, то ваши тесты сломаются. И причина только в том, что вы замокали там где этого не надо делать.
В примере никакого jwt токена не было. Я не знаю ты о чём, у тебя где-то в голове какая-то более широкая картина, но к сожалению, я не очень хороший телепат.
P>>>а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)
P>·>А какая разница? Ведь эту конструкцию надо будет тоже каким-то тестом покрыть.
P>Для этого есть интеграционный тест
Жуть. Т.е. ты не можешь что-то протестировать быстрыми тестами, приходится плодить медленные интеграционные и тривиальные ошибки обнаруживать только после запуска всего приложения. Спасибо, не хочу.
P>>>Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.
P>·>То что ты логику в коде куда-то переносишь, не означает, что её вдруг можно не тестировать.
P>Там где логика — там юнит-тесты, без моков, а там где интеграция — там интеграционные тесты, тоже без моков.
Слишком тяжелый подход. Только на мелких приложениях работает.
P>>>Вот-вот, еще чтото надо куда то инжектить, вызывать, проверять, а вызвалось ли замоканое нужное количество раз, в нужной последовательности, итд итд итд
P>·>Это надо не для моков как таковых (если validateThing — тривиальна, то зачем её вообще мочить? Всегда можно запихать реальную имплементацию), а для разделения компонент и независимого их тестирования.
P>А задизайнить компоненты, что бы они не зависели друг от друга?
Это как? То что зависимости неявные — это не означает, что их нет.
P>·>Я тебе уже написал когда это _надо_, но ты как-то тихо проигнорировал: Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.
P>Это ваш выбор дизайна — на самом низу стека вы вдруг вызываете чтение из сети-базы-итд, или начинаете жечь cpu.
P>А если этого не делать, то и моки не нужны.
Ага-ага. Но тогда нужны ещё более медленные и неуклюжие интеграционные тесты, много.
P>>>Итого — вы прибили тесты к реализации
P>·>В js принято валить всё в Global и сплошные синглтоны? DI не слышали?
P>Зачем global? DI можно по разному использовать — речь же об этом. Посмотрите framework as a detail или clean architecture, сразу станет понятнее.
require это нифига не DI, это SL.
P>Вместо выстраивания глубокой иерархии, где на самом низу стека идет чтение из бд, делаем иначе — вычисляем, что читать из бд, следующим шагом выполняем чтение.
P>Итого — самое важное тестируется плотно юнит-тестами, а остальное — интеграционными
P>С вашими моками покрытие будет хуже, кода больше, а интеграционные всё равно нужны
Интеграционные тесты тестируют интеграцию крупных, тяжелых компонент. А у тебя они тестируют всю мелкую функциональность.
P>>>На мой взгляд это не нужно — самый минимум это ожидания вызывающей стороны. Всё остальное это если время некуда девать.
P>·>Так пиши ожидания когда они известны, если ты их знаешь. Анализ вайтбокс, покрытие кода — именно поиск неизвестных ожиданий.
P>·>"Мы тут написали все ожидания по спеке, но вот в тот else почему-то ни разу не попали ни разу. Разбираемся почему."
P>С моками чаще получается так — моками некто заставил попасть в этот else, а на проде этот else срабатывает совсем в другом контексте, и все валится именно там где покрыто тестом.
Очень вряд-ли, но допустим даже что-то вальнулось в проде. Изучив логи прода, ты с моками сможешь быстро воспроизвести сценарий и получить быстрый красный тест, который можно спокойно отлаживать и фиксить, без всякой интеграции.
P>·>Это только для tryAcceptComposition. А таких врапперов придётся написать под каждый метод контроллера. И на каждый пару тестов.
P>Именно! И никакие моки этого не отменяют!
Они отменяют необходимость написания тучи этих самых xxxComposition, нет кода — нечего тестировать.
P>>>Во вторых — моки не избавляют от необхидимости интеграционных тестов, в т.ч. запуска на реальной субд
P>·>Такой тест может быть один на всю систему. Запустилось, соединения установлены, версии совпадают, пинги идут. Готово.
P>Если у вас система из одного единственного роута и без зависимостей входящих-исходящих, то так и будет. А если роутов много, и есть зависимости, то надо интеграционными тестами покрыть весь входящий и исходящий трафик.
Не весь, а только покрывающий интеграцию компонент.
P>>>Вот я и говорю — вы сделали в бл зависимость от бд, а потом в тестах изолируетесь от нее. Можно ж и иначе пойти — изолироваться на этапе дизайна, тогда ничего мокать не надо
P>·>В бл зависимость от репы.
P>А следовательно и от бд. Например, вечная проблема с репозиториями, как туда фильтры передавать и всякие параметры, что бы не плодить сотни методов. Сменили эту механику — и весь код где прокидываются фильтры, издох — начинай переписывать тесты.
Это какая-то специфичная проблема в твоих проектах, я не в курсе.
P>·>Гораздо больше, чем динамическая.
P>Этого недостаточно. Нужен внятный интеграционный тест, его задача — следить что подсистема с правильными зависимостями соответствует ожиданиям
Задача инеграционного теста — тестировать места взаимодействия компонент, а не сценарии поведения.
P>>>тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория
P>·>А какие типы у объектов getUser/updateUser? И примерная реализация всех методов?
P>getUser — чистая функция, выдает объект-реквест, например select * from users where id=? + параметры + трансформации. UpdateUser — чистая функция, вычисляет все что нужно для операции. execute — принимает объект-реквест, возвращает трансформированый результат.
Как ассертить такую функцию в юнит-тесте? Как проверить, что текст запроса хотя бы синтаксически корректен? И как ассертить что параметры куда надо положены и транформация верна?
И далее, ну допустим ты как-то написал кучу тестов что getUser выглядит правильно. А дальше? Как убедиться, что настоящая субд будет работать с getUser именно так, как ты описал в ожиданиях теста репы?
>>> const updateUser = repository.updateUser(oldUser, newUser);
P>·>А если я случайно напишу
P>·>const updateUser = repository.updateUser(newUser, oldUser);
P>·>где что когда упадёт?
P>Упадет интеграционный тест, тест вида "выполним операцию и проверим, что состояние юзера соответствует ожиданиям".
Плохо, это функциональная бизнес-логика, а не интеграция. Это должно ловиться юнит-тестом, а не интеграционным.
P>·>А как контролиоруется что newUser возвращённый из бл обработается так как мы задумали в репе?
P>Метод репозитория тоже тестируется юнитами, без моков — его задача сгенерировать правильный реквест.
P>Проблему может вызвать ветвление в контроллере — он должен быть линейным.
Т.е. нужно писать интеграционный тест как минимум для каждого метода контроллера. Это слишком дохрена.
P>·>У тебя какое-то странное понимание моков. Цель мока не чтобы максимально точно описать поведение мокируемого, а чтобы описать различные возможные сценарии поведения мокируемого, на которые мы реагируем различным способом в sut.
P>Я вам пишу про разные кейсы из того что вижу, а вы все пытаетесь причину во мне найти
Т.е. ты пытаешься своё "Я так вижу" за какой-то объективный факт.
P>·>Т.е. при тестировании контроллера совершенно неважно, что именно делает реальный modifyUser. Важно какие варианты результата он может вернуть и что на них реагируем в sut в соответствии с ожиданиями.
P>А моки вам зачем для этого?
Чтобы описать варианты результата что может вернуть modifyUser.
P>>>А как это QA делают? Строчка требований — список тестов, где она фигурирует.
P>·>А как узнать, что требования покрывают как можно больше возможных сценариев, а не только те, о которых не забыли подумать?
P>Если мы очем то забыли, то никакой кавередж не спасет. Например, если вы забыли, что вам могут приходить данные разной природы, то всё, приплыли.
Это наверное для js актуально, где всё есть Object и где угодно может быть что угодно. При статической типизации вариантов разной природы не так уж много. Если modifyUser возвращает boolean, то тут только два варианта данных. А вот в js оно внезапно может выдать "FileNotFound" — и приехали.
P>>>Это я так трохи пошутил — идея в том, что нам всего то нужен один тест верхнего уровня. он у нас тупой как доска — буквально как http реквест-респонс
P>·>Ага. Но он, в лучшем случае, покроет только один xxxComposition. А их будет на каждый метод контроллера.
P>Это в любом случае так — e2e тесты должны покрыть весь входящий и исходящий трафик.
Вот тут тебе и аукнется цикломатическая сложность. Чтобы покрыть весть трафик — надо будет выполнить все возможные пути выполнения. Что просто дофига и очень долго.
P>>>Много где завезли, в джава коде я такое много где видел. А еще есть сравнение логов — если у вас вдруг появится лишний аудит, он обязательно сломает выхлоп. Это тоже блекбокс
P>·>Это означает, что небольшое изменение в формате аудита ломает все тесты с образцами логов. Вот и представь, добавление небольшой штучки в аудит поломает все твои "несколько штук" тестов на каждый роут.
P>Похоже, вы незнакомы с acceptance driven тестированием. В данном случае нам нужно просмотреть логи, и сделать коммит, если всё хорошо.
Это для игрушечных проектов ещё может только заработать. В более менее реальном проекте этих логов будут десятки тысяч. И просмотреть всё просто нереально. После тысячной строки диффа ты задолбаешься смотреть дальеш и тупо всё закоммитишь, незаметив особый случай в тысячепервой строке.
P>·>Именно. А должно быть несколько штук на весь сервис. Роутов может быть сотни.
P>Тесты верхнего уровня в любом случае должны покрывать весь апи. Вы же своими моками пытаетесь искусственно уровень тестирования понизить.
Нет. Интеграционные тесты должны протестировать интеграцию, а не весь апи.
P>>>Например, ваш бл-сервис неявно зависит от бд-клиента, через репозиторий. А это значит, смена бд сначала сломает ваш репозиторий, а это сломает ваш бл сервис.
P>·>Не понял, что значит "сломает"? Что за такая "смена бд"?
P>Как ваш репозиторий фильтры принимает ?
Не очень знаком с этой терминологией.
P>·>Зачем? В тесте контрллера используется бл, поэтому моки только для него. И что творится в репе с т.з. контроллера — совершенно неважно.
P>Если у вас полная изоляция, то так и будет. А если неполная, что чаще всего, то будет всё сломано.
P>Собственно — что бы добиться этой самой хорошей изоляции в коде девелопера надо тренировать очень долго.
Если изоляция нужна, можно использовать интерфейсы.
P>>> прямых — одна, а непрямых — все дочерние от того же репозитория
P>·>Неясно как "непрямые" зависимости влияют на _дизайн_ класса. Он их вообще никак не видит. Никак.
P>Я вот регулярно вижу такое — контроллер вызывает сервис, передает тому объекты http request response. А вот сервис, чья задача это бл, вычитывает всё из req, res, лезет в базу пополам с репозиторием, и еще собственно бл считает, плюс кеширование итд.
P>То есть, типичный разработчик не умеет пользоваться абстракциями и изоляцией, а пишет прямолинейный низкоуровневый код. И у него нет ни единого шанса протестировать такое чудовище кроме как моками.
Ну ликбез же.
P>>>Ничего странного — вы как то прошли мимо clean architecture.
P>·>Я как-то мимо rest и ui прошел в принципе... если есть какая ссылка "clean architecture for morons", давай.
P>https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Похоже это просто N-ая итерация очередной вариации самой правильной организации трёх слоёв типичного веб-приложения. MVC, MVP, MVI, MVVM, MVVM-C, VIPER, и ещё куча...
Причём тут моки — неясно.
Я вообще в последние N лет редко занимаюсь UI/web. У меня всякая поточка, очереди, сообщения, low/zero gc и прочее. Как там этот getUser сделать с zero gc — тот ещё вопрос.
Моки же общий инструмент, от деталей архитектуры зависит мало.
P>>>Я не знаю, в чем вы проблему видите. В жээс аннотация это обычный код, который инструментируется, и будет светиться или красным, или зеленым
P>·>В js вроде вообще нет аннотаций. Не знаю что именно ты зовёшь аннотацией тогда.
P>
А что за версия яп? Ну не важно. Это ещё можно простить, это просто маппинг урлов на объекты при отсутствии схемы REST. Как туда запихать аудит? Как аудит-аннотация будет описывать что, куда, когда и как аудитить?
P>·>И это всё юнит-тестится как ты обещал?! expext(Metadata.from(ControllerX, 'tryAccept')).metadata.to.match({...})?
P>Смотря что — валидатор, это юнит-тест. А вот чтото нужна или нет авторизация, это можно переопределить конфигом или переменными окружения.
P>>>Зачем? Есть трассировка и правило — результат аудита можно сравнивать
P>·>Не понял к чему ты это говоришь. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?
P>Кое что надо и в самом методе вызывать, без этого работать не будет.
Именно! И накой тогда аннотация нужна для этого?
P> Видите нужные строчки — аудит работает, выхлоп совпадает с предыдущим забегом — точно все отлично. Не совпадает — точно есть проблемы.
Строчки где видим? Аудит может сообщения куда-нибудь в сокет слать или в бд отдельную писать.
И где валидировать, что для данных сценариев записываются нужная инфа в аудит?
Суть в том, что детали аудита каждой бизнес-операции можно покрывать туевой хучей отдельных юнит-тестов с моками на каждое возможное ожидание. И тут аннотации будут только мешаться. Интеграционный тест же можно написать один — что хоть какая-то бизнес-операция записала хоть какой-то аудит.
P>>>Например, нам нужно считать токен для стрима, нас интересует ид класса, ид сущность, версия сущность b секрет.
P>·>Не очень ясно в чём идея. У тебя есть библотека которая умеет несколько алгоритмов, апдейт по частям с приведением типов, формирование дайджеста в разных форматах. И ты это оборачиваешь в свою обёртку, которая умеет только одно — конкретный алгоритм упакованный в отдельный метод Token.from, который содержит конкретные параметры и т.п. И что в этом такого особенного? Называется частичное применение функции. И причём тут моки?
P>Это я вам пример привел, для чего вообще люди мокают, когда всё прекрасно тестируется и без моков.
Т.е. проблема не в моках как таковых, а в том, что некоторые люди используют некоторые подходы и пишут корявый код? Это проблема ликбеза, а не моков. Ровно та же проблема есть в любых подходах тестирования.
P>·>Проблема такого подхода в том, что это работает только в тривиальных случаях. Когда в твоём приложении понадобятся много разных комбинаций, то у тебя количество частично применённых функций пойдёт по экспоненте. У тебя получатся ApiTokenAsHex.from, ApiTokenAsBytes.from, ApiTokenAsB64.from, HashedPassword.from, HashedPasswordAsBytes.from и пошло поехало.
P>А зачем вам эти AsHex, AsBytes? Это же просто преставление. У вас есть токен — это значение. Его можно создать парой-тройкой способов, и сохранить так же.
Именно. 3 способа создания x 3 способа представления — уже девять комбинаций. У тебя будет либо девять функций, либо своя обёртка над сторонним api, изоморфная по сложности тому же api.
P> Отсюда ясно, что нет нужды плодить те функции, что вы предложили, а следовательно и тестировать их тоже не нужно.
Нужда идёт из кода, из требований. Если у тебя достаточно сложная система, у тебя будет там все 9 комбинаций, а то и больше.
P>Всех дел — отделить данные от представления, и количество тестов уменьшается.
Количество может быть уменьшено, только если ты какое-то требование перестал тестировать.
P>>>Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.
P>·>Т.е. вынесли кусочек кода в метод и его протестировали отдельно. И? Моки тут причём?
P>Насколько я понимаю, я вам показываю, как разные люди понимают моки. Вы же всё приписываете лично мне.
Что лично тебе я приписал?
P>·>Но если у тебя оно нужно конкретно в одном месте для etag, то это уже оверкилл.
P>Вас всё тянет любое высказывание к абсурду свести. Естетсвенно, что городить вагон абстракций, ради того, что бы вычислить етаг не нужно.
А ты таки предложил новую абстракцию ввести в виде ApiToken.from.
P>При этом и моки тоже не нужны для этого случая.
P>У нас будет функция getEtag(content) которую можно проверить разными значениями.
Именно. Зачем нам тогда нужен ApiToken.from?
P>Тем не менее, пример на моках что я вам показал, он именно отсюда взят.
Пример на то и пример, чтобы показать как использовать моки. А в твоём примере было показано как _не надо_ писать код. Я показал как надо писать код при использовании моков. И мой код был вполне вменяемым. Оправданы ли они в данном случае моки — вопрос другой.
P>>>ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.
P>·>Оборачивать каждую стороннюю либу — т.к. её _возможно_ нужно будет заменить — это оверинжиниринг в большинстве случаев.
P>При чем здесь оборачивание? У вас есть функционал — верификация jwt токена.
P>Объясните внятно, почему замена jsonwebtoken на jose должна сломать тесты?
P>А вот если вы замокали jsonwebtoken, то ваши тесты сломаются. И причина только в том, что вы замокали там где этого не надо делать.
В примере никакого jwt токена не было. Я не знаю ты о чём, у тебя где-то в голове какая-то более широкая картина, но к сожалению, я не очень хороший телепат.
P>>>а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)
P>·>А какая разница? Ведь эту конструкцию надо будет тоже каким-то тестом покрыть.
P>Для этого есть интеграционный тест
Жуть. Т.е. ты не можешь что-то протестировать быстрыми тестами, приходится плодить медленные интеграционные и тривиальные ошибки обнаруживать только после запуска всего приложения. Спасибо, не хочу.
P>>>Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.
P>·>То что ты логику в коде куда-то переносишь, не означает, что её вдруг можно не тестировать.
P>Там где логика — там юнит-тесты, без моков, а там где интеграция — там интеграционные тесты, тоже без моков.
Слишком тяжелый подход. Только на мелких приложениях работает.
P>>>Вот-вот, еще чтото надо куда то инжектить, вызывать, проверять, а вызвалось ли замоканое нужное количество раз, в нужной последовательности, итд итд итд
P>·>Это надо не для моков как таковых (если validateThing — тривиальна, то зачем её вообще мочить? Всегда можно запихать реальную имплементацию), а для разделения компонент и независимого их тестирования.
P>А задизайнить компоненты, что бы они не зависели друг от друга?
Это как? То что зависимости неявные — это не означает, что их нет.
P>·>Я тебе уже написал когда это _надо_, но ты как-то тихо проигнорировал: Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.
P>Это ваш выбор дизайна — на самом низу стека вы вдруг вызываете чтение из сети-базы-итд, или начинаете жечь cpu.
P>А если этого не делать, то и моки не нужны.
Ага-ага. Но тогда нужны ещё более медленные и неуклюжие интеграционные тесты, много.
P>>>Итого — вы прибили тесты к реализации
P>·>В js принято валить всё в Global и сплошные синглтоны? DI не слышали?
P>Зачем global? DI можно по разному использовать — речь же об этом. Посмотрите framework as a detail или clean architecture, сразу станет понятнее.
require это нифига не DI, это SL.
P>Вместо выстраивания глубокой иерархии, где на самом низу стека идет чтение из бд, делаем иначе — вычисляем, что читать из бд, следующим шагом выполняем чтение.
P>Итого — самое важное тестируется плотно юнит-тестами, а остальное — интеграционными
P>С вашими моками покрытие будет хуже, кода больше, а интеграционные всё равно нужны
Интеграционные тесты тестируют интеграцию крупных, тяжелых компонент. А у тебя они тестируют всю мелкую функциональность.
P>>>На мой взгляд это не нужно — самый минимум это ожидания вызывающей стороны. Всё остальное это если время некуда девать.
P>·>Так пиши ожидания когда они известны, если ты их знаешь. Анализ вайтбокс, покрытие кода — именно поиск неизвестных ожиданий.
P>·>"Мы тут написали все ожидания по спеке, но вот в тот else почему-то ни разу не попали ни разу. Разбираемся почему."
P>С моками чаще получается так — моками некто заставил попасть в этот else, а на проде этот else срабатывает совсем в другом контексте, и все валится именно там где покрыто тестом.
Очень вряд-ли, но допустим даже что-то вальнулось в проде. Изучив логи прода, ты с моками сможешь быстро воспроизвести сценарий и получить быстрый красный тест, который можно спокойно отлаживать и фиксить, без всякой интеграции.
P>·>Это только для tryAcceptComposition. А таких врапперов придётся написать под каждый метод контроллера. И на каждый пару тестов.
P>Именно! И никакие моки этого не отменяют!
Они отменяют необходимость написания тучи этих самых xxxComposition, нет кода — нечего тестировать.
P>>>Во вторых — моки не избавляют от необхидимости интеграционных тестов, в т.ч. запуска на реальной субд
P>·>Такой тест может быть один на всю систему. Запустилось, соединения установлены, версии совпадают, пинги идут. Готово.
P>Если у вас система из одного единственного роута и без зависимостей входящих-исходящих, то так и будет. А если роутов много, и есть зависимости, то надо интеграционными тестами покрыть весь входящий и исходящий трафик.
Не весь, а только покрывающий интеграцию компонент.
P>>>Вот я и говорю — вы сделали в бл зависимость от бд, а потом в тестах изолируетесь от нее. Можно ж и иначе пойти — изолироваться на этапе дизайна, тогда ничего мокать не надо
P>·>В бл зависимость от репы.
P>А следовательно и от бд. Например, вечная проблема с репозиториями, как туда фильтры передавать и всякие параметры, что бы не плодить сотни методов. Сменили эту механику — и весь код где прокидываются фильтры, издох — начинай переписывать тесты.
Это какая-то специфичная проблема в твоих проектах, я не в курсе.
P>·>Гораздо больше, чем динамическая.
P>Этого недостаточно. Нужен внятный интеграционный тест, его задача — следить что подсистема с правильными зависимостями соответствует ожиданиям
Задача инеграционного теста — тестировать места взаимодействия компонент, а не сценарии поведения.
P>>>тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория
P>·>А какие типы у объектов getUser/updateUser? И примерная реализация всех методов?
P>getUser — чистая функция, выдает объект-реквест, например select * from users where id=? + параметры + трансформации. UpdateUser — чистая функция, вычисляет все что нужно для операции. execute — принимает объект-реквест, возвращает трансформированый результат.
Как ассертить такую функцию в юнит-тесте? Как проверить, что текст запроса хотя бы синтаксически корректен? И как ассертить что параметры куда надо положены и транформация верна?
И далее, ну допустим ты как-то написал кучу тестов что getUser выглядит правильно. А дальше? Как убедиться, что настоящая субд будет работать с getUser именно так, как ты описал в ожиданиях теста репы?
>>> const updateUser = repository.updateUser(oldUser, newUser);
P>·>А если я случайно напишу
P>·>const updateUser = repository.updateUser(newUser, oldUser);
P>·>где что когда упадёт?
P>Упадет интеграционный тест, тест вида "выполним операцию и проверим, что состояние юзера соответствует ожиданиям".
Плохо, это функциональная бизнес-логика, а не интеграция. Это должно ловиться юнит-тестом, а не интеграционным.
P>·>А как контролиоруется что newUser возвращённый из бл обработается так как мы задумали в репе?
P>Метод репозитория тоже тестируется юнитами, без моков — его задача сгенерировать правильный реквест.
P>Проблему может вызвать ветвление в контроллере — он должен быть линейным.
Т.е. нужно писать интеграционный тест как минимум для каждого метода контроллера. Это слишком дохрена.
P>·>У тебя какое-то странное понимание моков. Цель мока не чтобы максимально точно описать поведение мокируемого, а чтобы описать различные возможные сценарии поведения мокируемого, на которые мы реагируем различным способом в sut.
P>Я вам пишу про разные кейсы из того что вижу, а вы все пытаетесь причину во мне найти
Т.е. ты пытаешься своё "Я так вижу" за какой-то объективный факт.
P>·>Т.е. при тестировании контроллера совершенно неважно, что именно делает реальный modifyUser. Важно какие варианты результата он может вернуть и что на них реагируем в sut в соответствии с ожиданиями.
P>А моки вам зачем для этого?
Чтобы описать варианты результата что может вернуть modifyUser.
P>>>А как это QA делают? Строчка требований — список тестов, где она фигурирует.
P>·>А как узнать, что требования покрывают как можно больше возможных сценариев, а не только те, о которых не забыли подумать?
P>Если мы очем то забыли, то никакой кавередж не спасет. Например, если вы забыли, что вам могут приходить данные разной природы, то всё, приплыли.
Это наверное для js актуально, где всё есть Object и где угодно может быть что угодно. При статической типизации вариантов разной природы не так уж много. Если modifyUser возвращает boolean, то тут только два варианта данных. А вот в js оно внезапно может выдать "FileNotFound" — и приехали.
P>>>Это я так трохи пошутил — идея в том, что нам всего то нужен один тест верхнего уровня. он у нас тупой как доска — буквально как http реквест-респонс
P>·>Ага. Но он, в лучшем случае, покроет только один xxxComposition. А их будет на каждый метод контроллера.
P>Это в любом случае так — e2e тесты должны покрыть весь входящий и исходящий трафик.
Вот тут тебе и аукнется цикломатическая сложность. Чтобы покрыть весть трафик — надо будет выполнить все возможные пути выполнения. Что просто дофига и очень долго.
P>>>Много где завезли, в джава коде я такое много где видел. А еще есть сравнение логов — если у вас вдруг появится лишний аудит, он обязательно сломает выхлоп. Это тоже блекбокс
P>·>Это означает, что небольшое изменение в формате аудита ломает все тесты с образцами логов. Вот и представь, добавление небольшой штучки в аудит поломает все твои "несколько штук" тестов на каждый роут.
P>Похоже, вы незнакомы с acceptance driven тестированием. В данном случае нам нужно просмотреть логи, и сделать коммит, если всё хорошо.
Это для игрушечных проектов ещё может только заработать. В более менее реальном проекте этих логов будут десятки тысяч. И просмотреть всё просто нереально. После тысячной строки диффа ты задолбаешься смотреть дальеш и тупо всё закоммитишь, незаметив особый случай в тысячепервой строке.
P>·>Именно. А должно быть несколько штук на весь сервис. Роутов может быть сотни.
P>Тесты верхнего уровня в любом случае должны покрывать весь апи. Вы же своими моками пытаетесь искусственно уровень тестирования понизить.
Нет. Интеграционные тесты должны протестировать интеграцию, а не весь апи.
P>>>Например, ваш бл-сервис неявно зависит от бд-клиента, через репозиторий. А это значит, смена бд сначала сломает ваш репозиторий, а это сломает ваш бл сервис.
P>·>Не понял, что значит "сломает"? Что за такая "смена бд"?
P>Как ваш репозиторий фильтры принимает ?
Не очень знаком с этой терминологией.
P>·>Зачем? В тесте контрллера используется бл, поэтому моки только для него. И что творится в репе с т.з. контроллера — совершенно неважно.
P>Если у вас полная изоляция, то так и будет. А если неполная, что чаще всего, то будет всё сломано.
P>Собственно — что бы добиться этой самой хорошей изоляции в коде девелопера надо тренировать очень долго.
Если изоляция нужна, можно использовать интерфейсы.
P>>> прямых — одна, а непрямых — все дочерние от того же репозитория
P>·>Неясно как "непрямые" зависимости влияют на _дизайн_ класса. Он их вообще никак не видит. Никак.
P>Я вот регулярно вижу такое — контроллер вызывает сервис, передает тому объекты http request response. А вот сервис, чья задача это бл, вычитывает всё из req, res, лезет в базу пополам с репозиторием, и еще собственно бл считает, плюс кеширование итд.
P>То есть, типичный разработчик не умеет пользоваться абстракциями и изоляцией, а пишет прямолинейный низкоуровневый код. И у него нет ни единого шанса протестировать такое чудовище кроме как моками.
Ну ликбез же.
P>>>Ничего странного — вы как то прошли мимо clean architecture.
P>·>Я как-то мимо rest и ui прошел в принципе... если есть какая ссылка "clean architecture for morons", давай.
P>https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Похоже это просто N-ая итерация очередной вариации самой правильной организации трёх слоёв типичного веб-приложения. MVC, MVP, MVI, MVVM, MVVM-C, VIPER, и ещё куча...
Причём тут моки — неясно.
Я вообще в последние N лет редко занимаюсь UI/web. У меня всякая поточка, очереди, сообщения, low/zero gc и прочее. Как там этот getUser сделать с zero gc — тот ещё вопрос.
Моки же общий инструмент, от деталей архитектуры зависит мало.
P>>>Я не знаю, в чем вы проблему видите. В жээс аннотация это обычный код, который инструментируется, и будет светиться или красным, или зеленым
P>·>В js вроде вообще нет аннотаций. Не знаю что именно ты зовёшь аннотацией тогда.
P>
P> @Operation({method: 'PATCH', route:'/users', summary: 'modify user', responseType: User, })
P> modify(@Req() req: RawBodyRequest<User>): Promise<User> {
P> ...
P> }
P>
А что за версия яп? Ну не важно. Это ещё можно простить, это просто маппинг урлов на объекты при отсутствии схемы REST. Как туда запихать аудит? Как аудит-аннотация будет описывать что, куда, когда и как аудитить?
P>·>И это всё юнит-тестится как ты обещал?! expext(Metadata.from(ControllerX, 'tryAccept')).metadata.to.match({...})?
P>Смотря что — валидатор, это юнит-тест. А вот чтото нужна или нет авторизация, это можно переопределить конфигом или переменными окружения.
P>>>Зачем? Есть трассировка и правило — результат аудита можно сравнивать
P>·>Не понял к чему ты это говоришь. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?
P>Кое что надо и в самом методе вызывать, без этого работать не будет.
Именно! И накой тогда аннотация нужна для этого?
P> Видите нужные строчки — аудит работает, выхлоп совпадает с предыдущим забегом — точно все отлично. Не совпадает — точно есть проблемы.
Строчки где видим? Аудит может сообщения куда-нибудь в сокет слать или в бд отдельную писать.
И где валидировать, что для данных сценариев записываются нужная инфа в аудит?
Суть в том, что детали аудита каждой бизнес-операции можно покрывать туевой хучей отдельных юнит-тестов с моками на каждое возможное ожидание. И тут аннотации будут только мешаться. Интеграционный тест же можно написать один — что хоть какая-то бизнес-операция записала хоть какой-то аудит.