Сообщение Re[16]: Что такое Dependency Rejection от 06.12.2023 0:19
Изменено 06.12.2023 9:21 ·
Re[16]: Что такое Dependency Rejection
Здравствуйте, Pauel, Вы писали:
P>·>Мде... Или это "особенности" js?.. Но допустим. Как ты это предлагаешь переделать с "value"?
В общем я плохо знаю что есть в js, но тот код в jest действительно туфта какая-то, не надо так делать. Если не задаваться вопросом, зачем вообще вдруг понадобилось мокать hmac, то подобное с сохранением возможностей api библиотеки crypto в java будет выглядеть как-то так:
P>Например, нам нужно считать токен для стрима, нас интересует ид класса, ид сущность, версия сущность b секрет.
Не очень ясно в чём идея. У тебя есть библотека которая умеет несколько алгоритмов, апдейт по частям с приведением типов, формирование дайджеста в разных форматах. И ты это оборачиваешь в свою обёртку, которая умеет только одно — конкретный алгоритм упакованный в отдельный метод Token.from, который содержит конкретные параметры и т.п. И что в этом такого особенного? Называется частичное применение функции. И причём тут моки?
Проблема такого подхода в том, что это работает только в тривиальных случаях. Когда в твоём приложении понадобятся много разных комбинаций, то у тебя количество частично применённых функций пойдёт по экспоненте. У тебя получатся ApiTokenAsHex.from, ApiTokenAsBytes.from, ApiTokenAsB64.from, HashedPassword.from, HashedPasswordAsBytes.from и пошло поехало.
И это только _два_ метода из целой библиотеки crypto уже даёт кучу комбинаций.
P>
P>Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.
Т.е. вынесли кусочек кода в метод и его протестировали отдельно. И? Моки тут причём?
P>И теперь в приложении вам не надо мокать Token.from, т.к. вы знаете, что она соответствует нашим ожиданиям, и не надо проверять, что контроллер обновления стрима вызывает Token.from если ему необходимо отрендерить ETag.
Ты изобрёл функции? Конечно, если у тебя из нескольких мест используется конкретно sha256 hex hmac, то это имеет смысл вынести в отдельный переиспользуемый метод. Но если у тебя оно нужно конкретно в одном месте для etag, то это уже оверкилл.
В этом случае тогда непонятно как изменение формата etag может все тесты сделать красными. Могу объяснить лишь тем как ты дизайнишь тесты. Потому что "обязательно сломает выхлоп"? Потому что тестов много, проверяют они очень много. И изменения бьют по многим целям одновременно.
P>Все что вам необходимо — в интеграционны тестах вызвать тот самый контроллер и убедиться, что его значения совпадают с нашими.
Я о чём и говорю — пишешь обёртки стороннего кода. Моки тут вообще никаким боком.
P>>>Ну да — проблемы нет а тесты стали красными. И так каждый раз.
P>·>Ничего не понял. Если использование уязвимой тормозной либы — не проблема, то зачем вообще код трогать?
P>ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.
Оборачивать каждую стороннюю либу — т.к. её _возможно_ нужно будет заменить — это оверинжиниринг в большинстве случаев.
P>А вы непонятно с какой целью хотите что бы тесты сломались.
P>Что нового вы добавите в приложение починкой этих тестов?
Нет, я пытаюсь понять причём тут моки. Ты с таким же успехом можешь оборачивать либу своей обёрткой и свою обёртку мочить.
P>а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)
А какая разница? Ведь эту конструкцию надо будет тоже каким-то тестом покрыть.
P>Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.
То что ты логику в коде куда-то переносишь, не означает, что её вдруг можно не тестировать.
P>·>Создаётся мок для класса Thing.
P>·>Дальше мокаешь методы и инжектишь в SUT.
P>Вот-вот, еще чтото надо куда то инжектить, вызывать, проверять, а вызвалось ли замоканое нужное количество раз, в нужной последовательности, итд итд итд
Это надо не для моков как таковых (если validateThing — тривиальна, то зачем её вообще мочить? Всегда можно запихать реальную имплементацию), а для разделения компонент и независимого их тестирования.
Я тебе уже написал когда это _надо_, но ты как-то тихо проигнорировал: Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.
То что это _надо_ именно для моков — это ты сам придумал. Тут зависимость в противоположную сторону — есть проблема использовать какой-то кусок из тестов, т.к. он делает слишком много, мы описываем только интересные нам аспекты поведения куска кода и фиксируем их в виде моков.
P>Итого — вы прибили тесты к реализации
В js принято валить всё в Global и сплошные синглтоны? DI не слышали?
P>>>Вы там в своём уме? Чтобы замокать чего нибудь ради теста, вам нужно знание о реализации, следовательно, тесткейс автоматически становится вайтбоксом.
P>·>В общем случае: чтобы передать что-то методу для теста, тоже надо знание о реализации, иначе не узнаешь corner-case.
P>На мой взгляд это не нужно — самый минимум это ожидания вызывающей стороны. Всё остальное это если время некуда девать.
Так пиши ожидания когда они известны, если ты их знаешь. Анализ вайтбокс, покрытие кода — именно поиск неизвестных ожиданий.
"Мы тут написали все ожидания по спеке, но вот в тот else почему-то ни разу не попали ни разу. Разбираемся почему."
P>·>Я понимаю код лучше, чем твои объяснения.
P>Я только-только поменял работу, к сожалению физически не могу показать типичный пример.
Да мне не нужен код с реального проекта. Небольшой пример достаточно.
P>·>Важно то, что в этом tryAcceptComposition находится connectionString. Т.е. условно говоря твой "интеграционнный тест" полезет в реальную субд и будет безбожно тормозить.
P>Во первых — таких тестов у нас будет пару штук
Это только для tryAcceptComposition. А таких врапперов придётся написать под каждый метод контроллера. И на каждый пару тестов.
P>Во вторых — моки не избавляют от необхидимости интеграционных тестов, в т.ч. запуска на реальной субд
Такой тест может быть один на всю систему. Запустилось, соединения установлены, версии совпадают, пинги идут. Готово.
>>В коде с моками — был reservationsRepository и возможность отмежеваться от реальной субд чтобы тестировать что параметры поиска и создания резерваций верные
P>Вот я и говорю — вы сделали в бл зависимость от бд, а потом в тестах изолируетесь от нее. Можно ж и иначе пойти — изолироваться на этапе дизайна, тогда ничего мокать не надо
В бл зависимость от репы.
P>>>Вы идете наоброт — выпихиваете всё в контроллер, тогда дешевыми юнит-тестами покрывать нечего, зато теперь надо контроллер обкладывать моками, чтобы хоть как то протестировать.
P>·>В некоторых случаях интеграционным тестом будет "код скомпилировался".
P>Если у вас много зависимостей — статическая типизация помогает слабовато.
Гораздо больше, чем динамическая.
P>Есть другой вариант — не вы передаете репозиторий в bl, а вызываете bl внутри т.н. use-case в терминологии clean architecture
P>тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория
А какие типы у объектов getUser/updateUser? И примерная реализация всех методов?
> const updateUser = repository.updateUser(oldUser, newUser);
А если я случайно напишу
const updateUser = repository.updateUser(newUser, oldUser);
где что когда упадёт?
P>·>Для юнит-теста Controller надо мокать только BlService.
P>вот это и проблема — у вас тест "как написано", т.к. далеко не факт, что ваш мок этого бл-сервиса соответствует реальному блсервису. Эта часть вообще никак не контролируется — и крайне трудно научить этому разработчикв.
А как контролиоруется что newUser возвращённый из бл обработается так как мы задумали в репе?
У тебя какое-то странное понимание моков. Цель мока не чтобы максимально точно описать поведение мокируемого, а чтобы описать различные возможные сценарии поведения мокируемого, на которые мы реагируем различным способом в sut.
P>Типичная проблема — моки крайне слабо напоминают тот самый класс.
Не очень понимаю проблему. Что можно нереального сделать в моке bl.modifyUser(id, newUserData) не так? Суть в том, что контроллер зовёт modifyUser и конвертирует результат:
Т.е. при тестировании контроллера совершенно неважно, что именно делает реальный modifyUser. Важно какие варианты результата он может вернуть и что на них реагируем в sut в соответствии с ожиданиями.
P>>>Совсем необязательно. Тесты это далеко не единственный инструмент в обеспечении качества. Потому покрывать код идея изначально утопичная. Покрывать нужно требования, ожидания вызывающей стороны, а не код.
P>·>Как? И как оценивать качество покрытия требований/ожиданий?
P>А как это QA делают? Строчка требований — список тестов, где она фигурирует.
А как узнать, что требования покрывают как можно больше возможных сценариев, а не только те, о которых не забыли подумать?
P>>>Для самого кода у нас много разных инструментов — от генерации по мат-модели, всяких инвариантов-пред-пост-условий и до код-ревью.
P>·>А метрики какие? Чтобы качество обеспечивать, его надо как-то измерять и контролировать.
P>Строчки это плохие метрики, т.к. цикломатическая сложность может зашкаливать как угодно
Чтобы она не зашкаливала — разделяют на части и мокают. Тогда как бы ни повёл себя одна часть, другая знает что делать.
P>Это я так трохи пошутил — идея в том, что нам всего то нужен один тест верхнего уровня. он у нас тупой как доска — буквально как http реквест-респонс
Ага. Но он, в лучшем случае, покроет только один xxxComposition. А их будет на каждый метод контроллера.
P>>>Постусловия к вам еще не завезли? Это блакбокс, только это совсем не тест.
P>·>А куда их завезли?
P>Много где завезли, в джава коде я такое много где видел. А еще есть сравнение логов — если у вас вдруг появится лишний аудит, он обязательно сломает выхлоп. Это тоже блекбокс
Это означает, что небольшое изменение в формате аудита ломает все тесты с образцами логов. Вот и представь, добавление небольшой штучки в аудит поломает все твои "несколько штук" тестов на каждый роут.
P>·>Это слишком тяжелый тест. Количество таких тестов должно быть минимально.
P>Разумеется — таких у нас по несколько штук на каждый роут
Именно. А должно быть несколько штук на весь сервис. Роутов может быть сотни. А достаточно потрогать каждое соединение между зависимостями. Например, если MaitreD может успешно дёрнуть метод getReservation у Repository, то это автоматически означает репо работает, что можно дёргать любые методы этого же Repository из любого метода MaitreD. Поэтому тяжелые и-тесты для find/create/delete/modify/etc уже не нужны.
P>>>Вы же моками обрезаете зависимости. Забыли? Каждая зависимость = причины для изменений.
P>·>Я не понял на какой вопрос этот ответ.
P>Вы спрашивали, почему короткая функция более устойчива по сравнению с maitred — потому, что у ней зависимостей меньше. Каждая зависимость, прямая или нет, это причина для изменения.
Я не о конкретной функции рассуждаю, а о системе. Если у неё зависимостей меньше, в ней меньше логики. А это значит, что эта логика была перенесена куда-то. И теперь это что-то тоже приходится тестировать. И плюс ещё тестировать что это что-то соединяется с этой функцией верно.
В этом у меня и основная претензия к статье. Был код, который делал X. Его заменили на "чистую красивую маленькую функцию", которая делает Y и теперь её легче тестировать. Но факт, что Y<X — замели под коврик. А эту разницу X-Y — запихали в страхолюдный xxxComposition метод и сделали вид, что он скучный и неинтересный, и можно его даже не тестировать.
P>Например, ваш бл-сервис неявно зависит от бд-клиента, через репозиторий. А это значит, смена бд сначала сломает ваш репозиторий, а это сломает ваш бл сервис.
Не понял, что значит "сломает"? Что за такая "смена бд"?
P>А раз вы его замокали для теста контроллера, то вам надо перебулшитить все тесты
Зачем? В тесте контрллера используется бл, поэтому моки только для него. И что творится в репе с т.з. контроллера — совершенно неважно.
P>>>Вот с maitred все не просто — любое изменение дизайна затрагивает такой класс, тк у него куча зависимостей, каждая из которых может меняться по разными причнам.
P>·>Куча это сколько? Судя по коду в статье я там аж целых одну насчитал.
P>
прямых — одна, а непрямых — все дочерние от того же репозитория
Неясно как "непрямые" зависимости влияют на _дизайн_ класса. Он их вообще никак не видит. Никак. Вообще. Ок... Есть, конечно, вероятность, что т.к. класс есть в скопе, то кто-то в конструктор MaitreD впишет Db и connectionString. От такого можно отмежеваться интерфейсами и разделением на модули, но обычно оверкилл — проще на ревью такое резать.
P>>>И вам каждый раз надо будет подфикшивать тесты.
P>·>По-моему ты очень странно пишешь тесты и дизайнишь, от того и проблемы.
P>Ничего странного — вы как то прошли мимо clean architecture.
Я как-то мимо rest и ui прошел в принципе... если есть какая ссылка "clean architecture for morons", давай.
P>>>Тогда не совсем понятен ваш вопрос.
P>·>https://en.wikipedia.org/wiki/Code_coverage
P>Я не знаю, в чем вы проблему видите. В жээс аннотация это обычный код, который инструментируется, и будет светиться или красным, или зеленым
В js вроде вообще нет аннотаций. Не знаю что именно ты зовёшь аннотацией тогда.
P>>>·>Самое что ни на есть вайтбокс тестирование. Что конкретный метод содержит конкретную аннотацию.
P>>>Мы тестируем, что есть те или иные метаданные. А вот чем вы их установите — вообще дело десятое.
P>·>А как ещё на метод можно метаданные навесить? Или ты про js?
P>разными способами
P>1. аннотации метода
P>2. аннотации класса
P>3. аннотации контекста класса
P>4. скриптованием, например, на старте
P>5. конфигурация приложения
P>6. переменные окружения
И это всё юнит-тестится как ты обещал?!
P>>>Мне нужно будет добавить функцию которая будет возвращать экземпляр аудитора по метаданным и покрыть её простыми дешовыми тестами. А раз весь аудит проходить будет через неё, то достаточно сверху накинуть интеграционный тест, 1шт
P>·>Тебе как-то рефлексивно придётся извлекать детали аудита из места вызова и параметров метода. На ЯП со статической типизацией будет полный швах.
P>Зачем? Есть трассировка и правило — результат аудита можно сравнивать
Не понял. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?
P>·>Мде... Или это "особенности" js?.. Но допустим. Как ты это предлагаешь переделать с "value"?
В общем я плохо знаю что есть в js, но тот код в jest действительно туфта какая-то, не надо так делать. Если не задаваться вопросом, зачем вообще вдруг понадобилось мокать hmac, то подобное с сохранением возможностей api библиотеки crypto в java будет выглядеть как-то так:
@Test void shouldVerify()
{
// deps
var console = mock(Console.class);
var crypto = mock(Crypto.class);
var hmac = mock(Hmac.class);
// set up
when(crypto.createHmac("sha256", "API_TOKEN")).thenReturn(hmac);//factory pattern
when(hmac.digest("hex")).thenReturn("123");
//sut
var app = new App(console, crypto);// dependency injection
app.main();
//verify
verify(hmac).update("{}");
verify(console).log("verified successfully. Implement next logic");
//тут ещё не хватает проверок, что "123" где-то правильно использовалось в результатах.
}P>Например, нам нужно считать токен для стрима, нас интересует ид класса, ид сущность, версия сущность b секрет.
Не очень ясно в чём идея. У тебя есть библотека которая умеет несколько алгоритмов, апдейт по частям с приведением типов, формирование дайджеста в разных форматах. И ты это оборачиваешь в свою обёртку, которая умеет только одно — конкретный алгоритм упакованный в отдельный метод Token.from, который содержит конкретные параметры и т.п. И что в этом такого особенного? Называется частичное применение функции. И причём тут моки?
Проблема такого подхода в том, что это работает только в тривиальных случаях. Когда в твоём приложении понадобятся много разных комбинаций, то у тебя количество частично применённых функций пойдёт по экспоненте. У тебя получатся ApiTokenAsHex.from, ApiTokenAsBytes.from, ApiTokenAsB64.from, HashedPassword.from, HashedPasswordAsBytes.from и пошло поехало.
И это только _два_ метода из целой библиотеки crypto уже даёт кучу комбинаций.
P>
P>const stream = Stream.from({Sample.id, Sample.version, Sample.content});
P>expect(Token.from(stream, Sample.secret)).to.eq(Sample.sha256);
P>P>Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.
Т.е. вынесли кусочек кода в метод и его протестировали отдельно. И? Моки тут причём?
P>И теперь в приложении вам не надо мокать Token.from, т.к. вы знаете, что она соответствует нашим ожиданиям, и не надо проверять, что контроллер обновления стрима вызывает Token.from если ему необходимо отрендерить ETag.
В этом случае тогда непонятно как изменение формата etag может все тесты сделать красными. Могу объяснить лишь тем как ты дизайнишь тесты. Потому что "обязательно сломает выхлоп"? Потому что тестов много, проверяют они очень много. И изменения бьют по многим целям одновременно.
P>Все что вам необходимо — в интеграционны тестах вызвать тот самый контроллер и убедиться, что его значения совпадают с нашими.
Я о чём и говорю — пишешь обёртки стороннего кода. Моки тут вообще никаким боком.
P>>>Ну да — проблемы нет а тесты стали красными. И так каждый раз.
P>·>Ничего не понял. Если использование уязвимой тормозной либы — не проблема, то зачем вообще код трогать?
P>ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.
Оборачивать каждую стороннюю либу — т.к. её _возможно_ нужно будет заменить — это оверинжиниринг в большинстве случаев.
P>А вы непонятно с какой целью хотите что бы тесты сломались.
P>Что нового вы добавите в приложение починкой этих тестов?
Нет, я пытаюсь понять причём тут моки. Ты с таким же успехом можешь оборачивать либу своей обёрткой и свою обёртку мочить.
P>а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)
А какая разница? Ведь эту конструкцию надо будет тоже каким-то тестом покрыть.
P>Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.
То что ты логику в коде куда-то переносишь, не означает, что её вдруг можно не тестировать.
P>·>Создаётся мок для класса Thing.
P>·>Дальше мокаешь методы и инжектишь в SUT.
P>Вот-вот, еще чтото надо куда то инжектить, вызывать, проверять, а вызвалось ли замоканое нужное количество раз, в нужной последовательности, итд итд итд
Это надо не для моков как таковых (если validateThing — тривиальна, то зачем её вообще мочить? Всегда можно запихать реальную имплементацию), а для разделения компонент и независимого их тестирования.
Я тебе уже написал когда это _надо_, но ты как-то тихо проигнорировал: Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.
То что это _надо_ именно для моков — это ты сам придумал. Тут зависимость в противоположную сторону — есть проблема использовать какой-то кусок из тестов, т.к. он делает слишком много, мы описываем только интересные нам аспекты поведения куска кода и фиксируем их в виде моков.
P>Итого — вы прибили тесты к реализации
В js принято валить всё в Global и сплошные синглтоны? DI не слышали?
P>>>Вы там в своём уме? Чтобы замокать чего нибудь ради теста, вам нужно знание о реализации, следовательно, тесткейс автоматически становится вайтбоксом.
P>·>В общем случае: чтобы передать что-то методу для теста, тоже надо знание о реализации, иначе не узнаешь corner-case.
P>На мой взгляд это не нужно — самый минимум это ожидания вызывающей стороны. Всё остальное это если время некуда девать.
Так пиши ожидания когда они известны, если ты их знаешь. Анализ вайтбокс, покрытие кода — именно поиск неизвестных ожиданий.
"Мы тут написали все ожидания по спеке, но вот в тот else почему-то ни разу не попали ни разу. Разбираемся почему."
P>·>Я понимаю код лучше, чем твои объяснения.
P>Я только-только поменял работу, к сожалению физически не могу показать типичный пример.
Да мне не нужен код с реального проекта. Небольшой пример достаточно.
P>·>Важно то, что в этом tryAcceptComposition находится connectionString. Т.е. условно говоря твой "интеграционнный тест" полезет в реальную субд и будет безбожно тормозить.
P>Во первых — таких тестов у нас будет пару штук
Это только для tryAcceptComposition. А таких врапперов придётся написать под каждый метод контроллера. И на каждый пару тестов.
P>Во вторых — моки не избавляют от необхидимости интеграционных тестов, в т.ч. запуска на реальной субд
Такой тест может быть один на всю систему. Запустилось, соединения установлены, версии совпадают, пинги идут. Готово.
>>В коде с моками — был reservationsRepository и возможность отмежеваться от реальной субд чтобы тестировать что параметры поиска и создания резерваций верные
P>Вот я и говорю — вы сделали в бл зависимость от бд, а потом в тестах изолируетесь от нее. Можно ж и иначе пойти — изолироваться на этапе дизайна, тогда ничего мокать не надо
В бл зависимость от репы.
P>>>Вы идете наоброт — выпихиваете всё в контроллер, тогда дешевыми юнит-тестами покрывать нечего, зато теперь надо контроллер обкладывать моками, чтобы хоть как то протестировать.
P>·>В некоторых случаях интеграционным тестом будет "код скомпилировался".
P>Если у вас много зависимостей — статическая типизация помогает слабовато.
Гораздо больше, чем динамическая.
P>Есть другой вариант — не вы передаете репозиторий в bl, а вызываете bl внутри т.н. use-case в терминологии clean architecture
P>тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория
А какие типы у объектов getUser/updateUser? И примерная реализация всех методов?
> const updateUser = repository.updateUser(oldUser, newUser);
А если я случайно напишу
const updateUser = repository.updateUser(newUser, oldUser);
где что когда упадёт?
P>·>Для юнит-теста Controller надо мокать только BlService.
P>вот это и проблема — у вас тест "как написано", т.к. далеко не факт, что ваш мок этого бл-сервиса соответствует реальному блсервису. Эта часть вообще никак не контролируется — и крайне трудно научить этому разработчикв.
А как контролиоруется что newUser возвращённый из бл обработается так как мы задумали в репе?
У тебя какое-то странное понимание моков. Цель мока не чтобы максимально точно описать поведение мокируемого, а чтобы описать различные возможные сценарии поведения мокируемого, на которые мы реагируем различным способом в sut.
P>Типичная проблема — моки крайне слабо напоминают тот самый класс.
Не очень понимаю проблему. Что можно нереального сделать в моке bl.modifyUser(id, newUserData) не так? Суть в том, что контроллер зовёт modifyUser и конвертирует результат:
class UserController {
Response update(Request r) {
var id = extractId(r);
var data = extractData(r);
var result = bl.modifyUser(id, data);
return result.isSuccess ? Response(r.correlationId, prepare(result)) : Failure("sorry");
}
}Т.е. при тестировании контроллера совершенно неважно, что именно делает реальный modifyUser. Важно какие варианты результата он может вернуть и что на них реагируем в sut в соответствии с ожиданиями.
P>>>Совсем необязательно. Тесты это далеко не единственный инструмент в обеспечении качества. Потому покрывать код идея изначально утопичная. Покрывать нужно требования, ожидания вызывающей стороны, а не код.
P>·>Как? И как оценивать качество покрытия требований/ожиданий?
P>А как это QA делают? Строчка требований — список тестов, где она фигурирует.
А как узнать, что требования покрывают как можно больше возможных сценариев, а не только те, о которых не забыли подумать?
P>>>Для самого кода у нас много разных инструментов — от генерации по мат-модели, всяких инвариантов-пред-пост-условий и до код-ревью.
P>·>А метрики какие? Чтобы качество обеспечивать, его надо как-то измерять и контролировать.
P>Строчки это плохие метрики, т.к. цикломатическая сложность может зашкаливать как угодно
Чтобы она не зашкаливала — разделяют на части и мокают. Тогда как бы ни повёл себя одна часть, другая знает что делать.
P>Это я так трохи пошутил — идея в том, что нам всего то нужен один тест верхнего уровня. он у нас тупой как доска — буквально как http реквест-респонс
Ага. Но он, в лучшем случае, покроет только один xxxComposition. А их будет на каждый метод контроллера.
P>>>Постусловия к вам еще не завезли? Это блакбокс, только это совсем не тест.
P>·>А куда их завезли?
P>Много где завезли, в джава коде я такое много где видел. А еще есть сравнение логов — если у вас вдруг появится лишний аудит, он обязательно сломает выхлоп. Это тоже блекбокс
Это означает, что небольшое изменение в формате аудита ломает все тесты с образцами логов. Вот и представь, добавление небольшой штучки в аудит поломает все твои "несколько штук" тестов на каждый роут.
P>·>Это слишком тяжелый тест. Количество таких тестов должно быть минимально.
P>Разумеется — таких у нас по несколько штук на каждый роут
Именно. А должно быть несколько штук на весь сервис. Роутов может быть сотни. А достаточно потрогать каждое соединение между зависимостями. Например, если MaitreD может успешно дёрнуть метод getReservation у Repository, то это автоматически означает репо работает, что можно дёргать любые методы этого же Repository из любого метода MaitreD. Поэтому тяжелые и-тесты для find/create/delete/modify/etc уже не нужны.
P>>>Вы же моками обрезаете зависимости. Забыли? Каждая зависимость = причины для изменений.
P>·>Я не понял на какой вопрос этот ответ.
P>Вы спрашивали, почему короткая функция более устойчива по сравнению с maitred — потому, что у ней зависимостей меньше. Каждая зависимость, прямая или нет, это причина для изменения.
Я не о конкретной функции рассуждаю, а о системе. Если у неё зависимостей меньше, в ней меньше логики. А это значит, что эта логика была перенесена куда-то. И теперь это что-то тоже приходится тестировать. И плюс ещё тестировать что это что-то соединяется с этой функцией верно.
В этом у меня и основная претензия к статье. Был код, который делал X. Его заменили на "чистую красивую маленькую функцию", которая делает Y и теперь её легче тестировать. Но факт, что Y<X — замели под коврик. А эту разницу X-Y — запихали в страхолюдный xxxComposition метод и сделали вид, что он скучный и неинтересный, и можно его даже не тестировать.
P>Например, ваш бл-сервис неявно зависит от бд-клиента, через репозиторий. А это значит, смена бд сначала сломает ваш репозиторий, а это сломает ваш бл сервис.
Не понял, что значит "сломает"? Что за такая "смена бд"?
P>А раз вы его замокали для теста контроллера, то вам надо перебулшитить все тесты
Зачем? В тесте контрллера используется бл, поэтому моки только для него. И что творится в репе с т.з. контроллера — совершенно неважно.
P>>>Вот с maitred все не просто — любое изменение дизайна затрагивает такой класс, тк у него куча зависимостей, каждая из которых может меняться по разными причнам.
P>·>Куча это сколько? Судя по коду в статье я там аж целых одну насчитал.
P>
Неясно как "непрямые" зависимости влияют на _дизайн_ класса. Он их вообще никак не видит. Никак. Вообще. Ок... Есть, конечно, вероятность, что т.к. класс есть в скопе, то кто-то в конструктор MaitreD впишет Db и connectionString. От такого можно отмежеваться интерфейсами и разделением на модули, но обычно оверкилл — проще на ревью такое резать.
P>>>И вам каждый раз надо будет подфикшивать тесты.
P>·>По-моему ты очень странно пишешь тесты и дизайнишь, от того и проблемы.
P>Ничего странного — вы как то прошли мимо clean architecture.
Я как-то мимо rest и ui прошел в принципе... если есть какая ссылка "clean architecture for morons", давай.
P>>>Тогда не совсем понятен ваш вопрос.
P>·>https://en.wikipedia.org/wiki/Code_coverage
P>Я не знаю, в чем вы проблему видите. В жээс аннотация это обычный код, который инструментируется, и будет светиться или красным, или зеленым
В js вроде вообще нет аннотаций. Не знаю что именно ты зовёшь аннотацией тогда.
P>>>·>Самое что ни на есть вайтбокс тестирование. Что конкретный метод содержит конкретную аннотацию.
P>>>Мы тестируем, что есть те или иные метаданные. А вот чем вы их установите — вообще дело десятое.
P>·>А как ещё на метод можно метаданные навесить? Или ты про js?
P>разными способами
P>1. аннотации метода
P>2. аннотации класса
P>3. аннотации контекста класса
P>4. скриптованием, например, на старте
P>5. конфигурация приложения
P>6. переменные окружения
И это всё юнит-тестится как ты обещал?!
P>>>Мне нужно будет добавить функцию которая будет возвращать экземпляр аудитора по метаданным и покрыть её простыми дешовыми тестами. А раз весь аудит проходить будет через неё, то достаточно сверху накинуть интеграционный тест, 1шт
P>·>Тебе как-то рефлексивно придётся извлекать детали аудита из места вызова и параметров метода. На ЯП со статической типизацией будет полный швах.
P>Зачем? Есть трассировка и правило — результат аудита можно сравнивать
Не понял. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?
Re[16]: Что такое Dependency Rejection
Здравствуйте, Pauel, Вы писали:
P>·>Мде... Или это "особенности" js?.. Но допустим. Как ты это предлагаешь переделать с "value"?
В общем я плохо знаю что есть в js, но тот код в jest действительно туфта какая-то, не надо так делать. Если не задаваться вопросом, зачем вообще вдруг понадобилось мокать hmac, то подобное с сохранением возможностей api библиотеки crypto в java будет выглядеть как-то так:
P>Например, нам нужно считать токен для стрима, нас интересует ид класса, ид сущность, версия сущность b секрет.
Не очень ясно в чём идея. У тебя есть библотека которая умеет несколько алгоритмов, апдейт по частям с приведением типов, формирование дайджеста в разных форматах. И ты это оборачиваешь в свою обёртку, которая умеет только одно — конкретный алгоритм упакованный в отдельный метод Token.from, который содержит конкретные параметры и т.п. И что в этом такого особенного? Называется частичное применение функции. И причём тут моки?
Проблема такого подхода в том, что это работает только в тривиальных случаях. Когда в твоём приложении понадобятся много разных комбинаций, то у тебя количество частично применённых функций пойдёт по экспоненте. У тебя получатся ApiTokenAsHex.from, ApiTokenAsBytes.from, ApiTokenAsB64.from, HashedPassword.from, HashedPasswordAsBytes.from и пошло поехало.
И это только _два_ метода из целой библиотеки crypto уже даёт кучу комбинаций.
P>
P>Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.
Т.е. вынесли кусочек кода в метод и его протестировали отдельно. И? Моки тут причём?
P>И теперь в приложении вам не надо мокать Token.from, т.к. вы знаете, что она соответствует нашим ожиданиям, и не надо проверять, что контроллер обновления стрима вызывает Token.from если ему необходимо отрендерить ETag.
Ты изобрёл функции? Конечно, если у тебя из нескольких мест используется конкретно sha256 hex hmac, то это имеет смысл вынести в отдельный переиспользуемый метод. Но если у тебя оно нужно конкретно в одном месте для etag, то это уже оверкилл.
В этом случае тогда непонятно как изменение формата etag может все тесты сделать красными. Могу объяснить лишь тем как ты дизайнишь тесты. Потому что "обязательно сломает выхлоп"? Потому что тестов много, проверяют они очень много. И изменения бьют по многим целям одновременно.
P>Все что вам необходимо — в интеграционны тестах вызвать тот самый контроллер и убедиться, что его значения совпадают с нашими.
Я о чём и говорю — пишешь обёртки стороннего кода. Моки тут вообще никаким боком.
P>>>Ну да — проблемы нет а тесты стали красными. И так каждый раз.
P>·>Ничего не понял. Если использование уязвимой тормозной либы — не проблема, то зачем вообще код трогать?
P>ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.
Оборачивать каждую стороннюю либу — т.к. её _возможно_ нужно будет заменить — это оверинжиниринг в большинстве случаев.
P>А вы непонятно с какой целью хотите что бы тесты сломались.
P>Что нового вы добавите в приложение починкой этих тестов?
Нет, я пытаюсь понять причём тут моки. Ты с таким же успехом можешь оборачивать либу своей обёрткой и свою обёртку мочить.
P>а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)
А какая разница? Ведь эту конструкцию надо будет тоже каким-то тестом покрыть.
P>Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.
То что ты логику в коде куда-то переносишь, не означает, что её вдруг можно не тестировать.
P>·>Создаётся мок для класса Thing.
P>·>Дальше мокаешь методы и инжектишь в SUT.
P>Вот-вот, еще чтото надо куда то инжектить, вызывать, проверять, а вызвалось ли замоканое нужное количество раз, в нужной последовательности, итд итд итд
Это надо не для моков как таковых (если validateThing — тривиальна, то зачем её вообще мочить? Всегда можно запихать реальную имплементацию), а для разделения компонент и независимого их тестирования.
Я тебе уже написал когда это _надо_, но ты как-то тихо проигнорировал: Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.
То что это _надо_ именно для моков — это ты сам придумал. Тут зависимость в противоположную сторону — есть проблема использовать какой-то кусок из тестов, т.к. он делает слишком много, мы описываем только интересные нам аспекты поведения куска кода и фиксируем их в виде моков.
P>Итого — вы прибили тесты к реализации
В js принято валить всё в Global и сплошные синглтоны? DI не слышали?
P>>>Вы там в своём уме? Чтобы замокать чего нибудь ради теста, вам нужно знание о реализации, следовательно, тесткейс автоматически становится вайтбоксом.
P>·>В общем случае: чтобы передать что-то методу для теста, тоже надо знание о реализации, иначе не узнаешь corner-case.
P>На мой взгляд это не нужно — самый минимум это ожидания вызывающей стороны. Всё остальное это если время некуда девать.
Так пиши ожидания когда они известны, если ты их знаешь. Анализ вайтбокс, покрытие кода — именно поиск неизвестных ожиданий.
"Мы тут написали все ожидания по спеке, но вот в тот else почему-то ни разу не попали ни разу. Разбираемся почему."
P>·>Я понимаю код лучше, чем твои объяснения.
P>Я только-только поменял работу, к сожалению физически не могу показать типичный пример.
Да мне не нужен код с реального проекта. Небольшой пример достаточно.
P>·>Важно то, что в этом tryAcceptComposition находится connectionString. Т.е. условно говоря твой "интеграционнный тест" полезет в реальную субд и будет безбожно тормозить.
P>Во первых — таких тестов у нас будет пару штук
Это только для tryAcceptComposition. А таких врапперов придётся написать под каждый метод контроллера. И на каждый пару тестов.
P>Во вторых — моки не избавляют от необхидимости интеграционных тестов, в т.ч. запуска на реальной субд
Такой тест может быть один на всю систему. Запустилось, соединения установлены, версии совпадают, пинги идут. Готово.
>>В коде с моками — был reservationsRepository и возможность отмежеваться от реальной субд чтобы тестировать что параметры поиска и создания резерваций верные
P>Вот я и говорю — вы сделали в бл зависимость от бд, а потом в тестах изолируетесь от нее. Можно ж и иначе пойти — изолироваться на этапе дизайна, тогда ничего мокать не надо
В бл зависимость от репы.
P>>>Вы идете наоброт — выпихиваете всё в контроллер, тогда дешевыми юнит-тестами покрывать нечего, зато теперь надо контроллер обкладывать моками, чтобы хоть как то протестировать.
P>·>В некоторых случаях интеграционным тестом будет "код скомпилировался".
P>Если у вас много зависимостей — статическая типизация помогает слабовато.
Гораздо больше, чем динамическая.
P>Есть другой вариант — не вы передаете репозиторий в bl, а вызываете bl внутри т.н. use-case в терминологии clean architecture
P>тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория
А какие типы у объектов getUser/updateUser? И примерная реализация всех методов?
> const updateUser = repository.updateUser(oldUser, newUser);
А если я случайно напишу
const updateUser = repository.updateUser(newUser, oldUser);
где что когда упадёт?
P>·>Для юнит-теста Controller надо мокать только BlService.
P>вот это и проблема — у вас тест "как написано", т.к. далеко не факт, что ваш мок этого бл-сервиса соответствует реальному блсервису. Эта часть вообще никак не контролируется — и крайне трудно научить этому разработчикв.
А как контролиоруется что newUser возвращённый из бл обработается так как мы задумали в репе?
У тебя какое-то странное понимание моков. Цель мока не чтобы максимально точно описать поведение мокируемого, а чтобы описать различные возможные сценарии поведения мокируемого, на которые мы реагируем различным способом в sut.
P>Типичная проблема — моки крайне слабо напоминают тот самый класс.
Не очень понимаю проблему. Что можно нереального сделать в моке bl.modifyUser(id, newUserData) не так? Суть в том, что контроллер зовёт modifyUser и конвертирует результат:
Т.е. при тестировании контроллера совершенно неважно, что именно делает реальный modifyUser. Важно какие варианты результата он может вернуть и что на них реагируем в sut в соответствии с ожиданиями.
P>>>Совсем необязательно. Тесты это далеко не единственный инструмент в обеспечении качества. Потому покрывать код идея изначально утопичная. Покрывать нужно требования, ожидания вызывающей стороны, а не код.
P>·>Как? И как оценивать качество покрытия требований/ожиданий?
P>А как это QA делают? Строчка требований — список тестов, где она фигурирует.
А как узнать, что требования покрывают как можно больше возможных сценариев, а не только те, о которых не забыли подумать?
P>>>Для самого кода у нас много разных инструментов — от генерации по мат-модели, всяких инвариантов-пред-пост-условий и до код-ревью.
P>·>А метрики какие? Чтобы качество обеспечивать, его надо как-то измерять и контролировать.
P>Строчки это плохие метрики, т.к. цикломатическая сложность может зашкаливать как угодно
Чтобы она не зашкаливала — разделяют на части и мокают. Тогда как бы ни повёл себя одна часть, другая знает что делать.
P>Это я так трохи пошутил — идея в том, что нам всего то нужен один тест верхнего уровня. он у нас тупой как доска — буквально как http реквест-респонс
Ага. Но он, в лучшем случае, покроет только один xxxComposition. А их будет на каждый метод контроллера.
P>>>Постусловия к вам еще не завезли? Это блакбокс, только это совсем не тест.
P>·>А куда их завезли?
P>Много где завезли, в джава коде я такое много где видел. А еще есть сравнение логов — если у вас вдруг появится лишний аудит, он обязательно сломает выхлоп. Это тоже блекбокс
Это означает, что небольшое изменение в формате аудита ломает все тесты с образцами логов. Вот и представь, добавление небольшой штучки в аудит поломает все твои "несколько штук" тестов на каждый роут.
P>·>Это слишком тяжелый тест. Количество таких тестов должно быть минимально.
P>Разумеется — таких у нас по несколько штук на каждый роут
Именно. А должно быть несколько штук на весь сервис. Роутов может быть сотни. А достаточно потрогать каждое соединение между зависимостями. Например, если MaitreD может успешно дёрнуть метод getReservation у Repository, то это автоматически означает репо работает, что можно дёргать любые методы этого же Repository из любого метода MaitreD. Поэтому тяжелые и-тесты для find/create/delete/modify/etc уже не нужны.
P>>>Вы же моками обрезаете зависимости. Забыли? Каждая зависимость = причины для изменений.
P>·>Я не понял на какой вопрос этот ответ.
P>Вы спрашивали, почему короткая функция более устойчива по сравнению с maitred — потому, что у ней зависимостей меньше. Каждая зависимость, прямая или нет, это причина для изменения.
Я не о конкретной функции рассуждаю, а о системе. Если у неё зависимостей меньше, в ней меньше логики. А это значит, что эта логика была перенесена куда-то. И теперь это что-то тоже приходится тестировать. И плюс ещё тестировать что это что-то соединяется с этой функцией верно.
В этом у меня и основная претензия к статье. Был код, который делал X. Его заменили на "чистую красивую маленькую функцию", которая делает Y и теперь её легче тестировать. Но факт, что Y<X — замели под коврик. А эту разницу X-Y — запихали в страхолюдный xxxComposition метод и сделали вид, что он скучный и неинтересный, и можно его даже не тестировать.
P>Например, ваш бл-сервис неявно зависит от бд-клиента, через репозиторий. А это значит, смена бд сначала сломает ваш репозиторий, а это сломает ваш бл сервис.
Не понял, что значит "сломает"? Что за такая "смена бд"?
P>А раз вы его замокали для теста контроллера, то вам надо перебулшитить все тесты
Зачем? В тесте контрллера используется бл, поэтому моки только для него. И что творится в репе с т.з. контроллера — совершенно неважно.
P>>>Вот с maitred все не просто — любое изменение дизайна затрагивает такой класс, тк у него куча зависимостей, каждая из которых может меняться по разными причнам.
P>·>Куча это сколько? Судя по коду в статье я там аж целых одну насчитал.
P>
прямых — одна, а непрямых — все дочерние от того же репозитория
Неясно как "непрямые" зависимости влияют на _дизайн_ класса. Он их вообще никак не видит. Никак. Вообще. Ок... Есть, конечно, вероятность, что т.к. класс есть в скопе, то кто-то в конструктор MaitreD впишет Db и connectionString. От такого можно отмежеваться интерфейсами и разделением на модули, но обычно оверкилл — проще на ревью такое резать.
P>>>И вам каждый раз надо будет подфикшивать тесты.
P>·>По-моему ты очень странно пишешь тесты и дизайнишь, от того и проблемы.
P>Ничего странного — вы как то прошли мимо clean architecture.
Я как-то мимо rest и ui прошел в принципе... если есть какая ссылка "clean architecture for morons", давай.
P>>>Тогда не совсем понятен ваш вопрос.
P>·>https://en.wikipedia.org/wiki/Code_coverage
P>Я не знаю, в чем вы проблему видите. В жээс аннотация это обычный код, который инструментируется, и будет светиться или красным, или зеленым
В js вроде вообще нет аннотаций. Не знаю что именно ты зовёшь аннотацией тогда.
P>>>·>Самое что ни на есть вайтбокс тестирование. Что конкретный метод содержит конкретную аннотацию.
P>>>Мы тестируем, что есть те или иные метаданные. А вот чем вы их установите — вообще дело десятое.
P>·>А как ещё на метод можно метаданные навесить? Или ты про js?
P>разными способами
P>1. аннотации метода
P>2. аннотации класса
P>3. аннотации контекста класса
P>4. скриптованием, например, на старте
P>5. конфигурация приложения
P>6. переменные окружения
И это всё юнит-тестится как ты обещал?! expext(Metadata.from(ControllerX, 'tryAccept')).metadata.to.match({...})?
P>>>Мне нужно будет добавить функцию которая будет возвращать экземпляр аудитора по метаданным и покрыть её простыми дешовыми тестами. А раз весь аудит проходить будет через неё, то достаточно сверху накинуть интеграционный тест, 1шт
P>·>Тебе как-то рефлексивно придётся извлекать детали аудита из места вызова и параметров метода. На ЯП со статической типизацией будет полный швах.
P>Зачем? Есть трассировка и правило — результат аудита можно сравнивать
Не понял к чему ты это говоришь. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?
P>·>Мде... Или это "особенности" js?.. Но допустим. Как ты это предлагаешь переделать с "value"?
В общем я плохо знаю что есть в js, но тот код в jest действительно туфта какая-то, не надо так делать. Если не задаваться вопросом, зачем вообще вдруг понадобилось мокать hmac, то подобное с сохранением возможностей api библиотеки crypto в java будет выглядеть как-то так:
@Test void shouldVerify()
{
// deps
var console = mock(Console.class);
var crypto = mock(Crypto.class);
var hmac = mock(Hmac.class);
// set up
when(crypto.createHmac("sha256", "API_TOKEN")).thenReturn(hmac);//factory pattern
when(hmac.digest("hex")).thenReturn("123");
//sut
var app = new App(console, crypto);// dependency injection
app.main();
//verify
verify(hmac).update("{}");
verify(console).log("verified successfully. Implement next logic");
//тут ещё не хватает проверок, что "123" где-то правильно использовалось в результатах.
}P>Например, нам нужно считать токен для стрима, нас интересует ид класса, ид сущность, версия сущность b секрет.
Не очень ясно в чём идея. У тебя есть библотека которая умеет несколько алгоритмов, апдейт по частям с приведением типов, формирование дайджеста в разных форматах. И ты это оборачиваешь в свою обёртку, которая умеет только одно — конкретный алгоритм упакованный в отдельный метод Token.from, который содержит конкретные параметры и т.п. И что в этом такого особенного? Называется частичное применение функции. И причём тут моки?
Проблема такого подхода в том, что это работает только в тривиальных случаях. Когда в твоём приложении понадобятся много разных комбинаций, то у тебя количество частично применённых функций пойдёт по экспоненте. У тебя получатся ApiTokenAsHex.from, ApiTokenAsBytes.from, ApiTokenAsB64.from, HashedPassword.from, HashedPasswordAsBytes.from и пошло поехало.
И это только _два_ метода из целой библиотеки crypto уже даёт кучу комбинаций.
P>
P>const stream = Stream.from({Sample.id, Sample.version, Sample.content});
P>expect(Token.from(stream, Sample.secret)).to.eq(Sample.sha256);
P>P>Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.
Т.е. вынесли кусочек кода в метод и его протестировали отдельно. И? Моки тут причём?
P>И теперь в приложении вам не надо мокать Token.from, т.к. вы знаете, что она соответствует нашим ожиданиям, и не надо проверять, что контроллер обновления стрима вызывает Token.from если ему необходимо отрендерить ETag.
В этом случае тогда непонятно как изменение формата etag может все тесты сделать красными. Могу объяснить лишь тем как ты дизайнишь тесты. Потому что "обязательно сломает выхлоп"? Потому что тестов много, проверяют они очень много. И изменения бьют по многим целям одновременно.
P>Все что вам необходимо — в интеграционны тестах вызвать тот самый контроллер и убедиться, что его значения совпадают с нашими.
Я о чём и говорю — пишешь обёртки стороннего кода. Моки тут вообще никаким боком.
P>>>Ну да — проблемы нет а тесты стали красными. И так каждый раз.
P>·>Ничего не понял. Если использование уязвимой тормозной либы — не проблема, то зачем вообще код трогать?
P>ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.
Оборачивать каждую стороннюю либу — т.к. её _возможно_ нужно будет заменить — это оверинжиниринг в большинстве случаев.
P>А вы непонятно с какой целью хотите что бы тесты сломались.
P>Что нового вы добавите в приложение починкой этих тестов?
Нет, я пытаюсь понять причём тут моки. Ты с таким же успехом можешь оборачивать либу своей обёрткой и свою обёртку мочить.
P>а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)
А какая разница? Ведь эту конструкцию надо будет тоже каким-то тестом покрыть.
P>Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.
То что ты логику в коде куда-то переносишь, не означает, что её вдруг можно не тестировать.
P>·>Создаётся мок для класса Thing.
P>·>Дальше мокаешь методы и инжектишь в SUT.
P>Вот-вот, еще чтото надо куда то инжектить, вызывать, проверять, а вызвалось ли замоканое нужное количество раз, в нужной последовательности, итд итд итд
Это надо не для моков как таковых (если validateThing — тривиальна, то зачем её вообще мочить? Всегда можно запихать реальную имплементацию), а для разделения компонент и независимого их тестирования.
Я тебе уже написал когда это _надо_, но ты как-то тихо проигнорировал: Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.
То что это _надо_ именно для моков — это ты сам придумал. Тут зависимость в противоположную сторону — есть проблема использовать какой-то кусок из тестов, т.к. он делает слишком много, мы описываем только интересные нам аспекты поведения куска кода и фиксируем их в виде моков.
P>Итого — вы прибили тесты к реализации
В js принято валить всё в Global и сплошные синглтоны? DI не слышали?
P>>>Вы там в своём уме? Чтобы замокать чего нибудь ради теста, вам нужно знание о реализации, следовательно, тесткейс автоматически становится вайтбоксом.
P>·>В общем случае: чтобы передать что-то методу для теста, тоже надо знание о реализации, иначе не узнаешь corner-case.
P>На мой взгляд это не нужно — самый минимум это ожидания вызывающей стороны. Всё остальное это если время некуда девать.
Так пиши ожидания когда они известны, если ты их знаешь. Анализ вайтбокс, покрытие кода — именно поиск неизвестных ожиданий.
"Мы тут написали все ожидания по спеке, но вот в тот else почему-то ни разу не попали ни разу. Разбираемся почему."
P>·>Я понимаю код лучше, чем твои объяснения.
P>Я только-только поменял работу, к сожалению физически не могу показать типичный пример.
Да мне не нужен код с реального проекта. Небольшой пример достаточно.
P>·>Важно то, что в этом tryAcceptComposition находится connectionString. Т.е. условно говоря твой "интеграционнный тест" полезет в реальную субд и будет безбожно тормозить.
P>Во первых — таких тестов у нас будет пару штук
Это только для tryAcceptComposition. А таких врапперов придётся написать под каждый метод контроллера. И на каждый пару тестов.
P>Во вторых — моки не избавляют от необхидимости интеграционных тестов, в т.ч. запуска на реальной субд
Такой тест может быть один на всю систему. Запустилось, соединения установлены, версии совпадают, пинги идут. Готово.
>>В коде с моками — был reservationsRepository и возможность отмежеваться от реальной субд чтобы тестировать что параметры поиска и создания резерваций верные
P>Вот я и говорю — вы сделали в бл зависимость от бд, а потом в тестах изолируетесь от нее. Можно ж и иначе пойти — изолироваться на этапе дизайна, тогда ничего мокать не надо
В бл зависимость от репы.
P>>>Вы идете наоброт — выпихиваете всё в контроллер, тогда дешевыми юнит-тестами покрывать нечего, зато теперь надо контроллер обкладывать моками, чтобы хоть как то протестировать.
P>·>В некоторых случаях интеграционным тестом будет "код скомпилировался".
P>Если у вас много зависимостей — статическая типизация помогает слабовато.
Гораздо больше, чем динамическая.
P>Есть другой вариант — не вы передаете репозиторий в bl, а вызываете bl внутри т.н. use-case в терминологии clean architecture
P>тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория
А какие типы у объектов getUser/updateUser? И примерная реализация всех методов?
> const updateUser = repository.updateUser(oldUser, newUser);
А если я случайно напишу
const updateUser = repository.updateUser(newUser, oldUser);
где что когда упадёт?
P>·>Для юнит-теста Controller надо мокать только BlService.
P>вот это и проблема — у вас тест "как написано", т.к. далеко не факт, что ваш мок этого бл-сервиса соответствует реальному блсервису. Эта часть вообще никак не контролируется — и крайне трудно научить этому разработчикв.
А как контролиоруется что newUser возвращённый из бл обработается так как мы задумали в репе?
У тебя какое-то странное понимание моков. Цель мока не чтобы максимально точно описать поведение мокируемого, а чтобы описать различные возможные сценарии поведения мокируемого, на которые мы реагируем различным способом в sut.
P>Типичная проблема — моки крайне слабо напоминают тот самый класс.
Не очень понимаю проблему. Что можно нереального сделать в моке bl.modifyUser(id, newUserData) не так? Суть в том, что контроллер зовёт modifyUser и конвертирует результат:
class UserController {
Response update(Request r) {
var id = extractId(r);
var data = extractData(r);
var result = bl.modifyUser(id, data);
return result.isSuccess ? Response(r.correlationId, prepare(result)) : Failure("sorry");
}
}Т.е. при тестировании контроллера совершенно неважно, что именно делает реальный modifyUser. Важно какие варианты результата он может вернуть и что на них реагируем в sut в соответствии с ожиданиями.
P>>>Совсем необязательно. Тесты это далеко не единственный инструмент в обеспечении качества. Потому покрывать код идея изначально утопичная. Покрывать нужно требования, ожидания вызывающей стороны, а не код.
P>·>Как? И как оценивать качество покрытия требований/ожиданий?
P>А как это QA делают? Строчка требований — список тестов, где она фигурирует.
А как узнать, что требования покрывают как можно больше возможных сценариев, а не только те, о которых не забыли подумать?
P>>>Для самого кода у нас много разных инструментов — от генерации по мат-модели, всяких инвариантов-пред-пост-условий и до код-ревью.
P>·>А метрики какие? Чтобы качество обеспечивать, его надо как-то измерять и контролировать.
P>Строчки это плохие метрики, т.к. цикломатическая сложность может зашкаливать как угодно
Чтобы она не зашкаливала — разделяют на части и мокают. Тогда как бы ни повёл себя одна часть, другая знает что делать.
P>Это я так трохи пошутил — идея в том, что нам всего то нужен один тест верхнего уровня. он у нас тупой как доска — буквально как http реквест-респонс
Ага. Но он, в лучшем случае, покроет только один xxxComposition. А их будет на каждый метод контроллера.
P>>>Постусловия к вам еще не завезли? Это блакбокс, только это совсем не тест.
P>·>А куда их завезли?
P>Много где завезли, в джава коде я такое много где видел. А еще есть сравнение логов — если у вас вдруг появится лишний аудит, он обязательно сломает выхлоп. Это тоже блекбокс
Это означает, что небольшое изменение в формате аудита ломает все тесты с образцами логов. Вот и представь, добавление небольшой штучки в аудит поломает все твои "несколько штук" тестов на каждый роут.
P>·>Это слишком тяжелый тест. Количество таких тестов должно быть минимально.
P>Разумеется — таких у нас по несколько штук на каждый роут
Именно. А должно быть несколько штук на весь сервис. Роутов может быть сотни. А достаточно потрогать каждое соединение между зависимостями. Например, если MaitreD может успешно дёрнуть метод getReservation у Repository, то это автоматически означает репо работает, что можно дёргать любые методы этого же Repository из любого метода MaitreD. Поэтому тяжелые и-тесты для find/create/delete/modify/etc уже не нужны.
P>>>Вы же моками обрезаете зависимости. Забыли? Каждая зависимость = причины для изменений.
P>·>Я не понял на какой вопрос этот ответ.
P>Вы спрашивали, почему короткая функция более устойчива по сравнению с maitred — потому, что у ней зависимостей меньше. Каждая зависимость, прямая или нет, это причина для изменения.
Я не о конкретной функции рассуждаю, а о системе. Если у неё зависимостей меньше, в ней меньше логики. А это значит, что эта логика была перенесена куда-то. И теперь это что-то тоже приходится тестировать. И плюс ещё тестировать что это что-то соединяется с этой функцией верно.
В этом у меня и основная претензия к статье. Был код, который делал X. Его заменили на "чистую красивую маленькую функцию", которая делает Y и теперь её легче тестировать. Но факт, что Y<X — замели под коврик. А эту разницу X-Y — запихали в страхолюдный xxxComposition метод и сделали вид, что он скучный и неинтересный, и можно его даже не тестировать.
P>Например, ваш бл-сервис неявно зависит от бд-клиента, через репозиторий. А это значит, смена бд сначала сломает ваш репозиторий, а это сломает ваш бл сервис.
Не понял, что значит "сломает"? Что за такая "смена бд"?
P>А раз вы его замокали для теста контроллера, то вам надо перебулшитить все тесты
Зачем? В тесте контрллера используется бл, поэтому моки только для него. И что творится в репе с т.з. контроллера — совершенно неважно.
P>>>Вот с maitred все не просто — любое изменение дизайна затрагивает такой класс, тк у него куча зависимостей, каждая из которых может меняться по разными причнам.
P>·>Куча это сколько? Судя по коду в статье я там аж целых одну насчитал.
P>
Неясно как "непрямые" зависимости влияют на _дизайн_ класса. Он их вообще никак не видит. Никак. Вообще. Ок... Есть, конечно, вероятность, что т.к. класс есть в скопе, то кто-то в конструктор MaitreD впишет Db и connectionString. От такого можно отмежеваться интерфейсами и разделением на модули, но обычно оверкилл — проще на ревью такое резать.
P>>>И вам каждый раз надо будет подфикшивать тесты.
P>·>По-моему ты очень странно пишешь тесты и дизайнишь, от того и проблемы.
P>Ничего странного — вы как то прошли мимо clean architecture.
Я как-то мимо rest и ui прошел в принципе... если есть какая ссылка "clean architecture for morons", давай.
P>>>Тогда не совсем понятен ваш вопрос.
P>·>https://en.wikipedia.org/wiki/Code_coverage
P>Я не знаю, в чем вы проблему видите. В жээс аннотация это обычный код, который инструментируется, и будет светиться или красным, или зеленым
В js вроде вообще нет аннотаций. Не знаю что именно ты зовёшь аннотацией тогда.
P>>>·>Самое что ни на есть вайтбокс тестирование. Что конкретный метод содержит конкретную аннотацию.
P>>>Мы тестируем, что есть те или иные метаданные. А вот чем вы их установите — вообще дело десятое.
P>·>А как ещё на метод можно метаданные навесить? Или ты про js?
P>разными способами
P>1. аннотации метода
P>2. аннотации класса
P>3. аннотации контекста класса
P>4. скриптованием, например, на старте
P>5. конфигурация приложения
P>6. переменные окружения
И это всё юнит-тестится как ты обещал?! expext(Metadata.from(ControllerX, 'tryAccept')).metadata.to.match({...})?
P>>>Мне нужно будет добавить функцию которая будет возвращать экземпляр аудитора по метаданным и покрыть её простыми дешовыми тестами. А раз весь аудит проходить будет через неё, то достаточно сверху накинуть интеграционный тест, 1шт
P>·>Тебе как-то рефлексивно придётся извлекать детали аудита из места вызова и параметров метода. На ЯП со статической типизацией будет полный швах.
P>Зачем? Есть трассировка и правило — результат аудита можно сравнивать
Не понял к чему ты это говоришь. На вход метода подаются какие-то парамы. У тебя на нём висит аннотация. Откуда аннотация узнает что/как извлечь из каких парамов чтобы записать что-то в аудит?