Re[17]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 09.12.23 19:00
Оценка: +1
Здравствуйте, ·, Вы писали:

P>>Например, нам нужно считать токен для стрима, нас интересует ид класса, ид сущность, версия сущность b секрет.

·>Не очень ясно в чём идея. У тебя есть библотека которая умеет несколько алгоритмов, апдейт по частям с приведением типов, формирование дайджеста в разных форматах. И ты это оборачиваешь в свою обёртку, которая умеет только одно — конкретный алгоритм упакованный в отдельный метод Token.from, который содержит конкретные параметры и т.п. И что в этом такого особенного? Называется частичное применение функции. И причём тут моки?

Это я вам пример привел, для чего вообще люди мокают, когда всё прекрасно тестируется и без моков.

·>Проблема такого подхода в том, что это работает только в тривиальных случаях. Когда в твоём приложении понадобятся много разных комбинаций, то у тебя количество частично применённых функций пойдёт по экспоненте. У тебя получатся ApiTokenAsHex.from, ApiTokenAsBytes.from, ApiTokenAsB64.from, HashedPassword.from, HashedPasswordAsBytes.from и пошло поехало.


А зачем вам эти AsHex, AsBytes? Это же просто преставление. У вас есть токен — это значение. Его можно создать парой-тройкой способов, и сохранить так же. Отсюда ясно, что нет нужды плодить те функции, что вы предложили, а следовательно и тестировать их тоже не нужно.
Всех дел — отделить данные от представления, и количество тестов уменьшается.

P>>Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.

·>Т.е. вынесли кусочек кода в метод и его протестировали отдельно. И? Моки тут причём?

Насколько я понимаю, я вам показываю, как разные люди понимают моки. Вы же всё приписываете лично мне.

·>Но если у тебя оно нужно конкретно в одном месте для etag, то это уже оверкилл.


Вас всё тянет любое высказывание к абсурду свести. Естетсвенно, что городить вагон абстракций, ради того, что бы вычислить етаг не нужно.
При этом и моки тоже не нужны для этого случая.
У нас будет функция getEtag(content) которую можно проверить разными значениями.

Тем не менее, пример на моках что я вам показал, он именно отсюда взят.

P>>ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.

·>Оборачивать каждую стороннюю либу — т.к. её _возможно_ нужно будет заменить — это оверинжиниринг в большинстве случаев.

При чем здесь оборачивание? У вас есть функционал — верификация jwt токена.
Объясните внятно, почему замена jsonwebtoken на jose должна сломать тесты?
А вот если вы замокали jsonwebtoken, то ваши тесты сломаются. И причина только в том, что вы замокали там где этого не надо делать.

P>>А вы непонятно с какой целью хотите что бы тесты сломались.

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

Не надо здесь вообще ничего мокать, и ничего не сломается.

P>>а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)

·>А какая разница? Ведь эту конструкцию надо будет тоже каким-то тестом покрыть.

Для этого есть интеграционный тест

P>>Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.

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

Там где логика — там юнит-тесты, без моков, а там где интеграция — там интеграционные тесты, тоже без моков.

P>>Вот-вот, еще чтото надо куда то инжектить, вызывать, проверять, а вызвалось ли замоканое нужное количество раз, в нужной последовательности, итд итд итд

·>Это надо не для моков как таковых (если validateThing — тривиальна, то зачем её вообще мочить? Всегда можно запихать реальную имплементацию), а для разделения компонент и независимого их тестирования.

А задизайнить компоненты, что бы они не зависели друг от друга?

·>Я тебе уже написал когда это _надо_, но ты как-то тихо проигнорировал: Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.


Это ваш выбор дизайна — на самом низу стека вы вдруг вызываете чтение из сети-базы-итд, или начинаете жечь cpu.
А если этого не делать, то и моки не нужны.

P>>Итого — вы прибили тесты к реализации

·>В js принято валить всё в Global и сплошные синглтоны? DI не слышали?

Зачем global? DI можно по разному использовать — речь же об этом. Посмотрите framework as a detail или clean architecture, сразу станет понятнее.

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

P>>На мой взгляд это не нужно — самый минимум это ожидания вызывающей стороны. Всё остальное это если время некуда девать.

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

С моками чаще получается так — моками некто заставил попасть в этот else, а на проде этот else срабатывает совсем в другом контексте, и все валится именно там где покрыто тестом.

P>>Я только-только поменял работу, к сожалению физически не могу показать типичный пример.

·>Да мне не нужен код с реального проекта. Небольшой пример достаточно.

Я ж вам его и дал, дважды

·>Это только для tryAcceptComposition. А таких врапперов придётся написать под каждый метод контроллера. И на каждый пару тестов.


Именно! И никакие моки этого не отменяют!

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

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

Если у вас система из одного единственного роута и без зависимостей входящих-исходящих, то так и будет. А если роутов много, и есть зависимости, то надо интеграционными тестами покрыть весь входящий и исходящий трафик.

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

·>В бл зависимость от репы.

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

P>>Если у вас много зависимостей — статическая типизация помогает слабовато.

·>Гораздо больше, чем динамическая.

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

P>>тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория

·>А какие типы у объектов getUser/updateUser? И примерная реализация всех методов?

getUser — чистая функция, выдает объект-реквест, например select * from users where id=? + параметры + трансформации. UpdateUser — чистая функция, вычисляет все что нужно для операции. execute — принимает объект-реквест, возвращает трансформированый результат.

>> const updateUser = repository.updateUser(oldUser, newUser);

·>А если я случайно напишу
·>const updateUser = repository.updateUser(newUser, oldUser);
·>где что когда упадёт?

Упадет интеграционный тест, тест вида "выполним операцию и проверим, что состояние юзера соответствует ожиданиям".

·>А как контролиоруется что newUser возвращённый из бл обработается так как мы задумали в репе?


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

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


Я вам пишу про разные кейсы из того что вижу, а вы все пытаетесь причину во мне найти

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


А моки вам зачем для этого?

P>>·>Как? И как оценивать качество покрытия требований/ожиданий?

P>>А как это QA делают? Строчка требований — список тестов, где она фигурирует.
·>А как узнать, что требования покрывают как можно больше возможных сценариев, а не только те, о которых не забыли подумать?

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

P>>Строчки это плохие метрики, т.к. цикломатическая сложность может зашкаливать как угодно

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

Это можно делать и без моков.

P>>Это я так трохи пошутил — идея в том, что нам всего то нужен один тест верхнего уровня. он у нас тупой как доска — буквально как http реквест-респонс

·>Ага. Но он, в лучшем случае, покроет только один xxxComposition. А их будет на каждый метод контроллера.

Это в любом случае так — e2e тесты должны покрыть весь входящий и исходящий трафик.

P>>Много где завезли, в джава коде я такое много где видел. А еще есть сравнение логов — если у вас вдруг появится лишний аудит, он обязательно сломает выхлоп. Это тоже блекбокс

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

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

·>Именно. А должно быть несколько штук на весь сервис. Роутов может быть сотни.


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

P>>Например, ваш бл-сервис неявно зависит от бд-клиента, через репозиторий. А это значит, смена бд сначала сломает ваш репозиторий, а это сломает ваш бл сервис.

·>Не понял, что значит "сломает"? Что за такая "смена бд"?

Как ваш репозиторий фильтры принимает ?

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


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

P>> прямых — одна, а непрямых — все дочерние от того же репозитория

·>Неясно как "непрямые" зависимости влияют на _дизайн_ класса. Он их вообще никак не видит. Никак.

Я вот регулярно вижу такое — контроллер вызывает сервис, передает тому объекты http request response. А вот сервис, чья задача это бл, вычитывает всё из req, res, лезет в базу пополам с репозиторием, и еще собственно бл считает, плюс кеширование итд.
То есть, типичный разработчик не умеет пользоваться абстракциями и изоляцией, а пишет прямолинейный низкоуровневый код. И у него нет ни единого шанса протестировать такое чудовище кроме как моками.

P>>Ничего странного — вы как то прошли мимо clean architecture.

·>Я как-то мимо rest и ui прошел в принципе... если есть какая ссылка "clean architecture for morons", давай.

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

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

·>В js вроде вообще нет аннотаций. Не знаю что именно ты зовёшь аннотацией тогда.

  @Operation({method: 'PATCH', route:'/users', summary: 'modify user', responseType: User, })
  modify(@Req() req: RawBodyRequest<User>): Promise<User> {
  ...
  }


·>И это всё юнит-тестится как ты обещал?! expext(Metadata.from(ControllerX, 'tryAccept')).metadata.to.match({...})?


Смотря что — валидатор, это юнит-тест. А вот чтото нужна или нет авторизация, это можно переопределить конфигом или переменными окружения.

P>>Зачем? Есть трассировка и правило — результат аудита можно сравнивать

·>Не понял к чему ты это говоришь. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?

Кое что надо и в самом методе вызывать, без этого работать не будет. Видите нужные строчки — аудит работает, выхлоп совпадает с предыдущим забегом — точно все отлично. Не совпадает — точно есть проблемы.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.