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

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

P>>>Вот пример — у нас есть требование вернуть hash токен что бы по контенту можно было находить метадату итд
P>>>кода получается вагон и маленькая тележка
P>·>Лучше код покажи. Так что-то не очень ясно.
P>    expect(crypto.createHmac).toBeCalledWith('sha256', 'API_TOKEN');

Мде... Или это "особенности" js?.. Но допустим. Как ты это предлагаешь переделать с "value"?

P>>>А теперь фиаско — в либе есть уязвимость и проблема с перформансом, и её надо заменить, гы-гы все тесты на моках ушли на помойкку

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

P>·>Иными словами ты просто предлагаешь заворачивать стороннюю либу в свою обёртку. Это хорошо, иногда. Вот только моки-то тут причём?

P>Иными словами я предлагаю использовать value тест где только можно, т.к. таких можно настрочить сколько угодно.
Ок, покажи код как правильно должно быть по-твоему.

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

P>·>Ага. Вот только эта твоя обёртка как будет попадать в использующий код? Через зависимости. И будешь мокать эту обёртку.
P>Вот я и говорю, что вы без моков жить не можете. Если мы проверили, что функция валидная, зачем нам её мокать? Проблема то какая?
В том, что в SUT нам не важно всё сложное и навороченное поведение функции, пусть даже идеально работающей. Мы хотим только проверить те случаи, которые важны для тестируемого кода. Условно говоря, у нас есть функция "validateThing", написанная и отлаженая/етс.
Теперь мы используем её в коде:
Result doSomething(Request r) {
  if(validateThing(r.part1)) {
    wroom(r.zcxv);
    if(validateThing(r.part2) {
      return NiceResult(r.a, r.b);
    } else {
      foBarBaz();
      return SoSoResult(meh(r));
    }
  } else {
    taramparam(r.aaa);
    return NoNoNo(zzzz(r));
  }
}

Вот тут нам совершенно неважно что и как делает validateThing — тут нам надо всего три сценария рассмотреть:
1. validateThing(part1) => returns false;
2. validateThing(part1) => returns true; validateThing(part2) => returns false;
3. validateThing(part1) => returns true; validateThing(part2) => returns true;
вот это и будет замокано. Состав и содержимое part1/part2 — в данном месте нам совершенно неважно, там могут быть затычки. Более того, когда мы будем дорабатывать поведение фукнции validateThing потом, эти три наших теста не пострадают, что они вдруг не очень правильно сформированные part1/part2 подают. Вдобавок, функция хоть пусть и валидная, но она может быть медленной — лазить в сеть или банально жечь cpu вычислениями.

P>>>>>То есть, это инструменты которые решают разные классы задач — вместо "assert x = y" тестируем "унутре вызвали вон тот метод с такими параметрами"

P>>>·>"verify(mock).x(y)"
P>>>Вместо просто assert x = y вам надо засетапить мок и надеяться, что новые требования не сломают дизайн вашего решения.
P>·>А чего там сетапить-то? var mock = Mockito.mock(Thing.class). А что там может ломаться и зачем?
P>Я не понимаю, что здесь происходит.
Создаётся мок для класса Thing.
Дальше мокаешь методы и инжектишь в SUT.

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

P>·>Моки и вайтбокс — это ортогональные понятия.
P>Вы там в своём уме? Чтобы замокать чего нибудь ради теста, вам нужно знание о реализации, следовательно, тесткейс автоматически становится вайтбоксом.
В общем случае: чтобы передать что-то методу для теста, тоже надо знание о реализации, иначе не узнаешь corner-case.

P>>>А вот если в проекте пошли по пути классического юнит-тестирования, то как правило тесты особого внимания не требуют — все работает само.

P>·>Можно ссылочки?
P>Зачем?
Я понимаю код лучше, чем твои объяснения.

P>·>Условно говоря... Моки используются чтобы чего-то передать или проверить чего нет в сигнатуре метода. Т.е. метод с сайд-эффектами — его поведение зависит не только от его параметров и|или его результат влияет не только на возвращаемое значение.

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

P>>>·>Проигнорировал вопрос трижды: Напиши однострочный интеграционный тест для tryAcceptComposition. Код в студию!

P>>>
P>>>expect(new class().tryTryAcceptComposition(parameter)).to.eq(value);
P>>>

P>·>Заходим на четвёртый круг. Я не знаю что это такое и к чему относится. Напомню код:
P>Я не в курсе того фп языка, и ничего на нем не выдам. Для интеграционного тестам нам надо вызвать контроллер верхнего уровня, и убедиться, что результат тот самый.
Это хаскель вроде, впрочем это совершенно неважно.

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

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

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

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

P>·>Выделил твою цитату выше. И где ты у меня тут слово "мок" нашел?

P>Если это не про моки, то вопросов еще больше. Каким образом иерархия контроллер <- сервис-бл <- репозиторий <- клиент-бд стала у вас вдруг плоской?
DI же, не?
var db = new Db(connectionString);
var repo = new Repo(db);
var bl = new BlService(repo);
var ctl = new Controller(bl);
var server = new Server(port);
server.handle("/path", ctl);
server.start();


P>Я думал вы собираетесь замокать все зависимости контроллера, но вы похоже бьете все рекорды

Для юнит-теста Controller надо мокать только BlService.

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

P>·>Покрытие позволяет найти непротестированный код. Если мы его нашли — мы видим пропущенные требования и начинаем выяснять ожидание пользователя в пропущенном сценарии. И вот после этого тестируем ожидание.
P>Совсем необязательно. Тесты это далеко не единственный инструмент в обеспечении качества. Потому покрывать код идея изначально утопичная. Покрывать нужно требования, ожидания вызывающей стороны, а не код.
Как? И как оценивать качество покрытия требований/ожиданий?

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

А метрики какие? Чтобы качество обеспечивать, его надо как-то измерять и контролировать.

P>>>Мне непонято что в моем объяснении вам не ясно

P>·>Как выглядит код, который тестирует tryAcceptComposition.
P>Да хоть вот так, утрирую конечно
P>
P>curl -X POST -H "Content-Type: application/json" -d '{<резервирование>}' http://localhost:3000/api/v3/reservation
P>

А где ассерт?

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

P>>>А вот если вы идете методом блакбокс тестирования, то никакие моки вам применить не удастся, и это дает более устойчивый код решения и более устойчивые тесты
P>·>Вайтбокс знает имплементацию. Каким образом ты будешь блакбоксить такое? int square(int x) {return x == 47919 ? 1254 : x * x;}. Заметь, никаки моков, тривиальная пюрешка.
P>Постусловия к вам еще не завезли? Это блакбокс, только это совсем не тест.
А куда их завезли?

P>>>Уже объяснил. Что именно вам непонятно?

P>·>А я просил что-то объяснять? Я просил показать код.
P>
P>curl -X POST -H "Content-Type: application/json" -d '{<резервирование>}' http://localhost:3000/api/v3/reservation
P>

Это слишком тяжелый тест. Количество таких тестов должно быть минимально.

P>>>А вот простые тесты на вычисления резервирования остаются, потому как ни на какой MaitreD не завязаны. Что будет вместо MaitreD — вообще дело десятое.

P>·>А конкретно? Простые тесты чего именно в терминах ЯП? Какой языковой конструкции? И почему эта конкретная языковая конструкция отлита в граните и никак никогда поменяться не может?
P>Вы же моками обрезаете зависимости. Забыли? Каждая зависимость = причины для изменений.
Я не понял на какой вопрос этот ответ.

P>Простая функция типа "пюрешечки" — у неё зависимостей около нуля. Меньше зависимостей — меньше причин для изменения.

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

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

По-моему ты очень странно пишешь тесты и дизайнишь, от того и проблемы.

P>>>Выбираю абстракцию наименьшего размера, которая покрывает кейс, и которую я могу протестировать

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

P>>>>>Ровно наоборот — аннотации в покрытии учавствуют, просто обязаны.

P>>>·>Покажи мне coverage report, где непокрытые аннотации красненьким подсвечены.
P>>>
P>>>Expectation failed: ControllerX tryAccept to have metadata {...}
P>>>

P>·>Это не coverage report, это test report.
P>Тогда не совсем понятен ваш вопрос.
https://en.wikipedia.org/wiki/Code_coverage

P>>>
P>>>const tryAccept = Metadata.from(ControllerX, 'tryAccept')
P>>>expext(tryAccept).metadata.to.match({...})
P>>>

P>·>Самое что ни на есть вайтбокс тестирование. Что конкретный метод содержит конкретную аннотацию.
P>Мы тестируем, что есть те или иные метаданные. А вот чем вы их установите — вообще дело десятое.
А как ещё на метод можно метаданные навесить? Или ты про js?

P>А вот метаданные это совсем не вайтбокс, это нефункциональные требования.

Зачем тогда их в эти тесты класть?..

P>·>
P>·>controllerX.tryAccept(...)
P>·>verify(metadata).to.match(...);
P>·>

P>·>вот только у тебя не будет приколачивания к конкретному методу
P>Ну то есть tryAccept что в вашем тесте это никакой не конкретный метод?
Это публичная точка входа. Там внутре может быть что-то, вызов приватных методов или ещё чего. Не важно же как, не важно из какого конкретного места кода, главное, чтобы аудит был записан — всё как ты требуешь от меня, а сам гвоздями к имплементации приколачиваешь.

>> и никакой рефлекии не надо.

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

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

P>В вашем случае придется писать намного больше кода из за моков — перемокать всё подряд что бы выдать "вот этот метод не вызвался тут, там" и так по всем 100500 кейсам
Нет.

P>Мне нужно будет добавить функцию которая будет возвращать экземпляр аудитора по метаданным и покрыть её простыми дешовыми тестами. А раз весь аудит проходить будет через неё, то достаточно сверху накинуть интеграционный тест, 1шт

Тебе как-то рефлексивно придётся извлекать детали аудита из места вызова и параметров метода. На ЯП со статической типизацией будет полный швах.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[15]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 05.12.23 19:18
Оценка:
Здравствуйте, ·, Вы писали:

P>>·>Лучше код покажи. Так что-то не очень ясно.

·>
P>>    expect(crypto.createHmac).toBeCalledWith('sha256', 'API_TOKEN');
·>

·>Мде... Или это "особенности" js?.. Но допустим. Как ты это предлагаешь переделать с "value"?

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

const stream = Stream.from({Sample.id, Sample.version, Sample.content});

expect(Token.from(stream, Sample.secret)).to.eq(Sample.sha256);


Теперь вы можете покрыть все кейсы — 1-3 успешных, и ведёрко фейлов типа ид, секрет, версия не установлены.
И теперь в приложении вам не надо мокать Token.from, т.к. вы знаете, что она соответствует нашим ожиданиям, и не надо проверять, что контроллер обновления стрима вызывает Token.from если ему необходимо отрендерить ETag.
Все что вам необходимо — в интеграционны тестах вызвать тот самый контроллер и убедиться, что его значения совпадают с нашими.

P>>Ну да — проблемы нет а тесты стали красными. И так каждый раз.

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

ну вот представьте — либа слишком медленная. И вы находите ей замену. Идеальный результат — после замены тесты зеленые.
А вы непонятно с какой целью хотите что бы тесты сломались.
Что нового вы добавите в приложение починкой этих тестов?

P>>Иными словами я предлагаю использовать value тест где только можно, т.к. таких можно настрочить сколько угодно.

·>Ок, покажи код как правильно должно быть по-твоему.

Ниже будет пример подробный

·>Теперь мы используем её в коде:

·>
·>Result doSomething(Request r) {
·>  if(validateThing(r.part1)) {
·>    wroom(r.zcxv);
·>    if(validateThing(r.part2) {
·>      return NiceResult(r.a, r.b);
·>    } else {
·>      foBarBaz();
·>      return SoSoResult(meh(r));
·>    }
·>  } else {
·>    taramparam(r.aaa);
·>    return NoNoNo(zzzz(r));
·>  }
·>}
·>

·>Вот тут нам совершенно неважно что и как делает validateThing — тут нам надо всего три сценария рассмотреть:
·>1. validateThing(part1) => returns false;
·>2. validateThing(part1) => returns true; validateThing(part2) => returns false;
·>3. validateThing(part1) => returns true; validateThing(part2) => returns true;
·>вот это и будет замокано.

Что я вам и говорю — вы сделали дизайн под моки перемешивая изменения с вычислениями
Вместо вот такого:
     foBarBaz(); <- здесь у вас сайд эффекты
     return SoSoResult(meh(r));

у вас может быть следующее
     const toDo = calculate(); // покрыто юнит тестами, т.к. простые вычисления без сайд эффектов
     
     return SoSoChanges(todo);

а на вызывающей стороне вы вызываете какой нибудь result.applyTo(ctx)
Соответсвенно, все изменения у вас будут в конце, на самом верху, а до того — вычисления, которые легко тестируются.

·>Создаётся мок для класса Thing.

·>Дальше мокаешь методы и инжектишь в SUT.

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

P>>Вы там в своём уме? Чтобы замокать чего нибудь ради теста, вам нужно знание о реализации, следовательно, тесткейс автоматически становится вайтбоксом.

·>В общем случае: чтобы передать что-то методу для теста, тоже надо знание о реализации, иначе не узнаешь corner-case.

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

P>>Зачем?

·>Я понимаю код лучше, чем твои объяснения.

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

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

·>Хочу посмотреть на твой дизайн.

Мой дизайн в основном в закрытых репозиториях. Пока что так. Опенсорсные проекты я только исследую или использую.

·>Важно то, что в этом tryAcceptComposition находится connectionString. Т.е. условно говоря твой "интеграционнный тест" полезет в реальную субд и будет безбожно тормозить.


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

>В коде с моками — был reservationsRepository и возможность отмежеваться от реальной субд чтобы тестировать что параметры поиска и создания резерваций верные


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

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

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

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

P>>Если это не про моки, то вопросов еще больше. Каким образом иерархия контроллер <- сервис-бл <- репозиторий <- клиент-бд стала у вас вдруг плоской?

·>DI же, не?
·>
·>var db = new Db(connectionString);
·>var repo = new Repo(db);
·>var bl = new BlService(repo);
·>var ctl = new Controller(bl);
·>var server = new Server(port);
·>server.handle("/path", ctl);
·>server.start();
·>


Я вижу, что здесь вы создаёте ту самую глубокую иерархию — у вас сервис bl зависит от репозитория который зависит от bd.

Есть другой вариант — не вы передаете репозиторий в bl, а вызываете bl внутри т.н. use-case в терминологии clean architecture
тогда получается иерархия плоская — у контроллера кучка зависимостей, но репозиторий не зависит от бд, бл не зависит от репозитория

controller зависит от
* db
* repositotory
* bl

Вот наш юз кейс — интеграционный код, покрывается интеграционным тестом
const getUser = repository.getUser(id); // покрыто юнит тестом
const oldUser = db.execute(getUser);
const newUser = bl.modifyUser(oldUser); // покрыто юнит тестом
const updateUser = repository.updateUser(oldUser, newUser); // покрыто юнит тестом
const updatedUser = db.execute(updateUser); 

return updatedUser;


вот наш контроллер — интеграционный код, покрывается интеграционным тестом
handler(id, ctx, metadata) {
    const useCase = UpdateUser.from(ctx); // покрыто юнит тестом
    const serializer = Serializer.from(ctx, metadata) // покрыто юнит тестом
    const result = userCase.invoke(id); 

    return serializer.toResponse(result); // покрыто юнит тестом
}


P>>Я думал вы собираетесь замокать все зависимости контроллера, но вы похоже бьете все рекорды

·>Для юнит-теста Controller надо мокать только BlService.

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

Типичная проблема — моки крайне слабо напоминают тот самый класс.

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

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

А как это QA делают? Строчка требований — список тестов, где она фигурирует.

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

·>А метрики какие? Чтобы качество обеспечивать, его надо как-то измерять и контролировать.

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

P>>Да хоть вот так, утрирую конечно

P>>
P>>curl -X POST -H "Content-Type: application/json" -d '{<резервирование>}' http://localhost:3000/api/v3/reservation
P>>

·>А где ассерт?

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

P>>Постусловия к вам еще не завезли? Это блакбокс, только это совсем не тест.

·>А куда их завезли?

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

·>Это слишком тяжелый тест. Количество таких тестов должно быть минимально.


Разумеется — таких у нас по несколько штук на каждый роут

P>>Вы же моками обрезаете зависимости. Забыли? Каждая зависимость = причины для изменений.

·>Я не понял на какой вопрос этот ответ.

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

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

·>Куча это сколько? Судя по коду в статье я там аж целых одну насчитал.

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

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

·>По-моему ты очень странно пишешь тесты и дизайнишь, от того и проблемы.

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

P>>Тогда не совсем понятен ваш вопрос.

·>https://en.wikipedia.org/wiki/Code_coverage

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

P>>·>Самое что ни на есть вайтбокс тестирование. Что конкретный метод содержит конкретную аннотацию.

P>>Мы тестируем, что есть те или иные метаданные. А вот чем вы их установите — вообще дело десятое.
·>А как ещё на метод можно метаданные навесить? Или ты про js?

разными способами
1. аннотации метода
2. аннотации класса
3. аннотации контекста класса
4. скриптованием, например, на старте
5. конфигурация приложения
6. переменные окружения

P>>А вот метаданные это совсем не вайтбокс, это нефункциональные требования.

·>Зачем тогда их в эти тесты класть?..

затем и класть, что это часть требований. фиксируем капабилити

P>>Мне нужно будет добавить функцию которая будет возвращать экземпляр аудитора по метаданным и покрыть её простыми дешовыми тестами. А раз весь аудит проходить будет через неё, то достаточно сверху накинуть интеграционный тест, 1шт

·>Тебе как-то рефлексивно придётся извлекать детали аудита из места вызова и параметров метода. На ЯП со статической типизацией будет полный швах.

Зачем? Есть трассировка и правило — результат аудита можно сравнивать
Отредактировано 05.12.2023 19:32 Pauel . Предыдущая версия . Еще …
Отредактировано 05.12.2023 19:30 Pauel . Предыдущая версия .
Re[16]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 06.12.23 00:19
Оценка:
Здравствуйте, Pauel, Вы писали:

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.

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

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

Кое что надо и в самом методе вызывать, без этого работать не будет. Видите нужные строчки — аудит работает, выхлоп совпадает с предыдущим забегом — точно все отлично. Не совпадает — точно есть проблемы.
Re[16]: Что такое Dependency Rejection
От: Буравчик Россия  
Дата: 10.12.23 08:13
Оценка:
Здравствуйте, Pauel, Вы писали:

P>вот наш контроллер — интеграционный код, покрывается интеграционным тестом


Хочу тоже подключиться к вашему обсуждению.

1. Про контроллер

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

Кстати, что делаете если параметров много (как в примере)? Выделяете под них структуру для хранения?

http_handler(request):
  v1, v2, v3 = parse_and_validate(request)  // pure
  result = do_logic(v1, v2, v3)
  return serialize(result)  // pure


2. Про логику

Ниже пример того, что хотим протестировать. Есть обращение к БД и внешним сервисам. Как это будет протестировано?
Самый главный вопрос — как будет протестировано ветвление (if-ы, их может быть много).

do_logic(v1, v2, v3):
  // работаем с БД
  p = repo.get(v1)

  // получаем дополнительные данные из внешнего источника
  if что-то(p):  
    p2 = external_service.request(v2)
    p = update(p, p2)

  // отправляем в БД и условную кафку
  if что-то(p):   
    repo.put(v3)
    another_external_service.send(v3)

  result = serialize(p)  // pure
  return result
Best regards, Буравчик
Re[18]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 10.12.23 13:41
Оценка:
Здравствуйте, 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>
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> Видите нужные строчки — аудит работает, выхлоп совпадает с предыдущим забегом — точно все отлично. Не совпадает — точно есть проблемы.

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

Суть в том, что детали аудита каждой бизнес-операции можно покрывать туевой хучей отдельных юнит-тестов с моками на каждое возможное ожидание. И тут аннотации будут только мешаться. Интеграционный тест же можно написать один — что хоть какая-то бизнес-операция записала хоть какой-то аудит.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Отредактировано 10.12.2023 13:53 · . Предыдущая версия .
Re[17]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 20.12.23 11:48
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Кстати, что делаете если параметров много (как в примере)? Выделяете под них структуру для хранения?


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

Б>2. Про логику


Б>Ниже пример того, что хотим протестировать. Есть обращение к БД и внешним сервисам. Как это будет протестировано?


Если дизайн именно такой, и надо вдруг покрыть это тестами, то вариантов не много
1. интеграционные тесты с какой то базой
2. разного сорта моки

для п.2 для разработчиков очень трудно не скатиться в тавтологические тесты вида "метод вызывает другой метод"

Б>Самый главный вопрос — как будет протестировано ветвление (if-ы, их может быть много).


Это ничего не меняет — вашем дизайне вариантов нету, и наличие ветвлений их не увеличит.

Проблема в самих зависимостях, их много, и в том, что ваш дизайн мутабельный

Попробуйте отказаться от мутабельного дизайна. Как минимум, три эффекта из четырех легко отделяются. Так что можно упростить до безобразия.
Re[18]: Что такое Dependency Rejection
От: Буравчик Россия  
Дата: 20.12.23 16:29
Оценка:
Здравствуйте, Pauel, Вы писали:

P>Если дизайн именно такой, и надо вдруг покрыть это тестами, то вариантов не много

P>1. интеграционные тесты с какой то базой
P>2. разного сорта моки

Что значит дизайн такой? Это логика приложения такая, как ее по другому задизайнить?

P>для п.2 для разработчиков очень трудно не скатиться в тавтологические тесты вида "метод вызывает другой метод"


С моками можно по-разному тестировать. Правильно — в моках возвращаемое значение фиксировать, а не тестировать факт вызовы.
Хотя в некоторых случаях и факт вызова тоже может быть важен

P>Это ничего не меняет — вашем дизайне вариантов нету, и наличие ветвлений их не увеличит.


Не понял, что понимается под вариантами. И почему ветвления не влияют.

P>Проблема в самих зависимостях, их много, и в том, что ваш дизайн мутабельный

P>Попробуйте отказаться от мутабельного дизайна. Как минимум, три эффекта из четырех легко отделяются. Так что можно упростить до безобразия.

Я как раз и хочу понять, как эту логику сделать иммутабельной, чтобы все варианты протестить.
Как выделить эффекты, можно продемонстрировать?

С моками (стабами) я понимаю как это сделать — сайд-эффекты выделяются в интерфейсы (repo, external_service), мокаются, и далее вся логика проверяется как обычный юнит-тест.
Best regards, Буравчик
Re[19]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 20.12.23 21:03
Оценка: +1
Здравствуйте, Буравчик, Вы писали:

P>>Если дизайн именно такой, и надо вдруг покрыть это тестами, то вариантов не много

P>>1. интеграционные тесты с какой то базой
P>>2. разного сорта моки

Б>Что значит дизайн такой? Это логика приложения такая, как ее по другому задизайнить?


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

P>>для п.2 для разработчиков очень трудно не скатиться в тавтологические тесты вида "метод вызывает другой метод"


Б>С моками можно по-разному тестировать. Правильно — в моках возвращаемое значение фиксировать, а не тестировать факт вызовы.


Похоже, что моки про которые вы говорите, это фаулеровские стабы

Читайте фаулера

Mocks are objects pre-programmed with expectations which form a specification of the calls they are expected to receive


Мок в фаулеровской терминологии вещь бесполезная, хотя именно их чаще всего и лепят

P>>Это ничего не меняет — вашем дизайне вариантов нету, и наличие ветвлений их не увеличит.


Б>Не понял, что понимается под вариантами. И почему ветвления не влияют.


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

P>>Проблема в самих зависимостях, их много, и в том, что ваш дизайн мутабельный

P>>Попробуйте отказаться от мутабельного дизайна. Как минимум, три эффекта из четырех легко отделяются. Так что можно упростить до безобразия.

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


Гнаться за иммутабельностью не сильно нужно, главное отделить зависимости, эффекты

Б>Как выделить эффекты, можно продемонстрировать?


У вас их 4 — чтение из бд, вызов сервиса, запись в бд, вызов кафки

Общий подход — эффекты преобразовать в вызовы интерфейса, инстанц которого приходит с параметрами

Потом преобразуете метод в класс, v1,v2,v3 уходят в члены класса

class Logic(v1, v2, v3):  // параметры-значения в конструкторе
  def run(load, load2, commit): // эффекты-зависимости здесь, эдакий аналог импорта
     p = ctx.load(self.v1) 

     if что-то(p):  
       p2 = ctx.load2(self.v2) // load2 - абстракция так себе, должно быть хорошее имя
       p = update(p, p2)

     if что-то(p):   
       ctx.commit(self.v3)
      
     return p


Теперь в тестах вы можете создать n троек значений, m результатов операций
И покроется все одной таблицей тестов n x m

И вам не надо мокать репозитории, запись в кафку, хттп вызов сервиса итд

def do_logic(v1,v2,v3):
  l = Logic(v1,v2,v3)
  p = l.run(
    load = ..., repo  // здесь и ниже хватит простой лямбды
    load2 = ...., svc
    commit = ..., repo+kafka // как вариант, для кафки можно протащить доп. имя, например event
  );

  Return serialize(p)


Теперь do-logic стал линейным, его покроет любой интеграционный код, который в любом случае должен быть

Любители моков правда очень обрадуются, умножат(sic!) Количество тестов за счет проверок "commit вызывается после двух load"
Потом небольшое изменение кода, сломает все такие тесты, даже если результат p у нас гарантированно сохранится

Теперь смотрите, что будет, если вы напрямую мокаете репозиторий в своем предыдущем коде

do_logic(v1, v2, v3):
  p = repo.get(v1)

  if что-то(p):  
    arg = repo.get(v2) // появился доп параметр, который надо вытащить из бд
    p2 = external_service.request(v2, arg)
    p = update(p, p2)

...


Теперь ваш мок repo.get должен считать вызовы — что его вызвали первый раз с v1, второй раз с v2, и это важно, т.к на втором разе вы должны вернуть кое что другое
И это именно то, чего делать не стоит — в сложной логике ваш мок репозитория будет чудовищем, которое мало будет походить на реальную часть системы
Чем более унивесальная зависимость, тем труднее её мокать, и тем выше шансы на ошибку

Если не хотите возиться с интерфейсом — можно вытащить repo.get и подобное в методы рядом с do_logic. Идея на самом деле так себе, этот подход стоит применять если у вас не сильно много зависимостей
do_logic(v1, v2, v3):
  p = self.load(v1)

  if что-то(p):  
    p2 = self.load2(v2)
    p = update(p, p2)

  if что-то(p):   
    self.commit(v3)

  result = serialize(p) 
  return result


Выглядит компактно — только штука в том, что вам придется мокать сам класс, ну хотя бы через наследника под тесты или сетать все методы и надеятся что ничего не пропустите.
А вот зависимости как параметры вы можете понасоздвать или нагенерить на все случаи жизни
1 ошибки всех сортов
2 пустой результат
3 куча вариантов ответа
4 куча сочетаний того и этого
Отредактировано 21.12.2023 8:57 Pauel . Предыдущая версия .
Re[19]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 21.12.23 10:20
Оценка: +1
Здравствуйте, ·, Вы писали:

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

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

Хорошесть инструмента это именно то как другие его понимают и умеют применять. Если код на моках в основном кривой — значит моки за 20 лет индустрия так и не осилила.

Вы вот сами путаете моки и стабы

> Это проблема ликбеза, а не моков. Ровно та же проблема есть в любых подходах тестирования.


Даем слово Фаулеру:

Mocks are objects pre-programmed with expectations which form a specification of the calls they are expected to receive


Видите — моки это про описание вызовов и связаные с этим ожидания

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

P>>А зачем вам эти AsHex, AsBytes? Это же просто преставление. У вас есть токен — это значение. Его можно создать парой-тройкой способов, и сохранить так же.

·>Именно. 3 способа создания x 3 способа представления — уже девять комбинаций. У тебя будет либо девять функций, либо своя обёртка над сторонним api, изоморфная по сложности тому же api.

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

P>> Отсюда ясно, что нет нужды плодить те функции, что вы предложили, а следовательно и тестировать их тоже не нужно.

·>Нужда идёт из кода, из требований. Если у тебя достаточно сложная система, у тебя будет там все 9 комбинаций, а то и больше.

Это у вас так будет, тут я охотно верю.

P>>Всех дел — отделить данные от представления, и количество тестов уменьшается.

·>Количество может быть уменьшено, только если ты какое-то требование перестал тестировать.

Количество может быть уменьшено за счет подхода к дизайну.
Например, у нас есть Date, Time, DateTime, DateTimeOffset, LocalDateTime, Instant, Absolute, и целый набор всякой всячины.
Так вот, для создания DateTimeOffset нам не надо перебирать всё множество вариантов, как вы предлагаете
Можно свести ровно к одному DateTimeOffset.from({day, month, year, hour, minute, seconds, offset})
И каждый тип должен уметь две базовые операции
1. создание из примитивов
2. деструктурирование в примитивы

Соответсвенно вместо N! комбинаций нам надо протестировать всего 2N
А операция конверсии в прикладном коде будет вот такой DateTimeOffset.from({...date.toFields(), ...time.toFields, offset: serverOffset});

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

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

·>Что лично тебе я приписал?

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

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

·>А ты таки предложил новую абстракцию ввести в виде ApiToken.from.

ну да, ваше taraparam куда лучшая абстракция, не подкопаешься.

·>Именно. Зачем нам тогда нужен ApiToken.from?


Затем, зачем и ваш taraparam

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

·>Пример на то и пример, чтобы показать как использовать моки. А в твоём примере было показано как _не надо_ писать код.

Именно — так не надо, но именно так пишут и это типичный пример, к сожалению

P>>А вот если вы замокали jsonwebtoken, то ваши тесты сломаются. И причина только в том, что вы замокали там где этого не надо делать.

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

Я вам привел конкретный пример — одну библиотеку для jwt заменили другой. Один клиент для бд, заменили другим. Один хттп клиент заменили другим...

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

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

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

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

·>Слишком тяжелый подход. Только на мелких приложениях работает.

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

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

·>Это как? То что зависимости неявные — это не означает, что их нет.

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

P>>А если этого не делать, то и моки не нужны.

·>Ага-ага. Но тогда нужны ещё более медленные и неуклюжие интеграционные тесты, много

Интеграционные всех уровней нужны всегда. Их количество определяется
1. апи
2. сценариями

Большой апи — будет много тестов апи, иначе никак.
Много сценариев — будет много тестов e2e, и этого тоже не избежать.

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

·>require это нифига не DI, это SL

В огороде бузина, а в киеве дядька. При чем здесь require ? framework as a detail и clean architecture это подходы к дизайну, архитектуре.

P>>С вашими моками покрытие будет хуже, кода больше, а интеграционные всё равно нужны

·>Интеграционные тесты тестируют интеграцию крупных, тяжелых компонент. А у тебя они тестируют всю мелкую функциональность.

Интеграционные тесты начинаются после самых простых юнит-тестов. компонентные тесты — это уже интеграционные. Функциональные — снова интеграционные. Апи — они же.
Приложение — снова они же. Система — снова интеграционные. E2E, acceptance — интеграционнее некуда.

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

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

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


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

P>>Именно! И никакие моки этого не отменяют!
·>Они отменяют необходимость написания тучи этих самых xxxComposition, нет кода — нечего тестировать.

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

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

·>Не весь, а только покрывающий интеграцию компонент.

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

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

·>Это какая-то специфичная проблема в твоих проектах, я не в курсе.

Покажите как вы в репозиторий метод getAll передаете фильтры, сортировку итд

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

·>Задача инеграционного теста — тестировать места взаимодействия компонент, а не сценарии поведения.

Именно — кто даст гарантию что с тем или иным конфигом, рантаймом, переменными окружения, тот самый метод контролера не начнет вдруг гарантировано валиться ?

·>Как ассертить такую функцию в юнит-тесте? Как проверить, что текст запроса хотя бы синтаксически корректен? И как ассертить что параметры куда надо положены и транформация верна?


expect(query).to.deep.eq(pattern)


·>И далее, ну допустим ты как-то написал кучу тестов что getUser выглядит правильно. А дальше? Как убедиться, что настоящая субд будет работать с getUser именно так, как ты описал в ожиданиях теста репы?


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

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

·>Плохо, это функциональная бизнес-логика, а не интеграция. Это должно ловиться юнит-тестом, а не интеграционным.

Это типичный пример интеграция — вы соединяете выходы одного со входами другого.

P>>Проблему может вызвать ветвление в контроллере — он должен быть линейным.

·>Т.е. нужно писать интеграционный тест как минимум для каждого метода контроллера. Это слишком дохрена.

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

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

·>Т.е. ты пытаешься своё "Я так вижу" за какой-то объективный факт.

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

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

·>Чтобы описать варианты результата что может вернуть modifyUser.

Это называется стаб, а не мок

·>Это наверное для js актуально, где всё есть Object и где угодно может быть что угодно. При статической типизации вариантов разной природы не так уж много. Если modifyUser возвращает boolean, то тут только два варианта данных. А вот в js оно внезапно может выдать "FileNotFound" — и приехали.


И в джаве, и в дотнете полно разновидностей — ждете одно, приходит того же типа, похоже на то что надо, но чуточку другое. Бу-га-га.
А там где используют object, то отличий от js никакого нет

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

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

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

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

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

Вы там что, на каждый коммит всю систему переписываете?

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

·>Нет. Интеграционные тесты должны протестировать интеграцию, а не весь апи.

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

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

·>Не очень знаком с этой терминологией.

Типичная функциональность —
дай всё по страницам по фильтру на дату отсортированым


Вот здесь пространство решений чудовищное.

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

·>Если изоляция нужна, можно использовать интерфейсы.

Нужно. И девелопера на такое тренировать очень долго.

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

·>Ну ликбез же.

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

·>Похоже это просто N-ая итерация очередной вариации самой правильной организации трёх слоёв типичного веб-приложения. MVC, MVP, MVI, MVVM, MVVM-C, VIPER, и ещё куча...

·>Причём тут моки — неясно.

Старый подход — контролер вызывает сервис БД который вызывает БЛ через репозиторий итд. Моками надо обрезать зависимости на БД у БЛ.
Т.е. БЛ это клиент к БД, по факту.

В новом подходе БЛ, модель не имеет зависимости ни на репозиторий, на на БД. А раз так, то и моки не нужны.
Т.е. в центре у нас БЛ, модель, а вызывающий код является её клиентом, и именно там зависимости на БД итд.
Т.е. у нас слои поменялись местами.

·>Я вообще в последние N лет редко занимаюсь UI/web. У меня всякая поточка, очереди, сообщения, low/zero gc и прочее. Как там этот getUser сделать с zero gc — тот ещё вопрос.

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

Актуальность моков зависит от вашей архитектуры. Если мы делаем БЛ, модель без зависимостей к БД, то и моки не нужны для тестирования.

·>А что за версия яп? Ну не важно. Это ещё можно простить, это просто маппинг урлов на объекты при отсутствии схемы REST. Как туда запихать аудит? Как аудит-аннотация будет описывать что, куда, когда и как аудитить?


Покажите ваш типичный код с аудитом.

P>>Кое что надо и в самом методе вызывать, без этого работать не будет.

·>Именно! И накой тогда аннотация нужна для этого?

Базовые гарантии. Некоторые из них можно выключить или доопределить конфигом итд.

·>Строчки где видим? Аудит может сообщения куда-нибудь в сокет слать или в бд отдельную писать.

·>И где валидировать, что для данных сценариев записываются нужная инфа в аудит?

Разумеется, это интеграция — что аудит интегрирован с тем или иным стораджем. 1 тест.

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


Вот-вот. Только не ясно, чем вам мешают аннотации и зачем моки
Отредактировано 21.12.2023 13:41 Pauel . Предыдущая версия . Еще …
Отредактировано 21.12.2023 13:40 Pauel . Предыдущая версия .
Re[20]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 21.12.23 21:28
Оценка: +1
Здравствуйте, Pauel, Вы писали:

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

P>·>Т.е. проблема не в моках как таковых, а в том, что некоторые люди используют некоторые подходы и пишут корявый код?
P>Хорошесть инструмента это именно то как другие его понимают и умеют применять. Если код на моках в основном кривой — значит моки за 20 лет индустрия так и не осилила.
P>Вы вот сами путаете моки и стабы
Ок, пусть. Не вижу, в чём важность этой терминологии. Я использую название "мок", потому что библиотека называется mockito и создаются эти штуки функцией mock или аннотацией @Mock. Если тебе комфортнее стаб, пусть. Хоть горшком назови, только в печку не ставь.

>> Это проблема ликбеза, а не моков. Ровно та же проблема есть в любых подходах тестирования.

P>Даем слово Фаулеру:
P>

P>Mocks are objects pre-programmed with expectations which form a specification of the calls they are expected to receive

P>Видите — моки это про описание вызовов и связаные с этим ожидания
Я увидел главное: в твоём примере использовался некий jest mock и судя по его доке там, честно говоря, неясно как писать вменяемый код.

P>Вы же говорите про стабы. Если ваши утверждения в предыдущих постах поправить,заменив моки на стабы, будет более-менее понятно.

Ок, гуд.

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

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

P>>>А зачем вам эти AsHex, AsBytes? Это же просто преставление. У вас есть токен — это значение. Его можно создать парой-тройкой способов, и сохранить так же.

P>·>Именно. 3 способа создания x 3 способа представления — уже девять комбинаций. У тебя будет либо девять функций, либо своя обёртка над сторонним api, изоморфная по сложности тому же api.
P>Ну так в джаве и пишут — перемножают сущности и радуются.
Причём тут перемножают? Это диктуется требованиями. Если в json тебе нужен b64, а аналогичном месте protobuf тебе нужен Bytes, то хоть на bf пиши, у тебя будут комбинации.

P>Я портировал довольно много кода с джавы в тайпскрипт, и охренел от тамошних привычек.

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

P>Не нужны никакие "9 комбинаций" — нужно отделить создание и представление.

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

P>>> Отсюда ясно, что нет нужды плодить те функции, что вы предложили, а следовательно и тестировать их тоже не нужно.

P>·>Нужда идёт из кода, из требований. Если у тебя достаточно сложная система, у тебя будет там все 9 комбинаций, а то и больше.
P>Это у вас так будет, тут я охотно верю.
У кого "у нас"? Если бы никому не требовались разные виды токенов в разном представлении, то в библиотеке crypto был бы "makeToken" с ровно одним парамом.
Такое ощущение, что ты говоришь о дизайне какой-то домашней странички, а не о сложной системе.

P>>>Всех дел — отделить данные от представления, и количество тестов уменьшается.

P>·>Количество может быть уменьшено, только если ты какое-то требование перестал тестировать.
P>Количество может быть уменьшено за счет подхода к дизайну.
Я не понял, разговор был о crypto, теперь на какой-то time-api переключился. Я за твоим полётом мысли не успеваю.

P>А операция конверсии в прикладном коде будет вот такой DateTimeOffset.from({...date.toFields(), ...time.toFields, offset: serverOffset});

Это только на js годится такое писать. Со статической типизацией такое не прокатит.

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

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

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

P>·>Что лично тебе я приписал?
P>Читайте себя внимательно — вы регулярно утверждаете, что именно у меня проблемы с моками, что именно у меня квалификация под сомнением.
Так ты сам откровенно пишешь: "за 20 лет индустрия так и не осилила". Я сталкивался с другой индустрией, в которой с моками (или стабами?) проблем таких нет.

P>Прекратите заниматься ерундой — я пишу о том, что вижу в т.ч. в джава коде.

P>Кто его писал этот джава код, может вы и ваши друзья?
Не знаю о котором коде ты говоришь.

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

P>·>А ты таки предложил новую абстракцию ввести в виде ApiToken.from.
P>ну да, ваше taraparam куда лучшая абстракция, не подкопаешься.
Чё? Абстракция уже была, в crypto api. Но тебе она показалась слишком сложной и ты сделал partial application.

P>·>Именно. Зачем нам тогда нужен ApiToken.from?

P>Затем, зачем и ваш taraparam
Чё?

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

P>·>Пример на то и пример, чтобы показать как использовать моки. А в твоём примере было показано как _не надо_ писать код.
P>Именно — так не надо, но именно так пишут и это типичный пример, к сожалению
Может в твоей индустрии. В java так не пишут, ну если только совсем вчерашние студенты, но PR такое не пройдёт.

P>>>А вот если вы замокали jsonwebtoken, то ваши тесты сломаются. И причина только в том, что вы замокали там где этого не надо делать.

P>·>В примере никакого jwt токена не было. Я не знаю ты о чём, у тебя где-то в голове какая-то более широкая картина, но к сожалению, я не очень хороший телепат.
P>Я вам привел конкретный пример — одну библиотеку для jwt заменили другой. Один клиент для бд, заменили другим. Один хттп клиент заменили другим...
Где что и как замокали — я не знаю. О чём ты тут споришь — вообще непонятно.

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

P>>>Для этого есть интеграционный тест
P>·>Жуть. Т.е. ты не можешь что-то протестировать быстрыми тестами, приходится плодить медленные интеграционные и тривиальные ошибки обнаруживать только после запуска всего приложения. Спасибо, не хочу.
P>Интеграционные тесты это все выше уровня простых юнит тестов. Далеко не все интеграционные автоматически медленные. Условно, чистая функция вызывает десяток других чистых — опаньки, тест такой штуки будет интеграционным. Но нам это по барабану — они быстрые, можно обзапускаться хоть до посинения.
Ты куда-то мимо пошел. Не хочу вдаваться в подробности терминологии юнит-инт. Суть в том, что в твоём предложенном тесте появляется connectionString, значит "приходится плодить медленные тесты и тривиальные ошибки обнаруживать только после запуска всего приложения".

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

P>·>Слишком тяжелый подход. Только на мелких приложениях работает.
P>Наоборот. Большое количество дешовых тестов, тысячи минимум, и десятки-сотни интеграционных, самых тяжелых тестов по одному на каждый сценарий приложения — десяток другой.
Какой тесто обнаружит ошибку в коде "repository.updateUser(newUser, oldUser)" и как? Вот это "выполним операцию и проверим, что состояние юзера соответствует ожиданиям"? Что должно быть поднято для прогона такого теста?

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

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

P>>>А если этого не делать, то и моки не нужны.

P>·>Ага-ага. Но тогда нужны ещё более медленные и неуклюжие интеграционные тесты, много
P>Интеграционные всех уровней нужны всегда. Их количество определяется
P>1. апи
P>2. сценариями
P>Большой апи — будет много тестов апи, иначе никак.
P>Много сценариев — будет много тестов e2e, и этого тоже не избежать.
Под интеграцией я понимал в данном случае твоё "curl -X POST". Это плохой тест, медленный, неуклюжий. Число таких тестов надо стараться минимизировать.

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

P>·>require это нифига не DI, это SL
P>В огороде бузина, а в киеве дядька. При чем здесь require ? framework as a detail и clean architecture это подходы к дизайну, архитектуре.
В том, что в твоём коде jest подменял код внутри глобального реестра require.

P>>>С вашими моками покрытие будет хуже, кода больше, а интеграционные всё равно нужны

P>·>Интеграционные тесты тестируют интеграцию крупных, тяжелых компонент. А у тебя они тестируют всю мелкую функциональность.
P>Интеграционные тесты начинаются после самых простых юнит-тестов. компонентные тесты — это уже интеграционные. Функциональные — снова интеграционные. Апи — они же.
P>Приложение — снова они же. Система — снова интеграционные. E2E, acceptance — интеграционнее некуда.
whatever, не хочу в буквоедство играть.

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

P>·>Очень вряд-ли, но допустим даже что-то вальнулось в проде. Изучив логи прода, ты с моками сможешь быстро воспроизвести сценарий и получить быстрый красный тест, который можно спокойно отлаживать и фиксить, без всякой интеграции.
P>С учетом того, что покрывали логированием вы под ваши моки из другой реальности, на проде может и не оказаться тех самых логов, что вам нужны в том самом месте.
Это твои фантазии.

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

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

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

P>·>Не весь, а только покрывающий интеграцию компонент.
P>Тесты апи — интеграционные, и их должны быть минимум на количество ваших методов контроллеров, которые торчат наружу.
P>Мало ли по какой причине тот или иной контролер перестал вызываться снаружи — конфиг, переменные окружения, версия ос, рантайма, итд итд
P>А вы тут сказки рассказыете, что это делать не надо
Суть в том, что тесты апи можно тестировать без реальной субд|кафки и т.п. И описывать ожидания, что updateUser сделан именно с (newUser, oldUser), а не наоборот.

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

P>·>Это какая-то специфичная проблема в твоих проектах, я не в курсе.
P>Покажите как вы в репозиторий метод getAll передаете фильтры, сортировку итд
Не знаю, не припомню где у нас есть такое. Но наверное был бы какой-нибдуь value object, который передаёт необходимые настройки фильтров и сортировок.

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

P>·>Задача инеграционного теста — тестировать места взаимодействия компонент, а не сценарии поведения.
P>Именно — кто даст гарантию что с тем или иным конфигом, рантаймом, переменными окружения, тот самый метод контролера не начнет вдруг гарантировано валиться ?
Никто.

P>·>Как ассертить такую функцию в юнит-тесте? Как проверить, что текст запроса хотя бы синтаксически корректен? И как ассертить что параметры куда надо положены и транформация верна?

P>
P>expect(query).to.deep.eq(pattern)
P>

Ты не ответил ни на один заданный вопрос. Напомню: этот самый query был "select * from users where id=? + параметры + трансформации".

P>·>И далее, ну допустим ты как-то написал кучу тестов что getUser выглядит правильно. А дальше? Как убедиться, что настоящая субд будет работать с getUser именно так, как ты описал в ожиданиях теста репы?

P>вот, вы начинате понимать, для чего нужны интеграционные Здесь вам понадобится тест компонента с подклееной реальной бд. И без этого никак.
Т.е. та же ж, вид в профиль, но теперь большинство логики без реальной бд не протестировать. В случае с репо реальная бд нужна только для тестирования самого слоя взаимодействия с бд, а остальное может мокаться (стабиться?). Ты же предлагаешь всё приложение тестировать с реальной бд.

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

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

P>>>Проблему может вызвать ветвление в контроллере — он должен быть линейным.

P>·>Т.е. нужно писать интеграционный тест как минимум для каждого метода контроллера. Это слишком дохрена.
P>Это в любом случае необходимо — у контролера всегда хрензнаетсколькилион зависимостей прямых и косвенных, и чтото да может пойти не так.
Для этого и объединяют толпу зависимостей в одну. Чтобы не N! было.

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

P>·>Т.е. ты пытаешься своё "Я так вижу" за какой-то объективный факт.
P>Для начала это вы пользуетесь местечковым понимаем моков, а не смотрите как это определено ну у того же Фаулера.
Возможно, это всё терминология.

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

P>·>Чтобы описать варианты результата что может вернуть modifyUser.
P>Это называется стаб, а не мок
Ок, пусть.

P>·>Это наверное для js актуально, где всё есть Object и где угодно может быть что угодно. При статической типизации вариантов разной природы не так уж много. Если modifyUser возвращает boolean, то тут только два варианта данных. А вот в js оно внезапно может выдать "FileNotFound" — и приехали.

P>И в джаве, и в дотнете полно разновидностей — ждете одно, приходит того же типа, похоже на то что надо, но чуточку другое. Бу-га-га.
P>А там где используют object, то отличий от js никакого нет
Ну обычно object стараются избегать использовать.

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

P>·>Вот тут тебе и аукнется цикломатическая сложность. Чтобы покрыть весть трафик — надо будет выполнить все возможные пути выполнения. Что просто дофига и очень долго.
P>Это вы бы так делали. А здесь стоит вспомнить что такое апи тесты, и покрыть все что описании этого апи — как минимум роуты.
P>Иначе есть шанс что тот самый роут в нужный момент не сможет быть вызван, т.к. он просто задизаблен в конфиге
Не понял, ну раз задизаблен, значит и нельзя вызвать. Это является не ошибкой, а конфигурацией системы. Неясно что тут в принципе тестировать.

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

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

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

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

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

P>·>Не очень знаком с этой терминологией.
P>Типичная функциональность —
P>
P>дай всё по страницам по фильтру на дату отсортированым
P>

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

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

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

P>·>Похоже это просто N-ая итерация очередной вариации самой правильной организации трёх слоёв типичного веб-приложения. MVC, MVP, MVI, MVVM, MVVM-C, VIPER, и ещё куча...

P>·>Причём тут моки — неясно.
P>Старый подход — контролер вызывает сервис БД который вызывает БЛ через репозиторий итд. Моками надо обрезать зависимости на БД у БЛ.
P>Т.е. БЛ это клиент к БД, по факту.
Моками (стабами?) обрезается БД и прочие внешние вещи от БЛ. Чтобы при тестировании БЛ можно было быстро тестировать логику кода, без всяких сетевых соединений и запуска всего приложения.

P>В новом подходе БЛ, модель не имеет зависимости ни на репозиторий, на на БД. А раз так, то и моки не нужны.

P>Т.е. в центре у нас БЛ, модель, а вызывающий код является её клиентом, и именно там зависимости на БД итд.
P>Т.е. у нас слои поменялись местами.
Да пофиг. Эти слои можно жонглировать как сейчас модно. Это меняется каждый год.

P>·>Я вообще в последние N лет редко занимаюсь UI/web. У меня всякая поточка, очереди, сообщения, low/zero gc и прочее. Как там этот getUser сделать с zero gc — тот ещё вопрос.

P>·>Моки же общий инструмент, от деталей архитектуры зависит мало.
P>Актуальность моков зависит от вашей архитектуры. Если мы делаем БЛ, модель без зависимостей к БД, то и моки не нужны для тестирования.
Лично для меня главное разделение — это конструкты ЯП и всё что снаружи от ЯП. Когда код не лезет куда-то наружу, без сокетов, файлов, да если ещё и без многопоточки, и т.п., то тестировать такой код — одно удовольствие.
А как компоненты называются, controller, presenter, interactor, как взаимодействуют — это мелочи жизни, не знаю зачем это всегда все так громко обсуждать любят.

P>·>А что за версия яп? Ну не важно. Это ещё можно простить, это просто маппинг урлов на объекты при отсутствии схемы REST. Как туда запихать аудит? Как аудит-аннотация будет описывать что, куда, когда и как аудитить?

P>Покажите ваш типичный код с аудитом.
А что там показывать? В нужном месте стоит audit.send(new AuditMessage(....)) или типа того. Повторюсь, это всё не важно какие конкретно это компоненты, по итогу это просто всё сводится к вызовам методов с параметрами.

P>>>Кое что надо и в самом методе вызывать, без этого работать не будет.

P>·>Именно! И накой тогда аннотация нужна для этого?
P>Базовые гарантии. Некоторые из них можно выключить или доопределить конфигом итд.
Базовые гарантии чего?

P>·>Строчки где видим? Аудит может сообщения куда-нибудь в сокет слать или в бд отдельную писать.

P>·>И где валидировать, что для данных сценариев записываются нужная инфа в аудит?
P>Разумеется, это интеграция — что аудит интегрирован с тем или иным стораджем. 1 тест.
"для данных сценариев записываются нужная инфа". Это где-то нужно валидировать, что для некой бизнес-операции в соответвтующих условиях записывается соответвтующий требованиям сообщение аудита.

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

P>Вот-вот. Только не ясно, чем вам мешают аннотации и зачем моки
Как я понял скорее всего разница в том, что я обычно пишу на статике. Аннотации мешают тем, что их сложнее тестировать, контролировать типы. Приходится вместо простых вызовов методов поднимать очень много компонент и смотреть, что процессоры аннотаций сработали правильно. Т.е. вместо того, чтобы просто вызвать метод, мы пишем кучу слоёв какого-то мусора, чтобы вызывать ровно тот же метод.
А ты используешь динамические яп, и компилятор и так ничего делать не может, а значит и нечего терять.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[21]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 22.12.23 13:37
Оценка: +1
Здравствуйте, ·, Вы писали:

P>>Вы вот сами путаете моки и стабы

·>Ок, пусть. Не вижу, в чём важность этой терминологии. Я использую название "мок", потому что библиотека называется mockito и создаются эти штуки функцией mock или аннотацией @Mock. Если тебе комфортнее стаб, пусть. Хоть горшком назови, только в печку не ставь.

Моки изначально предназначались для тестов вида "вызываем вон то, вон так, в такой последовательности"

P>>Ну так в джаве и пишут — перемножают сущности и радуются.

·>Причём тут перемножают? Это диктуется требованиями. Если в json тебе нужен b64, а аналогичном месте protobuf тебе нужен Bytes, то хоть на bf пиши, у тебя будут комбинации.

У вас — охотно верю. Я вам пример приводил, про время, вам он ожидаемо не понравился — язык не тот

P>>Я портировал довольно много кода с джавы в тайпскрипт, и охренел от тамошних привычек.

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

С чего бы портирование стало означать удаление легаси ?

P>>Не нужны никакие "9 комбинаций" — нужно отделить создание и представление.

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

В данном случае мне не надо переживать за поломаные тесты, если понадобится другой алгоритм, другая либа, итд.
Вот понадобился не hmac а что другое — в файлике samples поменял десяток значений, и всё
А ваша альтернатива этому — еще и тесты перебулшитить.

·>У кого "у нас"? Если бы никому не требовались разные виды токенов в разном представлении, то в библиотеке crypto был бы "makeToken" с ровно одним парамом.

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

Ну да, этож только у вас сложные системы.

P>>Количество может быть уменьшено за счет подхода к дизайну.

·>Я не понял, разговор был о crypto, теперь на какой-то time-api переключился. Я за твоим полётом мысли не успеваю.

У вас любой из примеров вызывает какой то негатив

P>>А операция конверсии в прикладном коде будет вот такой DateTimeOffset.from({...date.toFields(), ...time.toFields, offset: serverOffset});

·>Это только на js годится такое писать. Со статической типизацией такое не прокатит.

Успокойтесь — это все статически контролируется, только в вашу джаву такие фичи еще не завезли.

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

·>Я вроде рассказал, как упрощать тестирование комбинаций.

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

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

·>Так ты сам откровенно пишешь: "за 20 лет индустрия так и не осилила". Я сталкивался с другой индустрией, в которой с моками (или стабами?) проблем таких нет.

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

P>>Именно — так не надо, но именно так пишут и это типичный пример, к сожалению

·>Может в твоей индустрии. В java так не пишут, ну если только совсем вчерашние студенты, но PR такое не пройдёт.

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

Эта разновидность тестов нужна и вам.

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

·>Какой тесто обнаружит ошибку в коде "repository.updateUser(newUser, oldUser)" и как? Вот это "выполним операцию и проверим, что состояние юзера соответствует ожиданиям"? Что должно быть поднято для прогона такого теста?

да вобщем то ничего, какая нибудь ин-мемори дб и инстанц контроллера или юз-кейса.

P>>·>Ага-ага. Но тогда нужны ещё более медленные и неуклюжие интеграционные тесты, много

P>>Интеграционные всех уровней нужны всегда. Их количество определяется
P>>1. апи
P>>2. сценариями
P>>Большой апи — будет много тестов апи, иначе никак.
P>>Много сценариев — будет много тестов e2e, и этого тоже не избежать.
·>Под интеграцией я понимал в данном случае твоё "curl -X POST". Это плохой тест, медленный, неуклюжий. Число таких тестов надо стараться минимизировать.

Такие тесты нужны в любом случае, моки-стабы от них не избавляют, это иллюзия.

P>>В огороде бузина, а в киеве дядька. При чем здесь require ? framework as a detail и clean architecture это подходы к дизайну, архитектуре.

·>В том, что в твоём коде jest подменял код внутри глобального реестра require.

Я вам пример привел, как применяют моки. Обмокать можно что угодно, в т.ч. и в джаве-дотнете

P>>·>Они отменяют необходимость написания тучи этих самых xxxComposition, нет кода — нечего тестировать.

P>>Неверно — тестируется не код, а соответствие ожиданиям, требованиями. какой бы дизайн у вас ни был — все равно нужны тесты апи, сценариев итд.
·>Опять буквоедство какое-то. Что по-твоему тестируется на соответствие ожиданиям, требованиям?

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

P>>А вы тут сказки рассказыете, что это делать не надо

·>Суть в том, что тесты апи можно тестировать без реальной субд|кафки и т.п. И описывать ожидания, что updateUser сделан именно с (newUser, oldUser), а не наоборот.

Можно. И это делается без моков. Если мы заложимся на значение или состояние, то получим куда больше гарантий.
В функции средний девелопер может понаделывать ошибок куда больше, чем перепутать old и new.

P>>Покажите как вы в репозиторий метод getAll передаете фильтры, сортировку итд

·>Не знаю, не припомню где у нас есть такое. Но наверное был бы какой-нибдуь value object, который передаёт необходимые настройки фильтров и сортировок.

Вот вы и встряли Чем лучше изолируете, тем больше вам тестировать. Тут помогло бы использование какого dsl. В том же дотнете есть такое, и всё равно надо думать, что передать в репозиторий, быть в курсе, съест ли тамошний провайдер конкретную функцию или нет.

P>>Именно — кто даст гарантию что с тем или иным конфигом, рантаймом, переменными окружения, тот самый метод контролера не начнет вдруг гарантировано валиться ?

·>Никто.

Именно! А потому нам все контроллеры нужно тестировать в апи тестах — тем самым мы исключаем добрый десяток классов проблем.

P>>
P>>expect(query).to.deep.eq(pattern)
P>>

·>Ты не ответил ни на один заданный вопрос. Напомню: этот самый query был "select * from users where id=? + параметры + трансформации".

Что вас смущает? deep.eq проверит всё, что надо. query это не строка, объект с конкретной структурой, чтото навроде
{
  sql: 'select * from users where id=?',
  params: [id],
  transformations: {
     in: [fn1],
     out: [fn2]
  }
}


Сами трансформации тестируются отдельно, обычными юнитами, например expect(fn1(user)).to.eq(user.id);

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

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

Вы все думаете в единицах старого дизайна, когда БЛ прибита гвоздями к бд.

P>>Это типичный пример интеграция — вы соединяете выходы одного со входами другого.

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

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

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

·>Для этого и объединяют толпу зависимостей в одну. Чтобы не N! было.

Объединяйте что угодно — а контролер все равно придется вызвать в АПИ тестах.

P>>А там где используют object, то отличий от js никакого нет

·>Ну обычно object стараются избегать использовать.

Шота я этого за 4 года не заметил

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

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

Юзеру так и скажете — мы не можем вернуть ваши деньги, потому что у нас в конфигурации возвраты отключены.

P>>Вы там что, на каждый коммит всю систему переписываете?

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

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

·>Апи это про интерфейс системы. Интеграция тут причём?

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

P>>Старый подход — контролер вызывает сервис БД который вызывает БЛ через репозиторий итд. Моками надо обрезать зависимости на БД у БЛ.

P>>Т.е. БЛ это клиент к БД, по факту.
·>Моками (стабами?) обрезается БД и прочие внешние вещи от БЛ. Чтобы при тестировании БЛ можно было быстро тестировать логику кода, без всяких сетевых соединений и запуска всего приложения.

Можно. Это старый подход.

P>>В новом подходе БЛ, модель не имеет зависимости ни на репозиторий, на на БД. А раз так, то и моки не нужны.

P>>Т.е. в центре у нас БЛ, модель, а вызывающий код является её клиентом, и именно там зависимости на БД итд.
P>>Т.е. у нас слои поменялись местами.
·>Да пофиг. Эти слои можно жонглировать как сейчас модно. Это меняется каждый год.

Это новый подход, позволяет обходиться без моков-стабов итд и свести тестирование ключевых вещей к дешовым юнит-тестам

P>>>>Кое что надо и в самом методе вызывать, без этого работать не будет.

P>>·>Именно! И накой тогда аннотация нужна для этого?
P>>Базовые гарантии. Некоторые из них можно выключить или доопределить конфигом итд.
·>Базовые гарантии чего?

Например, если вы аннотацией указываете что метод работает из под другого юзера, например Freemium, то базовая гарантия — вы будете получать Freemium юзера внутри этого метода. Ее можно модифицировать — отключить весь Freemium в конфиге приложения. Или навесить на Freemium юзера доп.свойства — свежегенеренное имя-емейл-баланс.

P>>Разумеется, это интеграция — что аудит интегрирован с тем или иным стораджем. 1 тест.

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

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

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


Вот эти вещи нужно выталкивать во фремворк, что бы там протестировать раз на все времена, и не надо было каждый раз перепроверять "а не отвалился ли процессор аннотации"
Вы же не проверяете, что джава выполняет циклы правильно? Так и здесь
Re[22]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 22.12.23 22:39
Оценка:
Здравствуйте, Pauel, Вы писали:

P>>>Вы вот сами путаете моки и стабы

P>·>Ок, пусть. Не вижу, в чём важность этой терминологии. Я использую название "мок", потому что библиотека называется mockito и создаются эти штуки функцией mock или аннотацией @Mock. Если тебе комфортнее стаб, пусть. Хоть горшком назови, только в печку не ставь.
P>Моки изначально предназначались для тестов вида "вызываем вон то, вон так, в такой последовательности"
Может быть. Неясно какое это имеет значение сейчас, так исторические курьёзны. Даже если посмотреть на историю библиотек для тестов — за много лет много чего менялось и переделывалось.

P>>>Ну так в джаве и пишут — перемножают сущности и радуются.

P>·>Причём тут перемножают? Это диктуется требованиями. Если в json тебе нужен b64, а аналогичном месте protobuf тебе нужен Bytes, то хоть на bf пиши, у тебя будут комбинации.
P>У вас — охотно верю.
Потому что у нас не хелловорды, а не как у вас, да?

P>Я вам пример приводил, про время, вам он ожидаемо не понравился — язык не тот

Я не понял к чему ты скачешь с одного на другое. Обсуждали статью и MaitreD, потом crypto api, теперь вот ещё time api зачем-то...

P>>>Я портировал довольно много кода с джавы в тайпскрипт, и охренел от тамошних привычек.

P>·>Портировал это значит, как минимум, ты по ходу удалял гору легаси кода, совместимость с устаревшим, етс.
P>С чего бы портирование стало означать удаление легаси ?
С того. Когда код существует хотя бы десяток лет — там наслоения чего попало, когда что-то делалось и переделывалось, много накапливается мусора "по историческим причинам".

P>>>Не нужны никакие "9 комбинаций" — нужно отделить создание и представление.

P>·>Так в изначальном дизайне crypto так и было — создаём hmac нужного алгоритма, потом выводим в нужном представлении. Ты почему-то упаковал это в одну функцию и назвал успехом.
P>В данном случае мне не надо переживать за поломаные тесты, если понадобится другой алгоритм, другая либа, итд.
P>Вот понадобился не hmac а что другое — в файлике samples поменял десяток значений, и всё
Почему десяток? У тебя только десяток операций в api?

P>А ваша альтернатива этому — еще и тесты перебулшитить.

Один, скорее всего. Ибо одного теста достаточно чтобы зафиксировать логику вычисления hmac. В остальных тестах достаточно ассертить его наличие, но не конкретный формат.

P>·>У кого "у нас"? Если бы никому не требовались разные виды токенов в разном представлении, то в библиотеке crypto был бы "makeToken" с ровно одним парамом.

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

P>>>Количество может быть уменьшено за счет подхода к дизайну.

P>·>Я не понял, разговор был о crypto, теперь на какой-то time-api переключился. Я за твоим полётом мысли не успеваю.
P>У вас любой из примеров вызывает какой то негатив
Какой негатив? Вроде говорили об одном, теперь чего-то новое.
Вообще неясно причём тут time api и причём тут моки и тестирование вообще.

P>>>А операция конверсии в прикладном коде будет вот такой DateTimeOffset.from({...date.toFields(), ...time.toFields, offset: serverOffset});

P>·>Это только на js годится такое писать. Со статической типизацией такое не прокатит.
P>Успокойтесь — это все статически контролируется, только в вашу джаву такие фичи еще не завезли.
Спасибо, но сводить всё к массиву из 7 чиселок — это фигня полная и издевательство. Даже такое DateTimeOffset.from({...time.toFields, ...date.toFields(), offset: serverOffset}); — и всё поехало. И где наносекуды, интересно?
Конструкторы удобнее и надёжнее с нужными типами, например, OffsetDateTime.of(date, time, offset), это хотя бы читабельно, и если что-то не так, не компиляется.

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

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

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

P>·>Так ты сам откровенно пишешь: "за 20 лет индустрия так и не осилила". Я сталкивался с другой индустрией, в которой с моками (или стабами?) проблем таких нет.
P>Ну да — для вас перебулшитить тесты проблемой не является Еще и настаиваете, что так и надо.
Я настаиваю, что так не надо.

P>>>Именно — так не надо, но именно так пишут и это типичный пример, к сожалению

P>·>Может в твоей индустрии. В java так не пишут, ну если только совсем вчерашние студенты, но PR такое не пройдёт.
P>Проходит же. Вот я и интересуюсь, кто настрочил горы непотребства
Вчерашние студенты?

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

P>·>Ты куда-то мимо пошел. Не хочу вдаваться в подробности терминологии юнит-инт. Суть в том, что в твоём предложенном тесте появляется connectionString, значит "приходится плодить медленные тесты и тривиальные ошибки обнаруживать только после запуска всего приложения".
P>Эта разновидность тестов нужна и вам.
Нужны, но не для проверки каждой строчки кода, что параметры вызова не перепутаны. А только для тестирования интеграции.

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

P>·>Какой тесто обнаружит ошибку в коде "repository.updateUser(newUser, oldUser)" и как? Вот это "выполним операцию и проверим, что состояние юзера соответствует ожиданиям"? Что должно быть поднято для прогона такого теста?
P>да вобщем то ничего, какая нибудь ин-мемори дб и инстанц контроллера или юз-кейса.
Ясно, закопайте.

P>·>Под интеграцией я понимал в данном случае твоё "curl -X POST". Это плохой тест, медленный, неуклюжий. Число таких тестов надо стараться минимизировать.

P>Такие тесты нужны в любом случае, моки-стабы от них не избавляют, это иллюзия.
Я знаю, что нужны, Я написал, что "Число таких тестов надо стараться минимизировать".

P>>>В огороде бузина, а в киеве дядька. При чем здесь require ? framework as a detail и clean architecture это подходы к дизайну, архитектуре.

P>·>В том, что в твоём коде jest подменял код внутри глобального реестра require.
P>Я вам пример привел, как применяют моки. Обмокать можно что угодно, в т.ч. и в джаве-дотнете
Я не знаю кто так применяет, но так делать не надо.

P>>>Неверно — тестируется не код, а соответствие ожиданиям, требованиями. какой бы дизайн у вас ни был — все равно нужны тесты апи, сценариев итд.

P>·>Опять буквоедство какое-то. Что по-твоему тестируется на соответствие ожиданиям, требованиям?
P>Продукт, система, приложение, апи, компонент, модуль. Для теста именно кода у нас другие инструменты — код-ревью, линт, дизайн-ревью, статические анализаторы, итд.
Я сказал "Они отменяют необходимость написания тучи этих самых xxxComposition, нет кода — нечего тестировать". Ты прикопаться решил к слову код? Ок, пусть будет не код, а модуль-компонент.
"Моки отменяют необходимость написания тучи этих самых xxxComposition, нет такого компонента|модуля — нечего тестировать." Так яснее?

P>>>А вы тут сказки рассказыете, что это делать не надо

P>·>Суть в том, что тесты апи можно тестировать без реальной субд|кафки и т.п. И описывать ожидания, что updateUser сделан именно с (newUser, oldUser), а не наоборот.
P>Можно. И это делается без моков. Если мы заложимся на значение или состояние, то получим куда больше гарантий.
Без моков ты предложил только полный запуск всей аппликухи, в лучшем случае с тестовой субд. Kafka/amps/lbm/solace/tibco/etc, кстати, тоже тестовую будешь пускать? Или как?

P>В функции средний девелопер может понаделывать ошибок куда больше, чем перепутать old и new.

Я в этом и до среднего не дотягиваю, дохрена таких ошибок делаю, но вся эта фигня на раз ловится тестами, c моками (стабами), да.

P>>>Покажите как вы в репозиторий метод getAll передаете фильтры, сортировку итд

P>·>Не знаю, не припомню где у нас есть такое. Но наверное был бы какой-нибдуь value object, который передаёт необходимые настройки фильтров и сортировок.
P>Вот вы и встряли Чем лучше изолируете, тем больше вам тестировать. Тут помогло бы использование какого dsl. В том же дотнете есть такое, и всё равно надо думать, что передать в репозиторий, быть в курсе, съест ли тамошний провайдер конкретную функцию или нет.
Пофиг. Я не знаю о чём ты говоришь и делаю wild guesses.

P>>>Именно — кто даст гарантию что с тем или иным конфигом, рантаймом, переменными окружения, тот самый метод контролера не начнет вдруг гарантировано валиться ?

P>·>Никто.
P>Именно! А потому нам все контроллеры нужно тестировать в апи тестах — тем самым мы исключаем добрый десяток классов проблем.
И эти тесты дадут тебе гарантию?

P>Что вас смущает? deep.eq проверит всё, что надо. query это не строка, объект с конкретной структурой, чтото навроде

P>
P>{
P>  sql: 'select * from users where id=?',
P>  params: [id],
P>  transformations: {
P>     in: [fn1],
P>     out: [fn2]
P>  }
P>}
P>

Жуть. Ну ладно. Т.е. тут мы тестируем репозиторий и ассертим текст sql
Как проверить, что текст запроса хотя бы синтаксически корректен? И что он при выполнении выдаст то, что ожидается? И как ты собрался тут подменять субд? Ты обещал, что теперь эту хрень можно отдать любой db и оно выдаст одинаковый результат? Даже если эта субд это oracle или cassandra? Да там синтаксис банально другой.

P>Сами трансформации тестируются отдельно, обычными юнитами, например expect(fn1(user)).to.eq(user.id);

Так ведь надо трансформировать не некий user, а конкретно ResultSet возвращённый из "select *". Что колоночки в select местами не перепутаны, как минимум.

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

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

P>·>Т.е. та же ж, вид в профиль, но теперь большинство логики без реальной бд не протестировать. В случае с репо реальная бд нужна только для тестирования самого слоя взаимодействия с бд, а остальное может мокаться (стабиться?). Ты же предлагаешь всё приложение тестировать с реальной бд.
P>Вы все думаете в единицах старого дизайна, когда БЛ прибита гвоздями к бд.
Не к бд прибита. БЛ прибивается к репе.

P>>>Это типичный пример интеграция — вы соединяете выходы одного со входами другого.

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

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

P>·>Для этого и объединяют толпу зависимостей в одну. Чтобы не N! было.
P>Объединяйте что угодно — а контролер все равно придется вызвать в АПИ тестах.
Не знаю что за апи тесты ты тут имеешь в виду, предполагаю, что это твой "curl". Если так, то достаточно позвать один метод для проверки интеграции всех компонент, а не для тестирования всей бизнес-логики апи.

P>>>А там где используют object, то отличий от js никакого нет

P>·>Ну обычно object стараются избегать использовать.
P>Шота я этого за 4 года не заметил
Может у тебя проекты написанные вайтишниками после курса ML, я не знаю. Но у тебя есть целый гитхаб, погляди насколько часто и для чего Object используют.

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

P>·>Не понял, ну раз задизаблен, значит и нельзя вызвать. Это является не ошибкой, а конфигурацией системы. Неясно что тут в принципе тестировать.
P>Юзеру так и скажете — мы не можем вернуть ваши деньги, потому что у нас в конфигурации возвраты отключены.
Для этого и есть L1 support — "проверьте, что ваш компьютер включен".

P>>>Вы там что, на каждый коммит всю систему переписываете?

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

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

P>·>Апи это про интерфейс системы. Интеграция тут причём?
P>Если апи возвращает то, что надо, это значит что у вас связка конфиг-di-sl-рантайм-переменные-итд склеено правильно.
И это ты предлагаешь гонять на каждый "updateUser(newUser, oldUser)"? Спасибо, не надо нам такого щастя.

P>>>Старый подход — контролер вызывает сервис БД который вызывает БЛ через репозиторий итд. Моками надо обрезать зависимости на БД у БЛ.

P>>>Т.е. БЛ это клиент к БД, по факту.
P>·>Моками (стабами?) обрезается БД и прочие внешние вещи от БЛ. Чтобы при тестировании БЛ можно было быстро тестировать логику кода, без всяких сетевых соединений и запуска всего приложения.
P>Можно. Это старый подход.
Ты говоришь как будто это что-то плохое. Это разумный подход. Делить не по тому, что сейчас у фаулеров в моде, а по конкретному осязаемому критерию, т.к. это напрямую выражается во времени запуска и требуемых ресурсов для тестирования.

P>·>Да пофиг. Эти слои можно жонглировать как сейчас модно. Это меняется каждый год.

P>Это новый подход, позволяет обходиться без моков-стабов итд и свести тестирование ключевых вещей к дешовым юнит-тестам
У тебя тривиальное updateUser(newUser, oldUser) не тестируется юнитами. Любая мелочёвка требует запуска всего, в лучшем случае с in-mem субд.

P>>>>>Кое что надо и в самом методе вызывать, без этого работать не будет.

P>>>·>Именно! И накой тогда аннотация нужна для этого?
P>>>Базовые гарантии. Некоторые из них можно выключить или доопределить конфигом итд.
P>·>Базовые гарантии чего?
P>Например, если вы аннотацией указываете что метод работает из под другого юзера, например Freemium, то базовая гарантия — вы будете получать Freemium юзера внутри этого метода. Ее можно модифицировать — отключить весь Freemium в конфиге приложения. Или навесить на Freemium юзера доп.свойства — свежегенеренное имя-емейл-баланс.
С такими же гарантиями можно и просто метод дёргать. Аннотации это только от плохого дизайна.

P>>>Разумеется, это интеграция — что аудит интегрирован с тем или иным стораджем. 1 тест.

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

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

P>Вот эти вещи нужно выталкивать во фремворк, что бы там протестировать раз на все времена, и не надо было каждый раз перепроверять "а не отвалился ли процессор аннотации"
P>Вы же не проверяете, что джава выполняет циклы правильно? Так и здесь
Фреймворк-ориентед программинг? Нет, спасибо.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[23]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 23.12.23 12:52
Оценка: +1
Здравствуйте, ·, Вы писали:

P>>Моки изначально предназначались для тестов вида "вызываем вон то, вон так, в такой последовательности"

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

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

P>>·>Причём тут перемножают? Это диктуется требованиями. Если в json тебе нужен b64, а аналогичном месте protobuf тебе нужен Bytes, то хоть на bf пиши, у тебя будут комбинации.

P>>У вас — охотно верю.
·>Потому что у нас не хелловорды, а не как у вас, да?

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

P>>Я вам пример приводил, про время, вам он ожидаемо не понравился — язык не тот

·>Я не понял к чему ты скачешь с одного на другое. Обсуждали статью и MaitreD, потом crypto api, теперь вот ещё time api зачем-то...

Со статьё давно всё ясно. crypto api и time это пример, которые показывают, за счет можно отказаться от моков, и что будет, если этого не делать.

P>>·>Так в изначальном дизайне crypto так и было — создаём hmac нужного алгоритма, потом выводим в нужном представлении. Ты почему-то упаковал это в одну функцию и назвал успехом.

P>>В данном случае мне не надо переживать за поломаные тесты, если понадобится другой алгоритм, другая либа, итд.
P>>Вот понадобился не hmac а что другое — в файлике samples поменял десяток значений, и всё
·>Почему десяток? У тебя только десяток операций в api?

Это у вас на моках упадут все тесты всех операций апи. О том и речь. А на обычных юнит-тестах вы подкидываете новый набор значений, и всё путём.

P>>А ваша альтернатива этому — еще и тесты перебулшитить.

·>Один, скорее всего. Ибо одного теста достаточно чтобы зафиксировать логику вычисления hmac. В остальных тестах достаточно ассертить его наличие, но не конкретный формат.
"достаточно ассертить его наличие" — вот вам и привязка к "как написано"

Ага, тесты "как написано" Вам придется написать много больше, чем один тест — как минимум, пространство входов более-менее покрыть. Моками вы вспотеете это покрывать.

P>>Ну да, этож только у вас сложные системы.

·>Ну да, сам же сказал, что у тебя всё помещается в один файлик. Или он гиговый?

Вы там в телепатию заигрались? Что именно по вашему помещается в один файлик? Вся система, все тесты, вся система с тестами?
Вы чего то недопоняли, но вместо уточнения начинаете валять дурака?

·>Какой негатив? Вроде говорили об одном, теперь чего-то новое.

·>Вообще неясно причём тут time api и причём тут моки и тестирование вообще.

time api это пример дизайна, как можно уйти от комбинаторного взрыва.

P>>Успокойтесь — это все статически контролируется, только в вашу джаву такие фичи еще не завезли.

·>Спасибо, но сводить всё к массиву из 7 чиселок — это фигня полная и издевательство. Даже такое DateTimeOffset.from({...time.toFields, ...date.toFields(), offset: serverOffset}); — и всё поехало. И где наносекуды, интересно?

Где вы видите массив? Это объект, статически типизированый, наносекунды будут в time.toFields как fractionalSeconds. Вам телепатия еще не надоела?

·>Конструкторы удобнее и надёжнее с нужными типами, например, OffsetDateTime.of(date, time, offset), это хотя бы читабельно, и если что-то не так, не компиляется


Принципиальное отличе of вместо from

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

·>Это твои фантазии.

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

P>>Ну да — для вас перебулшитить тесты проблемой не является Еще и настаиваете, что так и надо.

·>Я настаиваю, что так не надо.

Если не надо, то должен быть другой подход к тестированию. А вы выбираете тесты на моках "проверим что вызывается hmac с нужными параметрами"

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

·>Вчерашние студенты?

Да не похоже.

P>>Эта разновидность тестов нужна и вам.

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

АПИ и есть та самая интеграция.

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

·>Я сказал "Они отменяют необходимость написания тучи этих самых xxxComposition, нет кода — нечего тестировать". Ты прикопаться решил к слову код? Ок, пусть будет не код, а модуль-компонент.
·>"Моки отменяют необходимость написания тучи этих самых xxxComposition, нет такого компонента|модуля — нечего тестировать." Так яснее?

Не отменяют. Когда вы укрупняете компонент, то тестировать его сложнее. Или ваше покрытие будет решетом, или вам придется понаписывать много моков вида "это вызывает то", которые сломаются даже при минорных изменения компоненты

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

·>Без моков ты предложил только полный запуск всей аппликухи, в лучшем случае с тестовой субд. Kafka/amps/lbm/solace/tibco/etc, кстати, тоже тестовую будешь пускать? Или как?

Вы додумываете. Это в вашем понадобился бы запуск всей апликухи. У меня же апликуха запускается только ради e2e тестов.
Кафка — это обычный эвент, как вариант, можно обойтись и простым юнитом, убедиться что функция возвращает объект-эвент.
Если у вас такой роскоши нет — добавьте мок ровно на нотификацию, а не на кафку.

P>>В функции средний девелопер может понаделывать ошибок куда больше, чем перепутать old и new.

·>Я в этом и до среднего не дотягиваю, дохрена таких ошибок делаю, но вся эта фигня на раз ловится тестами, c моками (стабами), да.

Если вы написали тест на old и new, то конечно же словится. Только откуда следует, что это единственные проблемы?
Проблем может быть сколько угодно, именно конечный результат говорит что правильно а что нет
Иногда без контроля вызовов не получится — см. пример у буравчика, но и там можно уйти от самих моков за счет максимально дешовых стабов.

P>>Именно! А потому нам все контроллеры нужно тестировать в апи тестах — тем самым мы исключаем добрый десяток классов проблем.

·>И эти тесты дадут тебе гарантию?

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

P>>Что вас смущает? deep.eq проверит всё, что надо. query это не строка, объект с конкретной структурой, чтото навроде

P>>
P>>{
P>>  sql: 'select * from users where id=?',
P>>  params: [id],
P>>  transformations: {
P>>     in: [fn1],
P>>     out: [fn2]
P>>  }
P>>}
P>>

·>Жуть. Ну ладно. Т.е. тут мы тестируем репозиторий и ассертим текст sql

Здесь мы тестируем построение запроса

·>Как проверить, что текст запроса хотя бы синтаксически корректен?


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

> И что он при выполнении выдаст то, что ожидается?


А здесь как вы моками обойдетесь?

> И как ты собрался тут подменять субд? Ты обещал, что теперь эту хрень можно отдать любой db и оно выдаст одинаковый результат? Даже если эта субд это oracle или cassandra? Да там синтаксис банально другой.


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

P>>Сами трансформации тестируются отдельно, обычными юнитами, например expect(fn1(user)).to.eq(user.id);

·>Так ведь надо трансформировать не некий user, а конкретно ResultSet возвращённый из "select *". Что колоночки в select местами не перепутаны, как минимум.

Что вас смущает? трансформация результата принимает ResultSet и возвращает вам нужный объект.

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


Расскажите, как вы это моками решите

P>>Вы все думаете в единицах старого дизайна, когда БЛ прибита гвоздями к бд.

·>Не к бд прибита. БЛ прибивается к репе.

Это одно и то же, и в этом проблема. Я ж вам говорю, как можно иначе — но вы упорно видите только вариант из 90х-00х
В новом подходе не надо БЛ прибивать ни к какой репе, ни к бд, ни к стораджу, ни к кафе, ни к внешним сервисам

P>>Шота я этого за 4 года не заметил

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

Я как раз с гитхаба и брал, если что.

P>>Юзеру так и скажете — мы не можем вернуть ваши деньги, потому что у нас в конфигурации возвраты отключены.

·>Для этого и есть L1 support — "проверьте, что ваш компьютер включен".

Я именно это сказал — L1 support так и скажет юзеру, "в конфиге отключено, значит неположено"

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

P>>Добавили, посмотрели дифом, вкомитали. Вам же не надо сразу всё обрабатывать.

·>А в дифе 10к строчек отличаются. И как это смотреть? Аудит же пишется для каждой операции, т.е. каждого теста, да ещё и не по разу.

А почему 10к а не 10к миллионов?

P>>Если апи возвращает то, что надо, это значит что у вас связка конфиг-di-sl-рантайм-переменные-итд склеено правильно.

·>И это ты предлагаешь гонять на каждый "updateUser(newUser, oldUser)"? Спасибо, не надо нам такого щастя.

Да, я в курсе, у вас есть L1 суппорт, который скажет клиенту, что возвраты не положены.
Это не шутка — мне так L1 однажды залепил
Неделя бодания и перекинули проблему наверх, только когда я сослался на федеральный закон
Признавайтесь, ваша идея?

P>>Можно. Это старый подход.

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

В данном случае вам нужно писать больше кода в тестах, и они будут хрупкими. Другого вы с моками не получите. Потому и убирают зависимости из БЛ целиком, что бы легче было покрывать тестами

P>>Это новый подход, позволяет обходиться без моков-стабов итд и свести тестирование ключевых вещей к дешовым юнит-тестам

·>У тебя тривиальное updateUser(newUser, oldUser) не тестируется юнитами. Любая мелочёвка требует запуска всего, в лучшем случае с in-mem субд.

Ага, сидим да годами ждём результаты одного теста. Вам самому не смешно?

P>>Например, если вы аннотацией указываете что метод работает из под другого юзера, например Freemium, то базовая гарантия — вы будете получать Freemium юзера внутри этого метода. Ее можно модифицировать — отключить весь Freemium в конфиге приложения. Или навесить на Freemium юзера доп.свойства — свежегенеренное имя-емейл-баланс.

·>С такими же гарантиями можно и просто метод дёргать. Аннотации это только от плохого дизайна.

Ну вот подергали вы метод. А потом вас попросили отразить всё такое в сваггере каком, что бы другим было понятно. Гы-гы. Ищите строчки в коде.
А с аннотациями взял да и отрендерил манифест.

P>>В этом случае можно сделать аудит через тесты состояния. Прогоняем операцию, сравниваем что в аудите. Тесты вида "вызываем аудит с параметрам" — это тесты "как написано"

·>Не "вызываем аудит с параметрам", а "послано такое-то сообщение в аудит".

Это непринципиально.

P>>Вы же не проверяете, что джава выполняет циклы правильно? Так и здесь

·>Фреймворк-ориентед программинг? Нет, спасибо.

Это ж классика — отделяем логику, которая меняется часто, от той, которая меняется редко. По этому пути индустрия идет последние 50 лет примерно.
Отредактировано 23.12.2023 18:52 Pauel . Предыдущая версия .
Re[20]: Что такое Dependency Rejection
От: Буравчик Россия  
Дата: 23.12.23 14:26
Оценка: +1
Здравствуйте, Pauel, Вы писали:

Б>>Что значит дизайн такой? Это логика приложения такая, как ее по другому задизайнить?


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


Ну, у тебя (будем на ты?) то же самое написано. У тебя тяжелые зависимости вынесены в функции, а у меня вынесены в отдельные классы, закрыты интерфейсами (они в дальнейшем и мокаются), передаются как параметры.

Моки позволяют отрезать эти тяжелые вычисления и заменить их на простое поведение для теста.

P>Похоже, что моки про которые вы говорите, это фаулеровские стабы


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

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

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

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

P>А вот конкретное оформление этого ветвления определяет то, какими могут быть тесты

Про дизайн. Подход с моками как раз не навязывает дизайн (не вынуждают выделять функции).
Где уместно, я создам чистую функцию и тесты (обычно для алгоритмов бизнес-логики). Где не уместно — буду использовать моки (обычно для логики приложения).
Т.е. любую сложную логику при желании могу протестить юнит-тестами

P>>>Проблема в самих зависимостях, их много, и в том, что ваш дизайн мутабельный

P>>>Попробуйте отказаться от мутабельного дизайна. Как минимум, три эффекта из четырех легко отделяются. Так что можно упростить до безобразия.

Они у меня эти зависимости отделены. Выделены в отдельные классы (репозитории и т.п.), прокидываются как параметры.

P>Потом преобразуете метод в класс, v1,v2,v3 уходят в члены класса


P>
P>class Logic(v1, v2, v3):  // параметры-значения в конструкторе
P>  def run(load, load2, commit): // эффекты-зависимости здесь, эдакий аналог импорта
P>     p = ctx.load(self.v1) 

P>     if что-то(p):  
P>       p2 = ctx.load2(self.v2) // load2 - абстракция так себе, должно быть хорошее имя
P>       p = update(p, p2)

P>     if что-то(p):   
P>       ctx.commit(self.v3)
      
P>     return p
P>


P>Теперь в тестах вы можете создать n троек значений, m результатов операций

P>И покроется все одной таблицей тестов n x m

Как тест будет написан? Как подготовлен, как будут передаваться эти n x m, как проверен?

P>Любители моков правда очень обрадуются, умножат(sic!) Количество тестов за счет проверок "commit вызывается после двух load"


Это твое (неправильное) понимание

P>Теперь ваш мок repo.get должен считать вызовы — что его вызвали первый раз с v1, второй раз с v2, и это важно, т.к на втором разе вы должны вернуть кое что другое

P>И это именно то, чего делать не стоит — в сложной логике ваш мок репозитория будет чудовищем, которое мало будет походить на реальную часть системы
P>Чем более унивесальная зависимость, тем труднее её мокать, и тем выше шансы на ошибку

Для этого теста (который будет проверять эту ветку логики) для операции repo.get будет написан такой стаб:

if v1:
  return value1
else if v2:
  return value2
else:
  raise


P>Если не хотите возиться с интерфейсом — можно вытащить repo.get и подобное в методы рядом с do_logic. Идея на самом деле так себе, этот подход стоит применять если у вас не сильно много зависимостей

P>
P>do_logic(v1, v2, v3):
P>  p = self.load(v1)

P>  if что-то(p):  
P>    p2 = self.load2(v2)
P>    p = update(p, p2)

P>  if что-то(p):   
P>    self.commit(v3)

P>  result = serialize(p) 
P>  return result
P>


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


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

Да, часть сложности перекладывается на тесты, из-за моков, но не критично.

P>А вот зависимости как параметры вы можете понасоздвать или нагенерить на все случаи жизни

P>1 ошибки всех сортов
P>2 пустой результат
P>3 куча вариантов ответа
P>4 куча сочетаний того и этого

Зависимости отделены также, как и у тебя. В твоем случае в тесты ты передаешь их как параметры-функции, а я подменяю поведение через стаб (по-сути ту же функцию). И в обоих случаях проверяются все варианты
Best regards, Буравчик
Отредактировано 23.12.2023 14:30 Буравчик . Предыдущая версия .
Re[21]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 23.12.23 19:46
Оценка: :)
Здравствуйте, Буравчик, Вы писали:

Б>Ну, у тебя (будем на ты?) то же самое написано. У тебя тяжелые зависимости вынесены в функции, а у меня вынесены в отдельные классы, закрыты интерфейсами (они в дальнейшем и мокаются), передаются как параметры.


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

Б>Моки позволяют отрезать эти тяжелые вычисления и заменить их на простое поведение для теста.


Вопрос в том, сколько вам кода для этих моков надо. Если у вас, скажем, три раза подряд идет вызов repo.get вам придется заниматься глупостями —
писать чтото навроде OnCall(3).Returnо(that) или писать кастомную вещь.

Собственно я вам привел пример, как вроде бы те же моки можно свести к простым тестам. Кода для моков около нуля, в отличие от вашего варианта.

Б>Стабы/моки — вопрос терминологии. Я называю моками все разновидности моков, а стабами те, которые работают как "функция" — получили результат, обработали, вернули нужны.


Стабы подкидывают значения. Моки — это тесты вида "проверить, что вызвали то и это"

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


Б>Про дизайн. Подход с моками как раз не навязывает дизайн (не вынуждают выделять функции).


Наоборон — моками вы бетонируете конкретный дизайн. Там где вы тестами на моках проверяете что repo.get вызвался с тем или иным параметром, изменить дизайн на другой вы можете только через поломку всех таких тестов. Даже если просто укажете, что "функция использует репозиторий", у вас будут поломки на ровном месте из за смены дизайна.
То еть — моки ограничивают вас в выборе

Б>Где уместно, я создам чистую функцию и тесты (обычно для алгоритмов бизнес-логики). Где не уместно — буду использовать моки (обычно для логики приложения).

Б>Т.е. любую сложную логику при желании могу протестить юнит-тестами

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

Б>Они у меня эти зависимости отделены. Выделены в отдельные классы (репозитории и т.п.), прокидываются как параметры.


P>>Теперь в тестах вы можете создать n троек значений, m результатов операций

P>>И покроется все одной таблицей тестов n x m

Б>Как тест будет написан? Как подготовлен, как будут передаваться эти n x m, как проверен?


Нам нужно описать все значения, табличкой. Часть значений протаскиваются через параметры, часть — через лямбды.
У нас их шесть штук, v1,v2,v3, repo1, svc, repo2,
Дальше у вас будет один параметризованый тест типа такого, не знаю как на вашем питоне это выглядит, напишу на жээсе

@test()
@params(
    1,2,3,4,5,6  // и так на каждую строчку в табличке
)
@params(
    8,4,2,1,3,5 // точно так же и исключения прокидывают
)
...   сколько строчек, столько и тестов фактически выполнится
"a cool name test name" (v1, v2, v3, repo1, svc, repo2, result) {
   let logic = Logic(v1,v2,v3);

   let r = logic.run({
      load: () => repo1,
      load2: () => svc,
      ... и так прокидываем все что надо
   }) 

   expect(r).to.deep.eq(result)
}



P>>Любители моков правда очень обрадуются, умножат(sic!) Количество тестов за счет проверок "commit вызывается после двух load"


Б>Это твое (неправильное) понимание


Это типичная картина, к сожалению. Еще могут оставить только такие тесты.

P>>Теперь ваш мок repo.get должен считать вызовы — что его вызвали первый раз с v1, второй раз с v2, и это важно, т.к на втором разе вы должны вернуть кое что другое

P>>И это именно то, чего делать не стоит — в сложной логике ваш мок репозитория будет чудовищем, которое мало будет походить на реальную часть системы
P>>Чем более унивесальная зависимость, тем труднее её мокать, и тем выше шансы на ошибку

Б>Для этого теста (который будет проверять эту ветку логики) для операции repo.get будет написан такой стаб:


Б>
Б>if v1:
Б>  return value1
Б>else if v2:
Б>  return value2
Б>else:
Б>  raise
Б>


а нам всего то надо подкинуть два значения
() => value1
() => value2


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


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

Б>Зависимости отделены также, как и у тебя. В твоем случае в тесты ты передаешь их как параметры-функции, а я подменяю поведение через стаб (по-сути ту же функцию). И в обоих случаях проверяются все варианты


Ну так посмотрите — у меня две строчки, у вас шесть.
Отредактировано 25.12.2023 8:08 Pauel . Предыдущая версия .
Re[22]: Что такое Dependency Rejection
От: Буравчик Россия  
Дата: 23.12.23 21:03
Оценка:
Здравствуйте, Pauel, Вы писали:

P>Здравствуйте, Буравчик, Вы писали:


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


И мне лямбду прокинуть — через мок (стаб)

P>Вопрос в том, сколько вам кода для этих моков надо. Если у вас, скажем, три раза подряд идет вызов repo.get вам придется заниматься глупостями —

P>писать чтото навроде OnCall(3).Returnо(that) или писать кастомную вещь.

Для repo.get будет использоваться стаб, т.е. лямбда с нужным поведением. И он сработает для любого количества вызовов.

А вот ваш подход не понял.
Если для каждого будет своя лямбда, то что произойдет, если добавится четвертый вызов get? Придется тесты переписывать?
А если код порефакторят и изменят вызовы?

P>Наоборон — моками вы бетонируете конкретный дизайн. Там где вы тестами на моках проверяете что repo.get вызвался с тем или иным параметром, изменить дизайн на другой вы можете только через поломку всех таких тестов. Даже если просто укажете, что "функция использует репозиторий", у вас будут поломки на ровном месте из за смены дизайна.


Уже сто раз сказал, что для repo.get не проверяется, что он "вызвался с тем или иным параметром".

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

P>У нас их шесть штук, v1,v2,v3, repo1, svc, repo2,
P>Дальше у вас будет один параметризованый тест типа такого, не знаю как на вашем питоне это выглядит, напишу на жээсе

P>[code

P>@test()
P>@params(
P> 1,2,3,4,5,6 // и так на каждую строчку в табличке
P>)
P>@params(
P> 8,4,2,1,3,5 // точно так же и исключения прокидывают
P>)
P>... сколько строчек, столько и тестов фактически выполнится
P>"a cool name test name" (v1, v2, v3, repo1, svc, repo2, result) {
P> let logic = Logic(v1,v2,v3);

P> let r = logic.run({

P> load: () => repo1,
P> load2: () => svc,
P> ... и так прокидываем все что надо
P> })

P> expect(r).to.deep.eq(result)

P>}
P>[/code]

Я не понял, тесты здесь используют реальные зависимости?
Или все же вместо них прокидываются лямбды. (лямбды с упрощенным поведением, т.е. стабы)
Best regards, Буравчик
Re[23]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 24.12.23 06:42
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>А вот ваш подход не понял.

Б>Если для каждого будет своя лямбда, то что произойдет, если добавится четвертый вызов get? Придется тесты переписывать?
Б>А если код порефакторят и изменят вызовы?

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

Б>Уже сто раз сказал, что для repo.get не проверяется, что он "вызвался с тем или иным параметром".


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

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

P>>У нас их шесть штук, v1,v2,v3, repo1, svc, repo2,
P>>Дальше у вас будет один параметризованый тест типа такого, не знаю как на вашем питоне это выглядит, напишу на жээсе

P>>
P>>@test()
P>>@params(
P>>    1,2,3,4,5,6  // и так на каждую строчку в табличке
P>>)
P>>@params(
P>>    8,4,2,1,3,5 // точно так же и исключения прокидывают
P>>)
P>>...   сколько строчек, столько и тестов фактически выполнится
P>>"a cool name test name" (v1, v2, v3, repo1, svc, repo2, result) {
P>>   let logic = Logic(v1,v2,v3);

P>>   let r = logic.run({
P>>      load: () => repo1,
P>>      load2: () => svc,
P>>      ... и так прокидываем все что надо
P>>   }) 

P>>   expect(r).to.deep.eq(result)
P>>}
P>>


Б>Я не понял, тесты здесь используют реальные зависимости?


Тестовые зависимости, @params это параметры-значения для тестов,
Т.е. у нас тест вызовется столько раз, сколько @params мы вкинули

Б>Или все же вместо них прокидываются лямбды. (лямбды с упрощенным поведением, т.е. стабы)


Лямбды-стабы
Отредактировано 24.12.2023 9:27 Pauel . Предыдущая версия . Еще …
Отредактировано 24.12.2023 7:22 Pauel . Предыдущая версия .
Re[24]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 24.12.23 17:46
Оценка:
Здравствуйте, Pauel, Вы писали:

P>>>Моки изначально предназначались для тестов вида "вызываем вон то, вон так, в такой последовательности"

P>·>Может быть. Неясно какое это имеет значение сейчас, так исторические курьёзны. Даже если посмотреть на историю библиотек для тестов — за много лет много чего менялось и переделывалось.
P>И сейчас это основной кейс для моков.
Я не знаю где это основной кейс. Можешь посмотреть туториалы по тому же mockito, там такого треша нет.

P>Вы и сами тащите "надо проверить old и new", и другого варианта не видите, как будто его нет. Можно же и дизайн поменять, что бы юнитами такое проверять. Но нет — вы топите за моки.

Есть, конечно, но другие варианты хуже. Больше писать кода и сложнее тестировать.

P>>>У вас — охотно верю.

P>·>Потому что у нас не хелловорды, а не как у вас, да?
P>Потому, что вы используете дизайн, который плодит лишнее. Я вам на примере дата-время показал, как можно иначе, но вам любой пример не в вашу пользу надо забраковать.
По факту пока что кода лишнего больше у тебя. Притом "вот фаулер написал моки — плохо, давайте накрутим слоёв в прод коде!".

P>>>Я вам пример приводил, про время, вам он ожидаемо не понравился — язык не тот

P>·>Я не понял к чему ты скачешь с одного на другое. Обсуждали статью и MaitreD, потом crypto api, теперь вот ещё time api зачем-то...
P>Со статьё давно всё ясно. crypto api и time это пример, которые показывают, за счет можно отказаться от моков, и что будет, если этого не делать.
Ни для crypto api, ни для time моки не нужны обычно не потому что "дизайн", а потому что они быстрые, детерминированные, с нулём внешних зависимостей.

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

P>>>Вот понадобился не hmac а что другое — в файлике samples поменял десяток значений, и всё
P>·>Почему десяток? У тебя только десяток операций в api?
P>Это у вас на моках упадут все тесты всех операций апи. О том и речь. А на обычных юнит-тестах вы подкидываете новый набор значений, и всё путём.
Почему десяток? У тебя только десяток операций в api? Откуда взялся десяток?

P>>>А ваша альтернатива этому — еще и тесты перебулшитить.

P>·>Один, скорее всего. Ибо одного теста достаточно чтобы зафиксировать логику вычисления hmac. В остальных тестах достаточно ассертить его наличие, но не конкретный формат.
P>"достаточно ассертить его наличие" — вот вам и привязка к "как написано"
Не понял. Наличие hmac — это ожидание, бизнес-требование.

P>Ага, тесты "как написано" Вам придется написать много больше, чем один тест — как минимум, пространство входов более-менее покрыть. Моками вы вспотеете это покрывать.

Я тебе показывал как надо моками покрывать.

P>>>Ну да, этож только у вас сложные системы.

P>·>Ну да, сам же сказал, что у тебя всё помещается в один файлик. Или он гиговый?
P>Вы там в телепатию заигрались? Что именно по вашему помещается в один файлик? Вся система, все тесты, вся система с тестами?
P>Вы чего то недопоняли, но вместо уточнения начинаете валять дурака?
А толку уточнять. Я задал вопрос чего десяток — ответа так и не последовало.

P>·>Какой негатив? Вроде говорили об одном, теперь чего-то новое.

P>·>Вообще неясно причём тут time api и причём тут моки и тестирование вообще.
P>time api это пример дизайна, как можно уйти от комбинаторного взрыва.
Это не "дизайн". Это если ЯП умеет в duck typing и сооружать типы на ходу. Впрочем, тут явно фича применена не по месту.

P>>>Успокойтесь — это все статически контролируется, только в вашу джаву такие фичи еще не завезли.

P>·>Спасибо, но сводить всё к массиву из 7 чиселок — это фигня полная и издевательство. Даже такое DateTimeOffset.from({...time.toFields, ...date.toFields(), offset: serverOffset}); — и всё поехало. И где наносекуды, интересно?
P>Где вы видите массив? Это объект, статически типизированый, наносекунды будут в time.toFields как fractionalSeconds. Вам телепатия еще не надоела?
Ужас. Закопайте.

P>·>Конструкторы удобнее и надёжнее с нужными типами, например, OffsetDateTime.of(date, time, offset), это хотя бы читабельно, и если что-то не так, не компиляется

P>Принципиальное отличе of вместо from
Принципиальное отличие что тут три разных типа Date, Time и Offset — и их никак не перепутать.

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

P>·>Это твои фантазии.
P>Вы сами пишите, что тестировать надо "как написано", это следует из того, как вы предлагаете решать задачу с hmac. Таких же задач будет не одна на проект, а целая куча. Вот и будут тесты "проверить что делает"
Я предложил как решают задачу взаимодействия с неким абстрактным сложным API в тестах с моками (стабами?). То что это crypto — к сути дела не относится, я это сразу заявил в ответе на твой пример, перечитай. Так что это твои фантазии.

P>>>Ну да — для вас перебулшитить тесты проблемой не является Еще и настаиваете, что так и надо.

P>·>Я настаиваю, что так не надо.
P>Если не надо, то должен быть другой подход к тестированию. А вы выбираете тесты на моках "проверим что вызывается hmac с нужными параметрами"
У меня в примере не было "проверим что вызывается hmac с нужными параметрами". Это было у тебя, и я сказал, что так не надо.

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

P>·>Вчерашние студенты?
P>Да не похоже.
Ок, пусть сорокалетние+ джуниоры.

P>>>Эта разновидность тестов нужна и вам.

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

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

P>·>Я сказал "Они отменяют необходимость написания тучи этих самых xxxComposition, нет кода — нечего тестировать". Ты прикопаться решил к слову код? Ок, пусть будет не код, а модуль-компонент.
P>·>"Моки отменяют необходимость написания тучи этих самых xxxComposition, нет такого компонента|модуля — нечего тестировать." Так яснее?
P>Не отменяют. Когда вы укрупняете компонент, то тестировать его сложнее. Или ваше покрытие будет решетом, или вам придется понаписывать много моков вида "это вызывает то", которые сломаются даже при минорных изменения компоненты
Суть в том, что компонент содержит несколько частей функциональности и весь компонент покрывается со всей его функциональностью, с моками (стабами?) зависимостей. После этого достаточно одного интеграционного теста, что несколько компонент соединились вместе и взаимодействуют хотя бы по одной части функциональности. Условно говоря, если в классе 20 методов и все 20 протестированы сотней юнит-тестов, то для тестирования интеграции обычно достаточно одного и-теста. В качестве аналогии — когда ты вставляешь CPU в материнку — тебе достаточно по сути smoke test, а не прогонять те миллионы тестов, которые были сделаны на фабрике для каждого кристалла и конденсатора.
У тебя же будет дополнительных xxxComposition на каждый метод в прод-коде и это надо будет всё как-то тестииовать в полном сборе.

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

P>·>Без моков ты предложил только полный запуск всей аппликухи, в лучшем случае с тестовой субд. Kafka/amps/lbm/solace/tibco/etc, кстати, тоже тестовую будешь пускать? Или как?
P>Вы додумываете. Это в вашем понадобился бы запуск всей апликухи. У меня же апликуха запускается только ради e2e тестов.
P>Кафка — это обычный эвент, как вариант, можно обойтись и простым юнитом, убедиться что функция возвращает объект-эвент.
P>Если у вас такой роскоши нет — добавьте мок ровно на нотификацию, а не на кафку.
Так вызов метода — это и есть по сути эвент. По крайней мере, изоморфное понятие.

P>>>В функции средний девелопер может понаделывать ошибок куда больше, чем перепутать old и new.

P>·>Я в этом и до среднего не дотягиваю, дохрена таких ошибок делаю, но вся эта фигня на раз ловится тестами, c моками (стабами), да.
P>Если вы написали тест на old и new, то конечно же словится. Только откуда следует, что это единственные проблемы?
P>Проблем может быть сколько угодно, именно конечный результат говорит что правильно а что нет
Не знаю как у тебя, но у меня большинство ошибок именно логике в реалзиации методов, когда я тупо ошибаюсь в +1 вместо -1, from/to и т.п.

P>>>Именно! А потому нам все контроллеры нужно тестировать в апи тестах — тем самым мы исключаем добрый десяток классов проблем.

P>·>И эти тесты дадут тебе гарантию?
P>Разумеется, мы ради этого и пишем тесты. Не только что бы убедиться, что работает здесь и сейчас, но что бы можно было перепроверить это в т.ч. на живом окружении.
P>После билда работает, после деплоя — нет, опаньки!
У меня такое редко бывает. Неделю мучаюсь, вылизываю corner cases, потом деплою и оно тупо работает. Ну или сразу грохается при запуске, т.к. новый парам забыл в конфиге определить и лечится за минуту.

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

Это ты фантазируешь. Не "повалится", а произойдёт реальная сделка.

P>·>Жуть. Ну ладно. Т.е. тут мы тестируем репозиторий и ассертим текст sql

P>Здесь мы тестируем построение запроса
Ассерт-то в чём? Что текст запроса 'select * from users where id=?'? У тебя в тесте именно текст забит или что?

P>·>Как проверить, что текст запроса хотя бы синтаксически корректен?

P>Для этого нам всё равно придется выполнить его на реальной бд. Расскажите, как вы решите задачу вашими моками.
Я уже рассказывал. Интеграционным тестом repo+db. Текстов запроса в тестах нет, и быть не должно. Как-то так:
var testSubject = new MyRepo("connectionString");// по возможности in-mem dbms, но может быть и реальная

var id = 42;
testSubject.save(new User(id, "Vasya")));
var actual = testSubject.getUser(id);
assert actual.getName().equals("Vasya");


>> И что он при выполнении выдаст то, что ожидается?

P>А здесь как вы моками обойдетесь?
Тут и не надо. Здесь же интеграция с субд тестируется. Моки (стабы?) будут дальше — в бл, в контроллере, етс.

>> И как ты собрался тут подменять субд? Ты обещал, что теперь эту хрень можно отдать любой db и оно выдаст одинаковый результат? Даже если эта субд это oracle или cassandra? Да там синтаксис банально другой.

P>А разве я говорил, что бд можно менять любую на любую? Построение запросов мы можем тестировать для тех бд, которые поддерживаются нами.
У тебя был такой код:
const getUser = repository.getUser(id);
const oldUser = db.execute(getUser);

Этот код выглядит так, что getUser выдаваемый из репы не зависит от db (и как ты заявил обещано в clean) и repo покрывается тестами независимо. Но по сути всё то же самое. Просто зависимость стала неявной. "генерим любой результат, но только такой, который где-то как-то будет использоваться в db.execute и мы должны знать где и как". Иными словами, repo может генерить только такие результаты, которые можно использовать далее, пишем репу, но должны всё знать о db слое. И вот это никак не тестируется без запуска всего кода контроллера в котором живёт этот код. А чтобы запустить контроллер — надо обеспечить ему все зависимости, т.е. по сути запустить всё приложение.

В моём коде выше с MyRepo("connectionString") — мы тестируем только интеграцию двух частей repo + dbms. И ничего больше в таких тестах поднимать не надо.

P>>>Сами трансформации тестируются отдельно, обычными юнитами, например expect(fn1(user)).to.eq(user.id);

P>·>Так ведь надо трансформировать не некий user, а конкретно ResultSet возвращённый из "select *". Что колоночки в select местами не перепутаны, как минимум.
P>Что вас смущает? трансформация результата принимает ResultSet и возвращает вам нужный объект.
Так ведь надо не абы какой ResultSet, а именно тот, который реально возвращается из "select *". У тебя же эти по настоящему связанные сущности тестируются независимо. Что абсолютно бесполезно.

P>·>Ещё это всё "красиво" только на твоих хоумпейдж проектах. В реальном коде эти sql будут сотни строк с многоэтажными джойнами и подзапросами, и такие "тесты" станут даже немножечко вредить.

P>Расскажите, как вы это моками решите
Моки (стабы) — это для отпиливания зависимостей в тестах. Репу тестируем медленными и-тестами с более-менее реальной субд, а дальше моки (стабы) для быстрого тестирования всего остального.

P>>>Вы все думаете в единицах старого дизайна, когда БЛ прибита гвоздями к бд.

P>·>Не к бд прибита. БЛ прибивается к репе.
P>Это одно и то же, и в этом проблема. Я ж вам говорю, как можно иначе — но вы упорно видите только вариант из 90х-00х
Ты показал это "иначе", но там всё ещё хуже прибито. Вплоть до побуквенного сравнения текста sql.

P>В новом подходе не надо БЛ прибивать ни к какой репе, ни к бд, ни к стораджу, ни к кафе, ни к внешним сервисам

Ага-ага.

P>>>Шота я этого за 4 года не заметил

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

P>>>Юзеру так и скажете — мы не можем вернуть ваши деньги, потому что у нас в конфигурации возвраты отключены.

P>·>Для этого и есть L1 support — "проверьте, что ваш компьютер включен".
P>Я именно это сказал — L1 support так и скажет юзеру, "в конфиге отключено, значит неположено"
P>Что бы такого не случалось, в тестах должен быть сценарий, который включает возврат. И так для всех апи которые вы выставляете.
Не понял, если возврат отключен, то и тест должен тестировать, что возврат отключен.

P>>>Добавили, посмотрели дифом, вкомитали. Вам же не надо сразу всё обрабатывать.

P>·>А в дифе 10к строчек отличаются. И как это смотреть? Аудит же пишется для каждой операции, т.е. каждого теста, да ещё и не по разу.
P>А почему 10к а не 10к миллионов?
Примерное типичное кол-во сценариев реальной системе большего, чем хоумпейдж масштаба.

P>>>Если апи возвращает то, что надо, это значит что у вас связка конфиг-di-sl-рантайм-переменные-итд склеено правильно.

P>·>И это ты предлагаешь гонять на каждый "updateUser(newUser, oldUser)"? Спасибо, не надо нам такого щастя.
P>Да, я в курсе, у вас есть L1 суппорт, который скажет клиенту, что возвраты не положены.
P>Это не шутка — мне так L1 однажды залепил
P>Неделя бодания и перекинули проблему наверх, только когда я сослался на федеральный закон
P>Признавайтесь, ваша идея?
Только не пытайся доказать, что это у них "не работало", потому что тесты у них неправильные. Уж поверь, у них всё работало как надо (им).

P>>>Можно. Это старый подход.

P>·>Ты говоришь как будто это что-то плохое. Это разумный подход. Делить не по тому, что сейчас у фаулеров в моде, а по конкретному осязаемому критерию, т.к. это напрямую выражается во времени запуска и требуемых ресурсов для тестирования.
P>В данном случае вам нужно писать больше кода в тестах, и они будут хрупкими. Другого вы с моками не получите. Потому и убирают зависимости из БЛ целиком, что бы легче было покрывать тестами
Но ведь не "убирают", а переносят. Что добавляет и количество прод-кода и число тестов.

P>>>Это новый подход, позволяет обходиться без моков-стабов итд и свести тестирование ключевых вещей к дешовым юнит-тестам

P>·>У тебя тривиальное updateUser(newUser, oldUser) не тестируется юнитами. Любая мелочёвка требует запуска всего, в лучшем случае с in-mem субд.
P>Ага, сидим да годами ждём результаты одного теста. Вам самому не смешно?
Думаю ваш хоум-пейдж за час тестируется, в лучшем случае.

P>·>С такими же гарантиями можно и просто метод дёргать. Аннотации это только от плохого дизайна.

P>Ну вот подергали вы метод. А потом вас попросили отразить всё такое в сваггере каком, что бы другим было понятно. Гы-гы. Ищите строчки в коде.
P>А с аннотациями взял да и отрендерил манифест.
Гы-гы. У тебя же, ты сам говорил, что попало где попало — и в переменных окружения, и в конфигах и где угодно эти "аннотации" висят. Сваггер у вас каждые 5 минут новый, да?

P>>>Вы же не проверяете, что джава выполняет циклы правильно? Так и здесь

P>·>Фреймворк-ориентед программинг? Нет, спасибо.
P>Это ж классика — отделяем логику, которая меняется часто, от той, которая меняется редко. По этому пути индустрия идет последние 50 лет
Ты любишь сложность на пустом месте, притом в прод-коде, я уже понял. Но это плохо. Фреймворк это совсем другая весовая категория. Для того что ты пишешь достаточно extract method refactoring, а не новая архитектура с фреймворками.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[24]: Что такое Dependency Rejection
От: · Великобритания  
Дата: 24.12.23 17:59
Оценка:
Здравствуйте, Pauel, Вы писали:

Б>>А вот ваш подход не понял.

Б>>Если для каждого будет своя лямбда, то что произойдет, если добавится четвертый вызов get? Придется тесты переписывать?
Б>>А если код порефакторят и изменят вызовы?
P>То же самое, что и у вас. Добавили 4й вызов — ваши моки, коих по количеству тестов, отвалятся.
Тебе уже раз тысячу написали, что НЕ НАДО ПИСАТЬ МОКИ по количеству. Моки (стабы?) пишутся по параметрам и условиям вызова. when(repo.get(v1)).thenReturn(r1); when(repo.get(v2)).thenReturn(r2);. Такой код OnCall(3) — это дичь полная. Так, мне кажется, писали может быть на заре изобретения фреймворков тестирования, лет 20-30 назад.

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

Любопытно, а что если "repo.get" дёргается в цикле? Заведёшь массив лямд?!
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.