Здравствуйте, ·, Вы писали:
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>>Зачем? Есть трассировка и правило — результат аудита можно сравнивать
·>Не понял к чему ты это говоришь. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?
Кое что надо и в самом методе вызывать, без этого работать не будет. Видите нужные строчки — аудит работает, выхлоп совпадает с предыдущим забегом — точно все отлично. Не совпадает — точно есть проблемы.