Здравствуйте, Pauel, Вы писали:
P>>>·>И? С моками (стабами) тесты такие же дешёвые. Т.к. мы мочим всё медленное и дорогое. В этом и цель моков (стабов) — удешевлять тесты.
P>>>Для начала это вы сами сделали тесты дорогими, когда поместили БЛ прямо в контроллер
P>·>Ты опять скрываешь что ты понимаешь под дороговизной.
P>Я ж сказал — если я поменяю внутреннюю реализацию, никакие тесты падать не должны.
Т.е. если ты поменяешь 'select * from users where id=?' на 'select id, name, email from users where id=?' у тебя тесты и посыпятся.
P>Мы покрываем ожидания вызывающей стороны, а у неё как раз нету ожиданий "вызовем crypto hmac вот таким макаром"
Я уже показал как пишутся тесты с моками для hmac. Там не должно быть такого.
P>А у вас видение "раз hmac в требованиях, то нужны моки"
Ты врёшь.
P>>>Вы все еще про статью, тут я ничем помочь не могу.
P>·>getUser и toFields это не про статью.
P>Разумеется. Зачем продолжать мусолить заведомо однобокую статью?
Ок. Наконец-то ты согласился, что таки да, у тебя прод-кода больше. Нам такого не надо.
P>·>У тебя было expect...toBeCalledWith — это было совершенно неправильное решение. Я просто попытался объяснить, что так делать не надо и показал как надо, сделав оговорку "Если не задаваться вопросом, зачем вообще вдруг понадобилось мокать hmac".
P>Ну ок — если вы не предлагаете мокать hmac, то всё в порядке.
С чего ты взял, что я в принципе мог такое предложить — я не знаю.
P>>>Не одна — для каждого из тестов вам нужен будет этот мок
P>·>Зачем для каждого? Этот мок можно куда-нибудь в setUp вынести.
P>Он будет везде использоваться. А раз так, то ваш мок надо или посать под каждый тест. Или запилить в ём поддержку всех кейсов.
Нет, только те тесты, в которых ассертится логика, зависящая от текущего времени. В остальных может чего-нибудь дефолтное возвращаться.
P>>>hmac, time, итд. В одном файле — что бы чинить не 100 тестов, а всего 10 значений когда вы поменяете hmac 256 на 512.
P>·>Я может что-то не знаю, но обычно hmac зависит от каждого теста — контрольная сумма сериализованного тела ответа. У тебя в твоём хоумпейдже на всю функциональность хватает 10 тестов?
P>А нам не надо везде тащить этот hmac и тестировать это в каждом тесте. юнит-тестами покрываем вычисление hmac по конкретному контенту, и всё. Остальное — в интеграционных, коих на порядок меньше чем юнит-тестов.
Что значит не надо? У тебя там deep equals и сравнение логов. А интеграционных у тебя тоже дофига, т.к. они покрывают каждый do_logic.
P>>>1 При изменении алгоритма, формата итд, сломаются ваши моки
P>·>Не сломаются.
P>Вот пример — вы решили изменить сигнатуру getAll у вашего repository, добавить-убрать фильры-сортировку-итд.
P>getAll у вас покрыт моками.
P>Вопрос — надо ли их фиксить, если поменялась сигнатура getAll? Если нет — покажите это чудо примером
Это типичный рефакторинг add-remove method parameter. Если добавляем парам, то тот прод-код, который мы меняем, ведь теперь надо передавать какое-то значение в такой параметр — надо и в тесте зафиксировать изменение в поведении. Удаление парама — так вообще тривиально, IDE автоматом везде подчистит, тест-не тест, не важно.
P>>>Эта согласованность проверится любым интеграционным тестом который вызовет контроллер.
P>·>Не любым.
P>Покажите пример.
Ну ты там ю-тест упоминал "а что если в колонке пусто". Вот такой соответствующий и-тест тоже придётся писать. Т.е. ю-тест бесполезен, ибо такое можно полноценно протестировать только и-тестом repo-dbms. Как я понял, ты здесь под и-тестом имеешь в виду тест вида "curl".
P>>>Поздравляю, это классика тавтологических тестов
P>>>2 вариант, кладем в файлик тестовы-данные.json "время", "правильный результат", штуки 10 строчк. Меняем дизайн на nextFriday(now) и теперь у нас простой юнит-тест
P>·>Ты подменил задание, а не дизайн. Вначале была функция nextFriday(), потом вдруг стала nextFriday(now). Если твоя система должна по контракту клиентам выдавать операцию nextFriday(), то поменять сигнатуру ты не можешь.
P>Разве я сказал, что nextFriday это функция системы? Это одна из операций в самой БЛ, вопрос только в том, как её тестировать — моками или без них
По сути это не столь важно. Если у тебя в бл будет nextFriday(now), то во всех контроллерах где зовётся этот метод придётся передавать туда это самое now. И придётся писать полные интеграционные тесты, что туда передаётся именно серверное текущее время, а не что-то другое.
Т.е. вместо одного легковесного ю-теста с моком для данного конкретного метода бл, нам теперь приходится писать туеву хучу тяжёлых и-тестов на каждый вызов этого метода из разных контроллеров. Ещё интересно послушать как именно ты собираешься в "curl" и-тестах ассертить, что туда передаётся именно текущее время.
P>>>Это неверное упрощение. Моки и тесты результатов это разные классы задач, с разными свойствами, а отсюда — разные гарантии, проблемы итд
P>·>Верно. Но почему-то ты считаешь, что задачи из других классов не существует.
P>Я ж прямо пишу — разные классы задач. Для одних используем моки, их совсем немного, от безвыходности, для других — тесты чем дешевле тем лучше.
У тебя появляются "дешевые", но бесполезные ю-тесты, т.к. и-тесты всё равно нужны, практически в том же количестве.
P>>>2. если нет другого варианта, как в примере Буравчика, изолировать от зависимостей, что бы логика всё равно зависела от своих параметров.
P>·>У тебя зависимости не изолировались, просто перелилось из пустого в порожнее. Кода стало больше.
P>А вы свой пример покажите, что бы можно было тестировать на разных наборах данных-входов итд, вот и сравним.
Ну я не знаю что именно показать. Ну допустим хотим nextFriday() потестировать:
var clock = mock(InstantSource.class);
var testSubject = new CalendarLogic(clock);
when(clock.instant()).thenReturn(Instant.of("2023-12-28T12:27:00Z"));
assertThat(testSubject.nextFriday()).equalTo(LocalDate.of("2023-12-29"));
when(clock.instant()).thenReturn(Instant.of("2023-12-29T23:59:59Z"));
assertThat(testSubject.nextFriday()).equalTo(LocalDate.of("2023-12-29"));
when(clock.instant()).thenReturn(Instant.of("2023-12-30T00:00:00Z"));
assertThat(testSubject.nextFriday()).equalTo(LocalDate.of("2024-01-05"));
Я вполне допускаю, что внутри CalendarLogic отрефакторить nextFriday() так, чтобы оно звало какой-нибудь static nextFriday(now) который можно потестирвать как пюрешку, но зачем? Если у нас конечная цель иметь именно nextFriday(), то рисовать ещё один слой, увеличивать количество прод-кода ради "единственно верного дизайна" — только энтропию увеличивать.
P>>>Или написать мок на все случаи жизни, что бы промоделировать все неявные зависимости
P>·>Это твои фантазии. Судя по тому коду, который ты привёл, ты не понимаешь как пользоваться моками и придумываешь себе страшилки.
P>Пока что от вас ни одного примера не было.
Было несколько. Погляди сниппеты кода в моих сообщениях.
P>·>Какой код будет в тесте для проверки этого?
P>Вот, что нового вы здесь открыли для себя? const pattern = ?
Появилась возможность пальчиком тыкнуть в то, что у тебя в коде теста полный кусок текста запроса 'select * from users where id=?', а не какой-то магический паттерн, проверяющий что запрос "тот". Т.е. по большому счёту ты никак не можешь знать что запрос хотя бы синтаксически корректен. Пишутся такие тесты обычно копипастой текста запроса из прод-кода в тест-код и тестируют "как написано", а не ожидания. И валятся только после полной сборки и запуска всего приложения, нередко только в проде.
P>P> in: [fn1],
P> out: [fn2]
P>expect(buildRequest(params)).to.deep.eq(pattern)
P>
Что такое "fn1" и "fn2"? Как deep.eq будет это сравнивать?
P>>>Вы вместо проверки построения бд тестируете весь пайплан — связывание, построение, вызов бд клиента, получение промежуточных результатов, обработка, возвращение окончательного
P>·>Именно, всё как ты хочешь "соответствует ожиданиям", и как ты любишь "блакбокс". Ожидаем, что если мы сохранили запись, то потом её можем найти и загрузить. А что там происходит за кулисами — совершенно неважно.
P>Ну вот выполняете запрос на проде, прод падает. Ваши действия?
Воспроизвожу сценарий в тесте и фиксю код.
P>>>Это и есть косвенная проверка. Например, если запрос у вас не тот, но из за вашей бд будет выдавать нужные данные, вы проблему из за косвенности не найдете
P>·>Что значит "не тот"? Как проверить тот он или не тот? Как в принципе узнать как должен выглядеть тот?
P>Для этого нужно знать, какие запросы кому вы отправляете. ORM — пишите тест на построение запроса к ORM. Если же ORM такого не умеет — вот он случай для косвенных проверок, и для моков.
Как это можно знать, что запрос "тот"? Единственный вменяемый вариант — выполнить запрос и поглядеть что вернулось. Полагаю ты делаешь это ручками, копипастишь текст запроса в консоль субд, вручную подставляешь парамы, выполняешь, глазками смотришь на результат, что похоже вроде бы на правду. Я предлагаю поручить это дело и-тестам repo+dbms. Пишем тест, он запускает построенный запрос с парамами и ассертит результат.
P>>>Много фильтров и их комбинаций, да еще и пагинация
P>·>И? зачем для этого несколько тысяч значений? У тебя там комбинаторно рвануло, да?
P>Смотря сколько фильтров и значений. Если у вас один филд и один простой фильтр <> — хватит десятка.
Не понял. Достаточно иметь одно значение и слать разные фильтры.
testSubject.save(new User(id, "Vasya")));
assertThat(testSubject.find(new Filter.byEqual("Vasya"))).isPresent();
assertThat(testSubject.find(new Filter.byNotEqual("Vasya"))).isEmpty();
assertThat(testSubject.find(new Filter.byEqual("Vasy"))).isEmpty();
assertThat(testSubject.find(new Filter.byLike("asy"))).isPresent();
...
Ок... ну может для сортировки и пагинации может понадобиться чуть больше одного значения.
P>·>Пока ты продемонстрировал лишь добавление кода.
P>Вы пока вообще ничего не продемонстрировали.
Я продемонстрировал как удалить код в твоём примере с hmac, например.
P>>>Затем, что самое сложное в БД это построение или запроса к ORM, или SQL. Слишком много запросов похожих на тот, что нам нужен, но нерабочих.
P>·>Построение запроса это детали реализации, важно что построенный запрос вернёт.
P>Нам нужны гарантии, что этот запрос не повалит нам прод, когда в него пойдут произвольные комбинации фильтров и всего остального
И как ты эти гарантии собрался обеспечивать? Неиспользованием моков?
P>·>Если эти наборы параметров забыли протестировать, то чем твой getUser поможет?
P>Тут мы возвращаемся к тем нескольким тысячам значений. Нам нужны все значения в фильтрах, и всех их ркомбинации, =, <>, like итд.
Для этого нужны комбинации фильтров, а не комбинации значений.
P>>>Если понятия не имеете, то скорее всего и запросы ваши будут работать приблизительно. Например, фильтры будут работать только на тестовых данных
P>·>У тебя тоже.
P>Я вам примеры беру из своей практики. На новом проекте именно такая проблема, я это как раз и исправляю.
Т.е. ты находишь баги и непротестированные сценарии. И?
P>>>Если вы протащили аудит прямо в бл, то так и будет. А если бл не имеет зависимостей на репу и бд, то для аудита у нас только одно место — там где применяем изменения. И таких тестов у нас много быть не может.
P>·>Аудит это и есть бл. Аудит — прямое требование бизнеса. Ты ведь надеюсь, уже перестал путать логи и аудит?
P>Ну да — раз аудит и бл нужны бизнесу, значит во всех функциях должны быть зависимости на репозиторий, бд, и кафку
Нет. Я тебе уже рассказывал что от чего зависит.
P>Функция может просто вернуть изменение состояния, и запись для аудита. А сохранять новое состояние и писать аудит вы будете снаружи. Но вы почему то такой вариант не рассматривает
Я уже рассказал. Это делает больше прод-кода, усложняет его и начинает требовать дополнительных тестов о том, что аудит таки пишется снаружи как надо куда надо.
P>>>Процентов 30-40 от сборки.
P>·>А что потом после сборки?
P>После сборки — тесты.
Т.е. сборка без тестов 1.5 часа, потом ещё тесты час?
P>>>Вы что, в первой версии приложения знаете, какие фичи понадобятся в течении следующих 10 лет? АПИ всегда эволюционирует
P>·>Как правило, он расширяется, а не эволюционирует. И чем реже, тем лучше.
P>у вас вероятно только расширяется. Откройте любое взрослое апи — v1, v2, v3, deprecated итд.
v1,v2,v3 — это разные спеки, практически новое api.
P>>>Если манифест держать кодом, то это хорошая заявочка на рассинхронизацию — у нас работает, а у клиентов — нет.
P>·>Я вообще за contract-first.
P>Вот аннотации и есть тот самый контракт — в нем указано всё, что надо. Кода еще нет, а интеграция уже готова и можно начинать одновременно писать и клиента, и тесты, и сам код бакенда.
Аннотации — часть кода. Если вы захотите переписать свой сервис на другой ЯП, ну там с js на rust — что вы будете делать со своими аннотациями?