Pull to refresh

Comments 302

Не читал (многабукаф, только пролистал) но осуждаю :) Мне лично юнит-тесты очень помогают не поломать что-нибудь, особенно когда общее представление о проекте еще не оформилось и приходится часто вносить правки. Я уверен (вернее даже знаю), что они сэкономили мне кучу времени. Другое дело, что не надо это возводить в абсолют, конечно, и стремиться к 100% покрытию или менять архитектуру в угоду тестируемости, которая может зависеть от используемого фреймворка для тестов.
UFO just landed and posted this here
Я отдельные модули по одному пишу. И в процессе покрываю тестами (это конечно не best practices, когда сначала пишут тесты, а потом код). Да, часть времени уходит на переписывание и тестов тоже, но дальше, когда модуль уже более-менее готов и уже может работать с другими (которые я начинаю писать, когда предыдущий почти оформился). И вот здесь тесты начинают очень помогать, так как теперь уже приходится вносить небольшие изменения в уже почти готовый код.
P.S. Это я рассказываю, как я над своим собственным проектом работаю. А заказчики как правило на тестах экономят. Но попадаются и такие, для которых тесты чуть ли не важнее кода.
(это конечно не best practices, когда сначала пишут тесты, а потом код)

Почему же? Есть целая методология под это — Test Driven Development.
Я отдельные модули по одному пишу

Приведите, пожалуйста, парочку примеров.
Вот, скажем, стандартный пример из моей практики: приходит ХТТП реквест на регистрацию, надо сделать следующие действия (happy path):
1. Сходить в базу проверить, нет ли пользователя.
2. Создать пользователя в базе
3. Закинуть в message bus сообщение, что новый пользователь был создан
4. Ответить 200 клиенту

Предположим, мы вынесли эту логику на некий бизнес-слой, т.е. у нас есть функция SignUpUser, которая внутри вот это всё делает. Что и как мы будем тестить?
1. Сформировать HTTP-запрос
2. Отправить в SignUpUser
3. Проверить что у нас в базе появилась запись (для тестов можно и тестовую базу иметь (имхо лучше так, я лично для своего проекта использую sqlite), или замокать класс для доступа к БД)
4. Проверить что там в message bus (надеюсь Вы интерфейсы используете, или просто хардкодите какую-то реализацию?)
5. Проверить ответ
P.S. А дальше извините, у меня работа а я и так уже три часа тут торчу.
P.S. А дальше извините, у меня работа а я и так уже три часа тут торчу.

Разумеется. Если будет время и желание, я бы с удовольствием обсудил это подробнее, т.к. тема лёгких и полезных юнит тестов очень интересна, особенно в сравнении чистых функций и алгоритмов против типичного энтерпрайза.
То, что вы описали — это не юнит-тест. О чем в статье и говорится:
Любопытно, что некоторые разработчики в подобных ситуациях в конечном итоге всё-таки пишут интегральные тесты, но по-прежнему называют их юнит-тестами.

Какой-то UserService с методом SignUpUser(UserRegistrationData userData) получает в зависимости IUserRepository и IMessageBus, делает простые вещи типа:


if (this.repo.findByEmail(userData.email) != null) {
  throw new NonUnqiueUserEmail(userData.email);
}
User user = User.fromRegistrationData(userData); // вариант new User(userData)
this.repo.add(user);
this.bus.publish(new UserRegistered(user));

Два мока: MemoryUserRepo и MemoryMessageBus


Два основных теста:


  • пустой репо и шина пробрасываются в конструктор сервиса, тестовые данные передаются в метод, после его выполнения проверяется, что юзер в репозитории появился (вариант — вызван метод add c нужным параметром) и сообщение опубликовано (вариант — вызван метод publish с нужным параметром).
  • то же, но перед вызовом метода добавляем юзера с тем же email и проверяем, что выбросилось исключение, а репозиторий и шина пустые (вариант — не вызывались)

Это покрывает именно бизнес-логику.

Спасибо. Именно такой вариант я наблюдал и практиковал у себя.
Что заметил из интересного:
  1. Приходится заглядывать в реализацию, дабы знать, какой именно метод IUserRepository вызывается и какой надо мокать. Когда классы разрастаются, это начинает напрягать и концептуально я не сторонник тестировать, зная реализацию (classical TDD мне по душе больше, чем mockist). В принципе, решается дроблением Repository на более атомарные операции.
  2. Если вы используете строгие моки, то вы также убеждаетесь, что ничего кроме этих зависимостей не вызвали. Звучит неплохо, но на практике у нас вылилось в невероятно хрупкие тесты. Без строгих моков тоже не идеально, но я лично готов это стерпеть.
  3. При увеличении количества вызываемых методов и классов, сетап моков становится сложнее и сложнее. Частично решается так же, как в пункте 1, но не до конца.
  4. Признаться, не уверен, стоит ли действительно тестировать такую логику. Я не припомню ни одного случая, когда у нас вызовы функций исчезали или дублировались, при этом написание таких тестов в более сложной системе это не пара минут.

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

Что меня очень интересует, это то, можно ли с минимальными усилиями описать спецификацию «мы делаем Б и В (publish, add) только если условие А истинно, в противном случае возвращаем ошибку». То есть, вместо того, чтобы императивно проверять, что и когда вызывается, выразить правила декларативно с гарантией того, что они не нарушатся разработчиком случайно.

Из того, что встречал — вместо непосредственно выполнения этих действий вернуть структуру, описывающую, что и как делать, наподобие AST:
blog.ploeh.dk/2017/07/31/combining-free-monads-in-f
Правда, и ASТ также придётся валидировать на корректность тестами.
Вопрос того, насколько с этим удобно работать конкретно в .NET на C# для меня пока открыт.
Приходится заглядывать в реализацию, дабы знать, какой именно метод IUserRepository вызывается и какой надо мокать.

Решается созданием InMemoryUserRepository — полноценной реализации IUserRepository в памяти. Проверяем не вызвался ли метод add, а появился ли пользователь в "базе" после вызова сервиса. Естественно InMemoryUserRepository покрываем тестами как какой-нибудь LinqUserRepository.


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

Вот это в целом я считаю излишним для юнит или функционального тестирования.


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

Такие случаи бывают когда сложные условия, циклы.


Что меня очень интересует, это то, можно ли с минимальными усилиями описать спецификацию «мы делаем Б и В (publish, add) только если условие А истинно, в противном случае возвращаем ошибку»

Очень похоже на формальную верификацию, так что вряд ли с минимальными усилиями получится. С другой стороны, на каком-нибудь gherkin в секции given можно написать что-то вроде "user with email test@example.com is(n't) registered", но нужно будет написать хэндлер для этого паттерна, приводящий систему в нужное состояние.

Проверяем не вызвался ли метод add, а появился ли пользователь в «базе» после вызова сервиса

Да, такое делали, в случае .NET просто через EF InMemory. Хороший подход и, в целом, более правильный, на мой взгляд.
Такие случаи бывают когда сложные условия, циклы.

Были куски логики посложнее, но там по итогу всё переписывалось на легковесную state machine и уже она тестилась. Традиционное «закинем моки и проверим вызовы» показалось не оптимальным.
Как пример: в зависимости от того, обращался пользователь ранее к сервису или нет, надо было вести себя по-разному. Сам вызов метода на получение информации о предыдущих обращениях не тестили, а вот логику в виде условной функции (currentState, userRequest) -> newState покрыли на ура. Пока что коллегам нравится.
Очень похоже на формальную верификацию, так что вряд ли с минимальными усилиями получится

Вот, у меня такие же мысли. Звучит хорошо, но на практике не так легко.
С другой стороны, на каком-нибудь gherkin в секции given можно написать что-то вроде «user with email test@example.com is(n't) registered», но нужно будет написать хэндлер для этого паттерна, приводящий систему в нужное состояние.

К сожалению, не работал с gherkin и вообще BDD не щупал на реальных проектах. Не исключаю, что в итоге оно может оказаться ещё более затратным, чем «тупой» тест с моками.
UFO just landed and posted this here
UFO just landed and posted this here

Я недавно для себя открыл SpecFlow очень даже интересная вещь- как раз-таки тестирование по спецификациям.

Мы бы каждый слой покрывали тестами отдельно. У вас 4 слоя:
1. Контроллер
2. Бизнес-слой
3. Слой работы с БД
4. Слой работы с очередью сообщений.

Контроллер работает только с бизнес-слоем (вызывает метод SignUpUser). Мокаем бизнес-слой. Тесты на контроллер проверяют только логику контроллера: аутентификацию, валидацию входящего DTO и поведение (реакцию контроллера) на разные варианты ответа бизнес-слоя (happy path, exception и т.п.)

Бизнес-слой работает со слоем БД и слоем очереди. Мокаем и то и другое и проверяем реакцию бизнес-слоя на различные варианты отклика от тех. слоев.

Слой работы с БД уже можно проверить на in-memory db. У вас две функции (поиск и создание пользователя), на каждую пишем свои тесты со своими данными.

Слой работы с очередью — по вкусу. Там явно будут вызовы функций какого-то фреймворка. Мокаем их и проверяем реакцию на описанные в доке исключения + happy path/

У Вас здесь только 1 тест — проверить наличие пользователя в БД.
Все остальное — действия, согласно описания, не содержащие логику. Они не требуют юнит тестов. Для них достаточно простых интеграционных тестов.
Мы же не хотим тестировать работу message queue? Нам достаточно на уровне интеграционных/automation/API тестов проверить happy path. Более детально проверит automation, который должен покрыть всю логику приложения, согласно ТЗ.

Соглашусь, и добавлю что иногда написание теста перед написанием функции само по себе облегчает жизнь
Я комбинирую оба подхода. Когда я уже точно знаю, что должна делать какая-нибудь функция или метод, я скорее сначала напишу пару тестов прежде чем писать реализацию.
UFO just landed and posted this here
В идеале, конечно, когда я пойму, что мне надо от языка, я сформулирую всякие нужные утверждения (например) и докажу их формально в каком-нибудь коке или идрисе. Тогда все эти тесты вообще можно будет выкинуть, по большому счёту.
Ну да, останется только написать по 500 строк кода формальной верификации на каждые 10 строчек реализации. :)

UFO just landed and posted this here
UFO just landed and posted this here

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

UFO just landed and posted this here
Подписываюсь под первым комментарием.
Автор выдумываем сложные примеры, что бы доказать свои убеждения, хотя забывает (или не знает), что главная задача юнит-тестов не выполнить бестпрактикс для уменьшения количество ошибок, а сделать дешевле разработку(поддержку) за счет упрощения последующей доработки и рефакторинга кода.
Угу, причём 80% пользы наносится даже не прогоном юнит-тестов, а самой возможностью их написать достаточно просто. Если тесты сложно писать – значит, архитектура сложная.
Если тесты сложно писать – значит, архитектура сложная.
Нет. Это просто значит что тесты сложно писать. Этого можно добиться и со сложной архитектурой и с простой.

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

Потому что если у вас типичная простая программа написанная в стиле «стурктурного программирования», то вы там ничего отюниттестировать не сможете. Функция регистрации пользователя будет ходить в базу, причём хорошо ещё если не в одну фиксированную базу… но это, несомненно, проще, чем все эти IoC, DI, позднее связывание и прочее. Во всём этом, представьте себе, тоже модно наделать кучу ошибок.

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

Угу, причём 80% пользы наносится даже не прогоном юнит-тестов, а самой возможностью их написать достаточно просто.
Когда вы пишите программу так, что она, с одной стороны, могла бы быть легко покрыта тестами, а с другой стороны — этого не делаете… вы получаете худший вариант из возможных.

Не надо так.
все эти IoC, DI, позднее связывание и прочее

Помимо тестируемости они еще и гибкости очень неплохо добавляют. Уж как я настрадался от отсутствия всего этого когда разрабатывал на 1с и нужно было заметно изменить или расширить работу системы. Как пример были у нас два почти идентичных сценария в системе. Регистрация выявленного дефекта либо построение плана предупредительных ремонтов -> оформление заявки на ремонт -> оформление наряда на работы -> оформление акта выполненного ремонта. И построение плана регламентных мероприятий -> оформление наряда на регламентное мероприятие -> оформление акта о выполнении регламентного мероприятия. Цепочки, операции и формы были очень и очень похожи, очень много общего поведения (с кучей вариаций отличающихся в зависимости от разных условий). Как же не хватало возможности нормально модульность на уровне кода организовывать, с внедрением стратегий извне, валидаторов под разные сценарии, динамической диспетчеризацией под разное поведение вместо гроздей ифов и прочего. В итоге вносилась куча похожих правок прямо внутрь документов, часто однообразных, поскольку не было возможности создать какой нибудь один интерактор для общего поведения и внедрить во все участки, в лучшем случае выносились в общие функции и обмазывались условиями.
они еще и гибкости очень неплохо добавляют

Немедленно возникает вопрос: а она точно нужна?

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

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

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

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

Да, возможно, он станет гибче, да, возможно дополнительные тесты сделают его надёжнее. Всё может быть.

Но само по себе такое действие — делает код однозначно сложнее. Просто потому что вы налагаете дополнительные требования к нему.
Проще/сложнее — субъективные понятия, которые непонятно в чем измерять. Код, приспособленный для тестирования, иногда более читаем чем неприспособленный за счет накладываемых на него ограничений. Поэтому он проще. С другой стороны, чтобы написать такой код, нужно придумывать новые абстракции, их имена и связи, что делает написание сложнее.
Например, под сложностью часто подразумевают количество сущностей, которые задействованы в юните кода и которыми нужно оперировать в процессе написания/чтения. Все эти IoC, DI, позднее связывание и прочее позволяют иногда размазать сущности по разным юнитам более равномерно, что снижает их количество в отдельно взятом юните, но скорее всего увеличивают их общее количество на всю программу.
Наши суждения о простоте/сложности строятся на нашем разном опыте, в моем случае программа, написанная в стиле «структурного программирования», — это часто мешанина флагов, пятиэтажных ифов и глобальных переменных, с чем никак не проще работать, чем если то же самое написать с использованием ООП-баззвордов. Встречались и обратные примеры, похожие на FizzBuzz Enterprise Edition, но как-то реже.

UPD. Еще хотелось бы добавить, что некоторые под сложностью подразумевают количество знаний, которое нужно применить для написания/чтения кода, но мне такое измерение не по душе, потому что субъективность здесь возводится в абсолют.
Все эти IoC, DI, позднее связывание и прочее позволяют иногда размазать сущности по разным юнитам более равномерно, что снижает их количество в отдельно взятом юните, но скорее всего увеличивают их общее количество на всю программу.
Собственно не «иногда», а «почти всегда». В результате ваш код становится сложнее, однако удельная сложность может и уменьшиться.

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

Поэтому он проще.
Каждые его 100 строк кода обычно действительно проще. Однако суммарно — он содержит как всю неотъемлемую (essential) сложность (сложность-же должна где-то жить), так ещё, допольнительно, и привнесённую (accidental). Он ну никак не может быть проще.

Например, под сложностью часто подразумевают количество сущностей, которые задействованы в юните кода и которыми нужно оперировать в процессе написания/чтения.
Ну… «настоящая» сложность невычислима, так что да, приходится пользоваться какими-то оценками…

UPD. Еще хотелось бы добавить, что некоторые под сложностью подразумевают количество знаний, которое нужно применить для написания/чтения кода, но мне такое измерение не по душе, потому что субъективность здесь возводится в абсолют.
С таким определением вообще невозможно оперировать, так как оно попросту неконструктивно. Что такое «знания»? Как их мерить?
Типичные случаи, которые я наблюдаю у себя — код, не задуманный с целью «супергибкости» заметно сложнее удельно, в пересчёте на строку кода — но при этом его гораздо меньше. Часто — на порядок меньше.

Аналогично, мне почти никогда не приходтся гнаться за размером кода, поэтому я чаще распределяю сложность, часто после того как возвращаюсь к ранее написаномму.
Каждые его 100 строк кода обычно действительно проще. Однако суммарно — он содержит как всю неотъемлемую (essential) сложность (сложность-же должна где-то жить), так ещё, допольнительно, и привнесённую (accidental). Он ну никак не может быть проще.

Имеет ли практический смысл эту суммарную сложность вообще рассматривать? Мы же в один момент времени чаще всего работаем с очень ограниченным количеством кода — и эта работа становится проще.
С таким определением вообще невозможно оперировать, так как оно попросту неконструктивно. Что такое «знания»? Как их мерить?

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

P.S. Мне не нравится фраза "… код становится сложнее", потому что ее сейчас прочитает какой-нибудь студент, не вдаваясь в суть и будет считать, что это безусловное зло, а это не так. Но какая-нибудь альтернатива фразе в голову не приходт.
Имеет ли практический смысл эту суммарную сложность вообще рассматривать?
Только в том случае, если вас волнует написание корректного кода, конечно.

Мы же в один момент времени чаще всего работаем с очень ограниченным количеством кода — и эта работа становится проще.
Зато программа, когда её запускают, работает сразу со всем кодом. И если у вас куча кода безошибочная и работоспособная, но одна проверка срабатывает неверно — вся конструкция в целом «разваливается».

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

А вот как раз если код простой, монотонный, однотипный — там как раз ошибки, зачастую, могут «сидеть» годами.

Мне встречаются люди, которые например говорят — «я не знаю как работает наследование в языке XXX, поэтому все что его использует — сложно»
Это бывает. Вон, в соседней статье идёт чуть не битва за то, чтобы одну из конструкций Python «закопать» и «задавить авторитетом».

Мне не нравится фраза "… код становится сложнее", потому что ее сейчас прочитает какой-нибудь студент, не вдаваясь в суть и будет считать, что это безусловное зло, а это не так.
Ну дык тут вопрос, что вы не там копаете: сложность — это безусловное зло… тут особо и спорить не с чем. Однако всё не так просто… читайте следующую главу…

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

Сложность — это плохо. Гибкость — это хорошо. А поскольку вы не можете сделать код одновременно и простым и гибким — то приходится выбирать.

Увы. Нет в мире серебрянной пули.

P.S. Да, я знаю — бывают ситуации, когда можно сделать код и проще и гибче одновременно. Тут спорить не о чем, нужно просто делать. К сожалению на практике — это, скорее, исключение, чем правило.

P.S. Вообще всё программирование — это о компромиссах. У любого решения есть преимущества и недостатки. Если кто-то вам вообще хоть что-то рекламирует как абсолютное благо (или абсолютное зло) — гоните этих людей прочь: они не понимают о чём говорят. А вот когда вы знаете — какие параметры то или иное решение улучшает, а какие — ухудшает… уже можно выбирать… Просто есть вещи, которых нужно использовать почти всегда, а есть вещи, которые нужно, наоборот, использовать «в гомеопатических дозах». Но я затрудняюсь вообще хоть какое-то техническое решение назвать, которые было бы всегда полезно или, наоборот, всегда вредно…
А не нужна потому что никакая альтернатива. Нужно продолжение. Да — это делает код более сложным, но и, одновременно, более гибким.
Сложность — это плохо. Гибкость — это хорошо. А поскольку вы не можете сделать код одновременно и простым и гибким — то приходится выбирать.

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

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

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

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

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

Чем более код абстрактный, тем сложнее в нем допустить ошибки.

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

Возьмем крайний случай, функция принимает параметр типа T и возвращает параметр типа T. О типе T ничего не известно. Много ли вариантов написать такую функцию есть? Особенно если использовать языки, в которых сайд-эффекты будут частью сигнатуры функции?

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

Это работает с любым кодом. Например, чуть сложнее, чем identity, пусть есть функция (List[Char], Char -> Char) -> List[Char]. Вариантов написать ее бесконечно много, как и вариантов ошибиться в них. Для функции (List[T], T -> U) -> List[U] вариантов ошибиться уже сильно-сильно меньше etc. Если отойти от полиморфизма и ФП, то будет +- то же самое. Возьмем репозиторий, возвращающем по id ошибку или искомый объект. Работать с ним без ошибок гораздо проще, чем с объектом соединения с БД, имеющим 100500 ручек.

Например, чуть сложнее, чем identity, пусть есть функция (List[Char], Char -> Char) -> List[Char]. Вариантов написать ее бесконечно много, как и вариантов ошибиться в них. Для функции (List[T], T -> U) -> List[U] вариантов ошибиться уже сильно-сильно меньше etc.

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

В первой функции функция может быть (не)вызвана для части элементов. Во втором — нет, т.к. мы фиксируем, что тип листа разный. Еще более абстрактный вариант (Functor[T], T->U) -> Functor[U] будет иметь вообще только одну возможную реализацию.

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

Подождите, это ровно то, что я сказал. И?..

Не совсем. Первая функция это может быть, например, что-то вроде applyAll, а может быть applyFirst. Вторая функция подобных вариантов не имеет.

Не понимаю. Вот первая функция: (List[Char], Char -> Char) -> List[Char]. Вот вторая функция: (List[T], T -> U) -> List[U].


Почему первая может быть applyFirst, а вторая — нет?

Потому что первая Char->Char, а вторая T->U. В List[U] не могут содержаться T, только U.

Эм. applyFirst — это когда на входе был список 'q', 'w', а на выходе стал f('q'). Нет никакой разницы, ограничены у меня типы преобразования или нет.

Я имел ввиду на входе "qw" на выходе "Qw"

Понимаете ли, эту ошибку сложно совершить. Очень сложно.


Ну и да, возьмите функцию List[Char], Char -> Int) -> List[Int]. Казалось бы, ее уровень абстракции не изменился, а (ту же) ошибку совершить уже нельзя.


Поэтому лично я считаю, что дело тут в выразительной системе типов и их качественной проверке, а не в какой-то "абстракции".

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


Выразительная система типов она потому и выразительная, что позволяет лучше описывать какие-то элементы предметной области, те абстракции. Сама по себе она бесполезна. В одной и той же системе типов возможны как функция addUser :: String, String -> String, так и функция addUser::UserId, UserName -> Result. И с первой можно совершить гораздо больше ошибок, чем со второй.

Как раз очень легко, просто немного ошибившись в постановке задачи.

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


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

Вот это вот "т.е. абстракции" мне и не очевидно. Лучше описывать элементы предметной области — да. Более абстрактная? Не знаю.

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

В первом случае возможна функция, которая возвращает ['a', 'b', 'c'], или например uppercase(f(list[i])), или ещё что-то завязанное именно на Char. Во втором — нет.

Это и есть "не вызвана функция из второго аргумента".

Почему не вызвана, если функция такая (второй пример из моего комментария): myfunc(list, f) = [uppercase(f(x)) for x in list]?

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

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

Да нет, я как раз не возражаю.

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

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

И что же не правильного в рефлексии? Как вы без неё узнаете, например, размер, который структура занимает в памяти, чтобы выделить для кольцевого буфера, необходимого для реализации wait-free очереди, столько памяти, чтобы уложиться в одну страницу памяти?

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

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

А как же динамические типы? Их размер только в рантайме известен.

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

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

UFO just landed and posted this here
UFO just landed and posted this here

В компайл тайме, конечно же. Таких языков много: D, Nim и другие

UFO just landed and posted this here
А где тут противоречие с моим утверждением? Да, добавляя в вашу задачу абстракции вы добавляете в него код, который легко понимать и тестировать. Но общую сложность решения это никоим образом уменьшить не может: этот код всё равно имеет дополнительные ограничения, которых исходная задача не имеет.

Почти всегда можно снизить удельную сложность в пересчёте на строку кода (но это редко когда важно). А вот бывает реально полезно — это абстрагировать части решения и использовать их для решения разных задач (возможно — решаемых разными людьми).

Вот в этом случае сложность решений всех задач, рассматриваемая совместно — может и реально снизиться. Сложность решения каждой отдельной задачи при этом, конечно, всё равно возрастает>

Но этим нужно заниматься очень аккуратно и банальным «шинкованием длинных функций в лапшу» — вы этого не сделаете…

Обычно всё это мотивируется наивными мантрами «да, здесь мне эта сложность не нужна, но „большие дяди“ так рекомендуют делать и скоро появится новая задача, где эта сложность себя окупит».

Вот в моей практике… это не работает. Если ваши задачи уже «висят в беклоге», уже известно — кто будет их делать и примерно когда… тогда добавление сложности может быть оправдано. Но если вы закладываете в код програмы для киоска по продаже сигарет код, который выстрелит, когда ваш магазин станет галактической империей и начнут проявлятся разные эффекты, связанные с парадоксом близнецов — то вы впустую тратите время и силы.
Почти всегда можно снизить удельную сложность в пересчёте на строку кода (но это редко когда важно).

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

В этом процессе мы работаем «со всей кодовой базой сразу». А оптимизацим, как мне кажется, должны делаться под то, чем мы занимаемся больше всего…

Но это, похоже, опять разговор жителей разных миров. Могу представить себе и мир, где баги никто не правит, только новые фичи лепятся друг на друга… собственно откуда берётся весь этот «хтонический легаси», о котором все стонут? Вот оттуда…

Я с этим миром стараюсь не пересекаться: если баги есть — их нужно править, а не «заметать под коврик».
Исправление ошибок чаще всего тоже происходит в одном, ну максимум 2-3 местах. В любом случае в 99% времени нам известен либо сценарий воспроизведения (а это уже позволяет неплохо место возникновения ошибки локализовать), либо у нас есть стектрейс. Вам не нужно анализировать все сотни тысяч, а то и миллионы строк кода.
Исправление ошибок чаще всего тоже происходит в одном, ну максимум 2-3 местах.
Исправление занимает вообще какую-то ничтожную часть времени. Даже меньше, чем первоначальное написание. Основное время уходит на поиск того места, где случилась проблема. И вот это вот — нифига не линейно и не локализовано.
Не знаю как у вас так выходит. Видимо мы разного рода системы разрабатываем. В моем случае все как я описал. Либо есть краш со стектрейсом, тогда идем в место краша, смотрим простой код и прикидываем какие условия могли к крашу привести, проверяем варианты, фиксим. Либо у нас есть описание пользователя/аналитика/сами наткнулись и поняли сценарий. И воспроизводим по этому сценарию баг, локализуем, смотрим какая часть кода за это отвечает и какой инвариант возможно нарушен, смотрим простой код который влияет на состояние, находим причину, фиксим. Вот серьезно, неужели вы зная что баг в расчете какой нибудь там ставки или валидации формы полезете весь код осматривать?
Либо есть краш со стектрейсом, тогда идем в место краша, смотрим простой код
Ok, принято. Пусть простой код такой такой:
  return order.item_info[item_id];

и прикидываем какие условия могли к крашу привести
Тут и думать нечего: item_id у нас, допустим, отрицательный. -1 для простоты.

проверяем варианты
Ооо… вот тут-то собака и порылась. Мы ведь всё «упростили для тестирования». Потому у нас этот item_id приходит… неизвестно откуда.

Потому что у нас же всё феншуйно, удобно для тестирования. Конструктор помечен, как положено, @Inject, когда кто-то просит наш объект оно, в лучших традициях DI одному богу известно откуда вытаскивает этот item_id. А попасть он может из трёх мест в коде. А туда — ещё из трёх.

Гибкость неописуемая, но если вы сами, лично, не писали этот компонент — то фиг вы чего поймёте, потому что у вас в процессе от того места где засунули в Guice вместо item_id какой-нибудь reservation_id (который равен -1 когда товара нет на складе, а так-то, обычно, с ним всё хорошо) участвуют десяток классов… на каждом «этаже» возможны 2-3 варианта и ни один из них не виден в stack trace, потому что они отрабатывают асинхронно в других тредах, блин!

Либо у нас есть описание пользователя/аналитика/сами наткнулись и поняли сценарий.
Знаете — если вы уже добились воспроизводимости и задача перешла из стадии «мы имеем X крешей в наших логах каждый день и жалобы от пользователей» к стадии «у нас есть чёткий способ вопроизвести проблему» — то вы уже на 90% задачу решили.

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

Вот серьезно, неужели вы зная что баг в расчете какой нибудь там ставки или валидации формы полезете весь код осматривать?
А какие есть варианты? Если у вас код сделан «под тесты», в нём что угодно может вызывать что угодно через Guice (или любую другую подобную систему, «упрощающую тестирование») и вы понятия не имеете что и где у вас не так сконфигурировано?

Это если у вас тупой процедурный код — то вы всё увидите прямо по стектрейсу. А вот если всё разрезано на кусочки для юниттетсов, а те разложены по микросервисам для изоляции, а те общаются между собой асинхронно… о, вот тут-то понять откуда что и куда пришло и куда ушло — целый ребус «для ценителей жанра».

Зато всё тестируется легко и все тесты проходят… потому что вообще имеют мало отношения к тому, что в реальной программе исполняется!
Вы слишком демонизируете DI и прочие практики. Такое ощущение что вы предполагаете что они дают сильное зацепление и слабую связность, но по факту наоборот, их применение зацепление снижает, а связность повышает. По крайней мере это то что я наблюдаю в своей практике. В отличие от обычной процедурщины где со временем начинает все со всем переплетаться.
Всё-таки не практики как сами по себе, а определённый стиль их применения.
В данном случае источник данных (как item_id) должен был проверить на выходе, что он не даёт чушь (как минимум залогать неверное значение). Но кто об этом думает, когда их пишет…
Всё-таки не практики как сами по себе, а определённый стиль их применения.
Это, извините, как? Если у нас была типичная структурная программа, а мы её через Guice порезали — то это какой, я извиняюсь, стиль?

В данном случае источник данных (как item_id) должен был проверить на выходе, что он не даёт чушь (как минимум залогать неверное значение).
Ага, источник данных должен было проверить. И получатель тоже. И промежуточные классы — так, в идеале-то.

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

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

Но ведь когда падал «монолит» — там было достаточно по одной цепочке в stack trace пройтись и увидеть что и где у нас там происходит.

Но кто об этом думает, когда их пишет…
Кто-то думает, кто-то нет. Но тот факт, что об это вообще нужно думать и писать ещё код, сверх того, который нужен для DI — по-моему наглядно показывает что проще, а что сложнее.
Либо есть краш со стектрейсом, тогда идем в место краша, смотрим простой код [...]
Либо у нас есть описание пользователя/аналитика/сами наткнулись и поняли сценарий.

Ну вот описание последнего бага, который я чинил. Высоконагруженная система, кластер, 6 нод, которые общаются друг с другом. Две ноды читают из RabbitMQ поток примерно 20К сообщений в секунду. На остальных четырех нодах крутится примерно 400К эрланг-процессов, каждый отвечает за одну сущность, то есть когда появляется новое значение сущности, процесс просыпается и что-то там с ней делает.


Примерно раз в сутки-двое, в неопределенное время, не заметное ни на одной из метрик (без пиков по ресурсам, без алертов, без ничего) — консьюмеры раббита вдруг проседают и перестают успевать разгребать очередь. Спасибо реализованному back pressure — через несколько минут все восстанавливается, но даже с лимитом в 100К очередь на это время засоряется и часть данных мы теряем.


Подробное описание? — Да более чем. Сценарий понятен? — Ну как бы да. Код, который читает из раббита простой? — Да проще некуда.


Ваши действия?

От такого рода систем я все таки далек, но если те две ноды что читают из кролика не на эрланге — то подумал бы что где то проблемы с взаимоблокировками при работе с очередями. Или есть какой то процесс который что то делает по расписанию и опять же останавливает другие процессы на блокировках или IO отнимает возможно им нужный. Но я по большому счету фронтендщик (ранее 1с, сейчас android), и рассуждаю о своей сфере разработки прикладного софта. А чем в вашей ситуации усложнит жизнь модульность и DI?

Ну, то есть, искать по всей кодовой базе. ЧТД ◁


Там нет блокировок;

А чем в вашей ситуации усложнит жизнь модульность и DI?
1. Увеличение количество кода примерно так на порядок усложняет попытки поиска банально тем, что кода много.
2. Каждый переход «наверх, к источнику проблему» затруднён, так как там у нас нет однозначности в угоду простоте тестирования.
На порядок это в десять раз, хотя по факту процентов на десять. Ну и DI не только для простоты тестирования применяется, у нас тестов нет почти, но DI используется и привносит больше удобства чем было без него, ибо бизнес логика отделена от конструирования и инициализации объектов. Плюс гибкости немного добавляет. Плюс вам один черт нужно пробрасывать многие вещи в глубину из наружных слоев, лучше это делать единообразно.
по факту процентов на десять.
Это, извините, по какому факту? Как часто вы участвовали в «феншуйном переписывании» (или, наоборот, в «нефеншуйном»)?

Я за этим слежу всю свою карьеру — ещё с тех пор когда сам, когда-то, переписал программу «по феншую» и она стала больше, чем оригинал в пять раз.

Сейчас, я правда, «феншуй» стараюсь, по возможности, извести, но ситуация не меняется: минимальная разница — где-то два-три раза по объёму кода, типичная — пять (и люди, при этом плачут, что у них не хватает ресурсов «всё сделать правильно»), «полный феншуй» — десять.

Ну и DI не только для простоты тестирования применяется, у нас тестов нет почти, но DI используется и привносит больше удобства чем было без него, ибо бизнес логика отделена от конструирования и инициализации объектов. Плюс гибкости немного добавляет
Гибкость и удобство — это понятно. Если бы весь этот «феншуй» не давал вообще никаких премуществ — то было бы странно, согласитесь.

Но отладку — он затрудняет. И написание корректного кода, без ошибок, на самом деле, затрудняет тоже. Позволяет гибко реагировать на все «колебания линии партии» — это да. Но это не везде нужно.

Плюс вам один черт нужно пробрасывать многие вещи в глубину из наружных слоев, лучше это делать единообразно.
А это уже другой вопрос. Во-первых уменьшение количества слоёв — зело помогает. Во-вторых — генераторы кода. Да, в бинарнике количество кода может остаться и неизменным, а иногда даже и вырости (редко), но зато если у вас вместо трёх классов написанных руками есть один кодогенератор — то вы можете быть уверены, что ошибка, если она и имеется, в этом кодогенераторе, а не в сгенерированных классах.

Конечно иногда «и на старуху бывает проруха» и получается так, что кодогенератор всё генерирует правильно, но вот в одном случае из 100, из-за пересечения названий, порождается чушь… но в моей практике это случается куда реже, чем «скопировал класс, в 9 местах название поля поправил, в 10м — забыл, неделю сидел в отладчике, пока понял».
> Ваши действия?

1. Лучшее: выкинуть Erlang и всё, что на нём написано. Нет, я серьёзно. Его разработчикам уже минимум с 2008 объясняют, что один-единственный mailbox на процесс, при том, что синхронное взаимодействие требует отправить сообщение и прочитать в ответ — это то преступление, что хуже ошибки, или та ошибка, что хуже преступления — всё едино. Но они не реагируют и отделываются 1/10-мерами типа пометки позиции прочитанного в очереди.
Пока все старперы не уйдут из руководства и это не сдвинется — я на любое предложение применить Erlang для чего-то кроме чистой раздачи контента буду крутить пальцем у виска, а если это не поможет — бить ногами. Критерий непригодности системы к задаче — формальная возможность наличия более одного сообщения на входе у процесса, который выполняет gen_tcp:send(), gen_server:call() или аналоги.

2. Вместо

> крутится примерно 400К эрланг-процессов, каждый отвечает за одну сущность

сделать лимитированное число процессов, каждый из которых отвечает за пару тысяч таких сущностей и держит вложенные машины состояний на каждую сущность. Это если со времён, когда я с ним возился, осталась раздельная куча у каждого процесса (толстые объекты не считаем).

> Подробное описание? — Да более чем. Сценарий понятен? — Ну как бы да. Код, который читает из раббита простой? — Да проще некуда.

И всё это не имеет никакого отношения к проблемам рантайма, в которых и зарыто целое кладбище собак.
Лучшее: выкинуть Erlang и всё, что на нём написано.

   


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

   


Критерий непригодности системы к задаче — формальная возможность наличия более одного сообщения на входе у процесса, который выполняет gen_tcp:send(), gen_server:call() или аналоги.

См. цитату выше. Я бы мог объяснить, почему один mailbox на процесс — это абсолютно правильно, но мне попросту лень связываться.


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


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

Рыдаю. Давайте все нафиг переусложним в триста раз, потому что мы не умеем сделать диспетчиризацию по-человечески (да там ее и нет, по сути). Да-да.

Я бы мог объяснить, почему один mailbox на процесс — это абсолютно правильно

Это как минимум требует блокировок и всех связанных с ними проблем. Я бы не назвал это хорошим решением.

Это как минимум требует блокировок и всех связанных с ними проблем.

Не очень понял, что вы хотели этим сказать.


Я бы не назвал это хорошим решением.

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


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

Хорошее решение — использовать wait-free каналы для коммуникации между потоками. И уж точно не стартовать тысячи процессов, которые сожрут всю память.

И уж точно не стартовать тысячи процессов, которые сожрут всю память.

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


Хорошее решение — использовать wait-free каналы для коммуникации между потоками.

О, да!!! Ладно, все ясно.

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

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

Мне хватило в середине девяностых, когда я много писал околомедицинских приложений, работавших с разным нестандартным вводом: RS-232, недокументированные платы передачи данных от приборов, вот это вот все.


сидеть на шее у эрланга или гошечки много ума не надо

Это правда (ну, кроме того, что го вообще неработоспособен). Ну, покажите вашу реализацию, может быть перейду на нее в продакшене, у вас-то вот я вижу ума поболее — буду учиться у мэтров.

> См. цитату выше. Я бы мог объяснить, почему один mailbox на процесс — это абсолютно правильно, но мне попросту лень связываться.

То есть аргументов у вас нет, и сказать что-то осмысленно тому, кто 5 лет писал на Erlang высоконагруженные приложения, вы не в состоянии, зато отделываетесь красивыми цитатами от неоднозначных персонажей. OK, понятно.

> когда код написан людьми, способными его писать.

Как минимум эти «люди, способные его писать» делают обход той же самой проблемы с gen_tcp:send() хаком во внутренности стандартной библиотеки, исключая проблемное ожидание. Но вы этого не хотите видеть.

> Давайте все нафиг переусложним в триста раз

Наоборот, упростим — по сравнению с нынешними кошмарами. Но вы можете продолжать упорствовать, ваше дело.
То есть аргументов у вас нет, и сказать что-то осмысленно тому, кто 5 лет писал на Erlang высоконагруженные приложения [...]

Можно триста лет «программировать» высоконагруженные приложения, и ничему не научиться. Вон был один широко известный в узких кругах проект, который нанял эксперта такой же примерно ориентации: «хакнем внутренности стандартной библиотеки», «перепишем все на нифах», и так далее. Как закончилось — можно погуглить, вкратце: мнезия не выдержала четырех терабайт. И я не могу тут винить мнезию. Она не для того была сделана.


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


[...] исключая проблемное ожидание. Но вы этого не хотите видеть.

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


вы можете продолжать упорствовать

Я не упорствую; я в начале ветки привел пример, (почитайте, пример чего именно, кстати) только и всего. Потом пришли вы с безумными советами, которые все только поломают (или нет, я особо не вчитывался).


Так вот, у меня нет никаких проблем с 200К процессами в одной виртуальной машине. Нет проблем, понимаете? Тех, которые вы предлагаете решать — их нет.

> Дело не в том, что у меня нет аргументов. Дело в том, что вы несете, простите, чушь

«А если найду»? И таки нашёл, за пару минут:

%% gen_tcp:send/2 does a selective receive of {inet_reply, Sock,
%% Status} to obtain the result. That is bad when it is called from
%% the writer since it requires scanning of the writers possibly quite
%% large message queue.
%%
%% So instead we lift the code from prim_inet:send/2, which is what
%% gen_tcp:send/2 calls, do the first half here and then just process
%% the result code in handle_message/2 as and when it arrives.


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

> Можно триста лет «программировать» высоконагруженные приложения, и ничему не научиться.

пока что вы показали только свойства собственного отражения.

> Я умею в архитектуру с тем, что есть.

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

> Так вот, у меня нет никаких проблем с 200К процессами в одном виртуальной машине. Нет проблем, понимаете? Тех, которые вы предлагаете решать — их нет.

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

Никогда в жизни ничего не делал с gen_tcp. Найденная вами «проблема» связана с тем, что не нужно в блокирующем вызове слушать сокет до победного конца. Как в этом виноват mailbox — тайна великая есть.


есть более благодарные средства, где не надо костылировать на каждом шагу

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


Значит, вы их не нагружаете соответственно ресурсам.

Опять двадцать пять. Откуда вам-то знать? Что это вообще за манера — делать безапелляционые заявления про интимную жизнь собеседника, которого вы даже в глаза ни разу не видели?

> Как в этом виноват mailbox — тайна великая есть.

Извините, для ответа на это вам надо было всего лишь прочитать моё сообщение.

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

А, selective receive, и правда.


На нагруженном процессе не нужно вызывать selective receive, да. Нужно процессить все сообщения, пересылая их в зависимости от их типа другим процессам, каждый из которых умеет работать только с одним типом сообщений.


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

> А, selective receive, и правда.

Да. Спасибо, что прочли.

> На нагруженном процессе не нужно вызывать selective receive, да.

Вы можете сколько угодно избегать его в явном виде, но:
— Если вы зовёте gen_server:call (явно или неявно, через чей-то API), вы его применяете для получения ответа вызванного процесса.
— Если вы используете отправку по TCP через gen_tcp, вам приходит сообщение в ответ и вы его сразу и ищете в очереди.

> А так, как сделали эти чуваки — делать не нужно. А то так можно любую очередь заткнуть,

Если «эти чуваки» это авторы TCP драйвера Erlang, тут я согласен — так делать просто нельзя — пока это создаёт проблемы.

Если «эти чуваки» это авторы кода в rabbitmq_common — наоборот, они просто защищали себя от дебилизма Erlang, как раз заменив штатное чтение «пятьсоттысячного сообщения, от-nack-иваясь от всех не подошедших» на свой код, который этим не страдает.

Проблема моей задачи была в том, что я не имел возможности устранить косвенный selective receive на gen_call, и некуда было переместить длинную входную очередь — все меры приводили бы в лучшем случае к удлинению цепочки обрабатывающих процессов.

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

А если бы были сделаны раздельные очереди — даже в простейшем варианте неизменяемого порядка выборки, но gen_server:reply отправлял бы в высокоприоритетную — всё могло бы работать и сейчас.
Знаете, может быть мне и не повезло просто в жизни, но в моём опыте я никогда не встречал ситуации, чтобы я, долгое время, просто писал бы код или там, юниттесты. Большую часть времени занимает либо отладка, исправление багов, поступивших от пользователей.

Я 90% процентов времени занимаюсь отладкой и исправлением багов, т.е. мы примерно из одного мира (полагаю уровень абстракции у нас сильно разный, у меня выше, у вас — ниже). И мне в мире исправления багов проще с большим количеством простых абстракций, чем с меньшим количеством более сложных.
Но общую сложность решения это никоим образом уменьшить не может: этот код всё равно имеет дополнительные ограничения, которых исходная задача не имеет.

Вполне может уменьшить сложность введение абстракций. Тупой пример: в задаче нужно несколько раз сложить натуранльное число само с собой. Вводим операцию умножения — абстракция над повторяющимся сложением и, внезапно, куча циклов/редьюсов заменяется одной операцией.


Если уж на то пошло, то любая переменная или константа вместо литерала — это абстракция. И, при условии нормального именования, очень часто они упрощают код.

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

Собственно об этом факте говорит хотя бы то, что большинство компиляторов умеют превратить умножение обратно в несколько сложений (и вычитаний). Примерно так.

Спасибо за «тупой пример», показавший, в очередной раз, мою правоту.

Если уж на то пошло, то любая переменная или константа вместо литерала — это абстракция.
Конечно.

И, при условии нормального именования, очень часто они упрощают код.
Нет. Замена одного литерала на одну константу — никогда не упрощает код.

Упрощение начинается тогда, когда вы начинаете эту константу переиспользовать (хотя бы в коде и в тесте). И да — вот в этом случае вы можете получить упрощение… но это частный случай, про который я уже писал: абстрагировать части решения и использовать их для решения разных задач.

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

Но вот, например, clang-tidy постоянно пытается предложить мне ввести именования вместо чисел 8, 16, 32 и 64. Которые в моём коде записаны явно там, где они указывают ширину регистра (8-битный, 16-битный и так далее). Вы вправду считаете, что какой-нибудь k8BitRegisterSizeWithInBits будет проще понять, чем число 8? Вас не удивляет, что типы char8_t, char16_t, char32_t пришли на замену типам char и wchar — которые как раз «абстрагировали» константы и, в результате, привели к дикой путанице?
Нет. Замена одного литерала на одну константу — никогда не упрощает код.

И снова я с вами не согласен. Читаемое и осмысленное название всяко лучше магического числа которое иногда неясно по каким критериям выбрано, не всегда понятно что означает, и неизвестно а точно ли в одном месте действительно используется или нужно поиском все такие числа в программе менять.
Забавно, что пафоса вы насыпали, а вот предложить на что заменить 8, 16 и 32 — так и не смогли.
А что я могу предложить если я не знаю контекста использования этих констант? Для чего вам ширина регистров в том месте, как используется? Впрочем даже банальный SIZE_8_REGISTER будет лучше магических чисел.
А что я могу предложить если я не знаю контекста использования этих констант?
JIT-компилятор, банально.

Для чего вам ширина регистров в том месте, как используется?
Ну, например если нам нужно прибавить константу. Что-нибудь в духе:
  if (int32_t(immediate) == immediate) {
    as.mov(result, immediate);
  } else {
    auto tmp =
      alloc.AllocateVirtualRegister<GPRegister<64>>;
    as.mov(GPRegister<32>(result), immediate);
    as.mov(GPRegister<32>(tmp), immediate >> 32);
    as.shl(tmp, 32);
    as.or(result, tmp);
  }


Впрочем даже банальный SIZE_8_REGISTER будет лучше магических чисел.
Серьёзно? GPRegister<SIZE_8_REGISTER> более понятно, чем GPRegister<8>? Я вас умоляю. Это карго-культ в чистом виде.

Как минимум потому что после такого «улучшения» банальный as.movsx(GPRegister<size * 2>(r1), GPRegister<size>(r2)); становится проблемой: если это у вас не просто числа, а магические имена, то чтобы перейти от SIZE_8_REGISTER к SIZE_16_REGISTER вам теперь, внезапно, нужна вспомогательная функция… и для обратного перехода — тоже… А вот с тем сдвигом наверху — как теперь быть? Можно там использовать SIZE_32_REGISTER или нужен отдельный SIZE_32_REGISTER_SHIFT?

Ну да — можно всё это развести, компилятор умный, всё лишнее уберёт… Но чего вы этим, извините, добъётесь? Ну усложнения кода — это понятно. А кроме этого? Приятного ощущения на душе, что clang-tidy теперь не ругается? Какое-то сомнительное достижение, как по мне…
Да как раз упращение то. Если у вас есть несколько разных констант со значением 32 например — они все гарантированно будут разделены по смыслу.
становится проблемой: если это у вас не просто числа, а магические имена, то чтобы перейти от SIZE_8_REGISTER к SIZE_16_REGISTER вам теперь, внезапно, нужна вспомогательная функция… и для обратного перехода — тоже…

А тут то в чем проблема? Не говоря уж о том что да, в функцию вынести удобнее — но никто собственно не мешает вам и дальше as.movsx(GPRegister<size * 2>(r1), GPRegister(r2)); использовать, если size один из ее аргументов. Зато сразу в месте вызова не читая сигнатуру можно понять что в функцию передаем, именно размер регистра, а не просто число 8 неясно чего значащее.
Да как раз упращение то. Если у вас есть несколько разных констант со значением 32 например — они все гарантированно будут разделены по смыслу.
Где упрощение-то? Пока я вижу усложнение. Нет, я понимаю, если для вас «просто» = «феншуйно», «так как в умных книжках» написано — то тут уже обсуждать нечего. Нужно от такого работника избавляться.

А если у вас какая-то другая метрика… ну можно что-то обсуждать.

Так по какому, извините, критерию, этот код проще стал?

А тут то в чем проблема?
Проблема в том, что мы не знаем, в соотвествии с теми самыми умными книжками, что именно у нас SIZE_8_REGISTER обозначает. Вдруг у нас SIZE_8_REGISTER = 0, SIZE_16_REGISTER = 1, SIZE_4_REGISTER = 2 и SIZE_64_REGISTER = 3? Чтобы их удобнее было в LEA использовать?

никто собственно не мешает вам и дальше as.movsx(GPRegister<size * 2>(r1), GPRegister(r2)); использовать, если size один из ее аргументов
Мешает. Если я вижу в коде, где-то рядом, GPRegister<8&rt; — то я понимаю, что size — это просто размер регистра в битах, а не, скажем, в байтах.

Убрав эту информацию с глаз пользователя — мы, тем самым, усложнили ему жизнь.

Я ведь недаром с самого начала упомянул char и wchar. Там тоже «улучшайзеры» вроде вас решили «абстрагироваться от размера». В результате получили UTF-16 в Windows, UTF-32 в Linux/Unix и кучу проблем с переносимостью.

Вот то же самое будет и с вашим «упрощающими» константами.

Удивиельным образом константы могут упрощать жизнь только тогда, когда они могут осмысленно меняться! В разных версиях программы, разумеется.

И да — M_E и M_PI не являются контрпримером: e и π — числа трансцендентные, в программе непредставимы, потому в разных программах, на разных системах, могут-таки отличаться.

Зато сразу в месте вызова не читая сигнатуру можно понять что в функцию передаем, именно размер регистра, а не просто число 8 неясно чего значащее.
То есть что значит 8 в uint8_t или char8_t — вам понятно «без слов»? А в GPRegister<8> вдруг стало непонятно?

Я боюсь если вы обладаете настолько плохой памятью, то вам этот код лучше бы вообще не трогать…
Где упрощение-то? Пока я вижу усложнение. Нет, я понимаю, если для вас «просто» = «феншуйно», «так как в умных книжках» написано — то тут уже обсуждать нечего.

Нет. Может для вас понимание из за адекватного нейминга усложняется, но для меня упрощается.
А уж аргумент
«феншуйно», «так как в умных книжках» написано — то тут уже обсуждать нечего. Нужно от такого работника избавляться.
вообще странный. Код стайлы, паттерны и прочее придуманы в т.ч. и как общий знаменатель для разработчиков, чтобы было проще читать код написанный другими людьми, он по возможности должен быть написан единообразно, по общим принципам. А если каждый будет писать как ему вздумается и хочется — то получим мешанину стилей и подходов.
Проблема в том, что мы не знаем, в соотвествии с теми самыми умными книжками, что именно у нас SIZE_8_REGISTER обозначает. Вдруг у нас SIZE_8_REGISTER = 0, SIZE_16_REGISTER = 1, SIZE_4_REGISTER = 2 и SIZE_64_REGISTER = 3? Чтобы их удобнее было в LEA использовать?

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

Ну и опять же, речь не о плохой или хорошей памяти а о единообразии стиля и безопасном программировании.
Можно всегда не забывать на null проверять, а можно взять язык в котором типы делятся на nullable и not nullable и для nullable компилятор заставляет проверять значение.

Удивиельным образом константы могут упрощать жизнь только тогда, когда они могут осмысленно меняться!

Вот вам константа 86400, изменяться она не будет. Очень понятная с ходу, угу.

А уж попытка два раза перейти на личности с вашей стороны выглядит как то странно в обмене аргументами о кодстайлах.
Код стайлы, паттерны и прочее придуманы в т.ч. и как общий знаменатель для разработчиков, чтобы было проще читать код написанный другими людьми, он по возможности должен быть написан единообразно, по общим принципам.
Во всех местах, где я это наблюдал речь шла либо о вещах, которые мало на что влияют (типа int* p; вместо int *p;), либо, если речь шла о серьёзных ограничениях, там были обоснования и процедура получения исключения (waiver).

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

Отражается в документации.
Это антитеза к упрощению. Если вам, для того, чтобы понять код, нужно читать документацию — то вы этот код, извините, не упростили, а усложнили. Потому что это значит, что вам теперь, собственно кода, для понимания, недостаточно. Нужна ещё и документация.

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

Можно всегда не забывать на null проверять, а можно взять язык в котором типы делятся на nullable и not nullable и для nullable компилятор заставляет проверять значение.
Если что-то можно переложить с программиста «на бездушную машину» — это прекрасно. Но вы-то, вашим изменением, сделали обратное — заставили программиста, читающего код, делать больше работы, а не меньше.

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

Слова про «кодстайл» вы произнесли, до вашего ответа мы, вроде как, обсуждали сложность программы.
А чем это плохо?

Тем что код становится сложнее читать, а соответственно и воспринимать.
Это антитеза к упрощению. Если вам, для того, чтобы понять код, нужно читать документацию — то вы этот код, извините, не упростили, а усложнили. Потому что это значит, что вам теперь, собственно кода, для понимания, недостаточно. Нужна ещё и документация.

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

Слова про «кодстайл» вы произнесли, до вашего ответа мы, вроде как, обсуждали сложность программы

Мы обсуждали вынос магических чисел в константы. А это имеет прямое отношение к кодстайлу. Да и не только про кодстайлы, но и про другие общие рекомендации по написанию кода, кои вы обозвали феншуйными книжками. Большинство программистов о них хотя бы имеет представления и им легче будет код воспринимать чем в самобытном стиле и с самобытной организацией блоков и модулей.
Тем что код становится сложнее читать, а соответственно и воспринимать.
Нет. Не становится. Там где разница в стилях реально ведёт к проблемам и можно объяснить почему — можно ведь и, точечно, правила соотвествующие ввести. А если объяснить «почему» нельзя и нужно «давить авторитетом» — то, значит, и правило такое смысла не имеет. Style Guide ведь не зря выводит на вот этот вот, вполне конкретный, style guide. Где для большинства пунктов подробно описаны «за» и «против». И где правила «избегайте магических констант чего бы это ни стоило» в принципе нету.

На плюсах писал давно и совсем немного, нужно было по мелочам по работе, потому и не помню какие там есть средства написать это по человечески.
Для того, чтобы ответить на этот вопрос нужно прежде всего сформулировать что это такое — «по человечески».

В идеале сигнатура должна быть максимально упрощена и не принимать ничего кроме этих значений.
Зачем? Какую проблему это решит?

Енам в этом плане как то лучше, документации не требует, но в плюсах насколько помню они грустные.
Ну сделать этот параметр class enum — это не проблема. Он может быть параметром шаблона. Но какую проблему это решит? GPRegister<42> — это и так будет ошибка компиляции со вменяемым сообщением об ошибке.

Мы обсуждали вынос магических чисел в константы.
Вот только это не «магические числа». Это часть названия. 8 битный — 800 тысяч раз Гугл находит, 16 битный — 300 тысяч, 32 битный — 700 тысяч, 64 битный — почти 800…

Для этого понятия нет никаких недвусмысленных называний, не включающих в себя эти числа. WORD/DWORD/QWORD, предложенные VolCh, извините, неоднозначны.

Большинство программистов о них хотя бы имеет представления и им легче будет код воспринимать чем в самобытном стиле и с самобытной организацией блоков и модулей.
Ну то есть от тезиса «так код будет проще» мы уже отказались? И перешли к тезису «так будет лучше — потому что умные дяди плохого не посоветуют»? Так, что ли?
Ну то есть от тезиса «так код будет проще» мы уже отказались? И перешли к тезису «так будет лучше — потому что умные дяди плохого не посоветуют»? Так, что ли?

Где вы это прочитали? Впрочем я в любом случае уже спорить банально устал.
const f = a => 6.2*a
console.log(f(2))

vs


const PI = 3.1
const f = r => 2 * PI * r
console.log(f(2))

Какую из двух "программ" вам проще понять? Какую будет проще поправить если заказчик скажет "результаты в целом верные, но нам нужно точнее"?

Для человека, который понятия не имеет о том, что такое π? Без разницы, на самом деле. Вторая версия только длиннее и всё.

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

Про 8/16/32/64 вы так ничего и не сказали — что характерно.

С одной стороны, предполагается, что человек, читающий этот код, хотя бы неполное среднее получил. С другой, даже без этого, он хотя бы вопрос сможет задать "а что за константа PI у нас в коде?" и ему куда быстрее ответят, чем на вопрос "а что за 6.2 у нас?"


Контекст был непонятен, с кодом выше чуть понятнее стало. Навскидку, я бы, минимум, сделал константы типа REGISTER_BYTE, REGISTER_WORD_SIZE, REGISTER_DWORD_SIZE, REGISTER_QWORD_SIZE — GPRegister<REGISTER_QWORD_SIZE> выглядит для меня проще, и исключает вопрос типа а можно ли сделать GPRegister<42>. И подумал бы о immediate >> 32 и as.shl(tmp, 32) — это сдвиги на половину ширины соответствующего регистра или какая-то другая логика. Если при смене GPRegister<64> на GPRegister<32> 32 в них нужно заменить на 16, то использовал бы REGISTER_QWORD_SIZE / 2

Навскидку, я бы, минимум, сделал константы типа REGISTER_BYTE, REGISTER_WORD_SIZE, REGISTER_DWORD_SIZE, REGISTER_QWORD_SIZE
Ok, принято.

GPRegister<REGISTER_QWORD_SIZE> выглядит для меня проще
Проще выглядит? Вы это серьёзно? Что вы ответите «наивному чукотскому вьюноше», который спросит почему у вас 128-битная константа не лезет в ваш QWORD? И ссылку на документацию, где чётко говорится о «32-bit words», «64-bit doublewords» и «128-бит quadwords»? Что, дескать, программа у нас для AArch64, но названия мы используем другие обозначения «для простоты»?

И да, мы, внезапно, поддерживаем x86-64 и AArch64. Второе — вообще самая популярная платформа в мире, первое — знаете, тоже ещё не отмерло.

и исключает вопрос типа а можно ли сделать GPRegister<42>
Попробуйте. Будет ошибка компиляции. Всё просто.

Названия я использовал по памяти о разработке более чем 20 лет тому назад. Вы хотели пример — я дал пример.

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

Какую мою неправоту? Что по памяти я неправильные обозначения выбрал?

Похоже придётся разжевать…

Что по памяти я неправильные обозначения выбрал?
В том-то и дело, что память вас не подвела. И я думал что уж про это-то вам напоминать не нужно — раз вы так «упростить» код решили.

В том-то и дело, что в документации по x86 название «word» действительно используется для 16 бит, doubleword — 32 бит и так далее (там, правда, есть забавные разночтения когда то, что одни называют octaword другие называют double-quadword… но то такое).

Вот только в современном мире x86 — не единственная и, в общем, даже не главная, на сегодня, платформа. ARM, Power, RISC-V… у них у всех WORD — 32-битный.

У Alpha Alpha WORD вообще 64-битное число обозначал (хотя сегодня это не слишком актуально)!

Потому название типа REGISTER_DWORD_SIZE — это, извините, не «упрощение», а «лучший способ запутать неприятеля» (каковым вы, как я понял, чаете читателя вашей программы).

В таком случае, да, неудачные названия, из альтернативных интелу платформ (Z80 таким не считаю) я оооочень давно только с 6502 игрался и, вроде бы, "переучиваться" что называть байтом, а что словом не пришлось. Так что считайте, что пример исключительно в контексте интеловской экосистемы, хотя бы потому что другого вы не задали изначально.


Чтобы нормальные примеры, подходящие для столь разных платформ, привести, мне нужно погружаться в них.

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

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


А дальше, внезапно, выясняется, что если оторвать только HttpClient, можно прекрасно протестировать связку из SolarCalculator и LocationProvider, не вводя ни одного интерфейса, и не заботясь о внутренностях их взаимодействия. Да, это не будет чистым юнит-тестом… ну так если нужные автору кейсы можно протестировать без выделения этой абстракции, то и какая разница? Правда, в этот момент становится не очень понятно, зачем эту абстракцию выделяли — ну и хорошо, нашли лишний код.

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

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

Я лично считаю, что юнит тесты — это самые простые для написания тесты, потому что для них нужно минимум инфраструктуры, и пишут их те же программисты, которые пишут код. По факту, это единственные тесты, которые могут тестировать внутренности реализации. Когда дело доходит до интеграционных тестов, оказывается, что заглушки для целых компонентов писать много сложнее. А c end-to-end тестами вообще часто беда, они выполняются долго, требуют сложной инфраструктуры и отдельных тестеров-автоматизаторов для поддержки.
По факту, это единственные тесты, которые могут тестировать внутренности реализации.
То есть тестируют то, что не нужно ни пользователи, ни разработчику, ни вообще кому-либо.

Когда дело доходит до интеграционных тестов, оказывается, что заглушки для целых компонентов писать много сложнее. А c end-to-end тестами вообще часто беда, они выполняются долго, требуют сложной инфраструктуры и отдельных тестеров-автоматизаторов для поддержки.
Гениально просто. Вбухать кучу времени и сил в инфраструктуру поиска ключей под фонорями — потому что она, типа, очень просто делается.

Когда я поломал интеграционный тест — то это, в 90% случаев ошибка, которую нужно править. Если я поломал end-to-end — это это уже в 99% случаев ошибка, без исправления которой релиза не будет.

А в случае с юнит-тестами в 90% случаев — это ошибка, возникшая из-за того, что кто-то закодировал в них поведение, которое я, собственно, и хочу изменить.

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

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

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

Тем не менее, чтобы получить «kernel oops» — вам нужно постараться, а завалить какую-нибудь эту «суперпротестированноу» бизнесс-програму часто можно парой кликов мышкой в неподходящий момент.
Когда я поломал интеграционный тест — то это, в 90% случаев ошибка, которую нужно править. Если я поломал end-to-end — это это уже в 99% случаев ошибка, без исправления которой релиза не будет.

Кто-то поменял API, кто-то поменял внутренее поведение — и куча ваших интеграционных тестов требуют адаптации, все точно так же как и юнит тесты. На самом деле я ни разу не написал, что остальные тесты не нужны. Очень нужны и очень важны! Но чем ниже в иерархии находится тест, тем проще его писать. А правильный дизайн позволяет писать тесты на более низком уровне.
В результате в тех 10% случаев, когда они таки срабатывают «по делу» — они, зачастую, точно так же механически «затыкаются» и свою функцию не исполняют.

Это культура разработки в команде. В моем случае срабатывание юнит-теста как правило означает проблему в коде. И при рефакторинге юниты позволяют сильно упростить верификацию изменений. При разработке нового функционала низкого уровня они позволяют описать требования.
Тем не менее, чтобы получить «kernel oops» — вам нужно постараться, а завалить какую-нибудь эту «суперпротестированноу» бизнесс-програму часто можно парой кликов мышкой в неподходящий момент.

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

И вот уже на следующий день кто-то ищет новую работу, вместе с тем, кто это пропустил на CR. В любом мало-мальски крупном проекте API не может быть изменено, оно может быть только дополнено, если команду не набирали на фестивале мазохистов, конечно.


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

FYI: языки, создатели которых отталкивались от научных работ по CS, а не от собственного извращенного понимания прекрасного, запрещают тестирование деталей реализации на уровне компилятора: в них не существует способа протестировать приватную функцию (как и задокументировать).


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

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

API — Это в том числе и интерфейсы внутренних компонентов сервиса, которые не публичны, могут менятся, и вполне себе тестируются интеграционным тестированием.

в них не существует способа протестировать приватную функцию

FYI: в некоторых из них нет и приватных функций вообще. Но как только ваш приватный код становится достаточно сложным, его приходится вытаскивать в видимую область (пространства имен details, например) и даже делать библиотечным, и покрывать тестами, чтобы избежать катастрофы от безобидного изменения.

Культура разработки — это не менять API, не ломать обратную совместимость — и не тестировать детали реализации

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

Вы бы менторского нарративчика поубавили, а то и так смешно читать, а в утвердительной форме, не терпящей возражений — и подавно. Ни в одной из своих OSS библиотек я не сломал обратную совместимость с версией v0.1.0. Среди них есть и довольно замысловатые. Проект показать не могу, публиковать бизнес-логику в паблик домен мне пока не позволяют, но и там ситуация такая же (несмотря на то, что хайлоад появился только ко второй версии, все старые интерфейсы до сих пор поддерживаются).


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

За что ж вы так коллег-то ненавидите, а? Я бы пришиб любого, кто пришел бы ко мне с приветом «а теперь поменяй все свои вызовы нашего микросервиса, потому что мы переписали API». Ну, не пришиб бы, но переписать набело с нуля с сохранением обратной совместимости — заставил бы точно.


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

Интерфейс вытащите и обтестируйтесь; детали реализации так и останутся деталями, будь они хоть трижды в библиотеке. В тестировании они не нуждаются просто потому, что иначе вы 90% времени будете тратить на озеленение тестов после каждой мини-правки, вместо работы.

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

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


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

Это когда у вас есть способ "заставить". Которого, сюрприз, может и не быть.

Это когда у вас есть способ «заставить». Которого, сюрприз, может и не быть.
Да даже если есть. Во всех крупных публичных проектах, с которыми я сталкивался, (как то: ядро Linux, GCC, Clang, Chromium, Android, далее везде) API всегда чётко разбиты на два класса:
1. Внешние API (1%-10% всех API) — стабильны как скала, не меняются годами, покрыты тестами по самое… в общем хорошо покрыты.
2. Внутренние API (90%-99%) — нестабильны и меняются в любой момент, легко могут оказаться несовместимыми между версиями и т.д. и т.п.
Объяснение просто: поддерживать стабильность — сложно и дорого (как не меряй — хоть в деньгах, хоть во времени, хоть в «потерянных конрибуторах» — всё равно дорого), но это необходимо, чтобы вашим продуктом кто-нибудь мог пользоваться. А вот подерживать стабильность внутренних API — тут сплошные минусы: ими никто, кроме разработчиков не пользуется, но потенциальных котрибутовров вы всё равно потеряете.

Не могу вспомнить ни одного популярного проекта более 100'000 строк (или, тем более, более 1'000'000 строк), который бы имел внутренние стабильные API.
Внешние API (1%-10% всех API) — стабильны как скала, не меняются годами, покрыты тестами по самое… в общем хорошо покрыты.

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

По второму пункту полностью согласен.
То то в андроид постоянно что то становится deprecated, выпиливается из внешнего api, добавляются новые…
Знаете — если бы я в этом всём не варился, то так бы вам и поверил. Да, конечно, там что-то выпиливается регулярно… только этот процесс занимает лет так пять-семь обычно. Фича, которую ввели максимально быстро (из известного мне) — это поддержка PIE. в Android 4.1 добавили, в Android 5.0 поддежку non-PIE убрали. Но это, во-первых, экстремальный случай (я вообще не могу припомнить никакой другой фичи, которую так форсированно вводили бы) — но и даже в этом случае процесс, всё-таки, занял два года.

Можете назвать что-нибудь что было введено быстрее, чем за пару лет?
Да вот например недавние изменения в работе с файлами. В десятке только начали отказываться, просто флаг ввели для легаси, а в 11 уже все.
Тащем то если бы они депрекейтнули прямой доступ и спустя лет 5 его выпилили — я бы согласился что норм. Но они депрекейтнули его только в десятке. А ведь на это была куча либ завязана и софта. Куча всего завязано именно на использование файловых дескрипторов которые с 11 насколько помню станут недоступны (если это не свои файлы приложения, там, опять же если не ошибаюсь, они останутся). Но всяким там видеоплеерам и прочему подлянка та еще.
Куча всего завязано именно на использование файловых дескрипторов которые с 11 насколько помню станут недоступны (если это не свои файлы приложения, там, опять же если не ошибаюсь, они останутся).
Конкретику можно? Я знаю только вот про это: To give developers additional time for testing, apps that target Android 10 (API level 29) can still request the requestLegacyExternalStorage attribute.

Если судить по текущим полиси ещё годик у вас есть.
Ну, годик конечно лучше чем ничего. Про конкретику к сожалению не отвечу поскольку в нашем приложении файлы почти не используются (а там где используется давно от прямой работы с файлами отказались), потому сильно эту тему не изучал. Но в Android Dev Podcast неплохо эту тему раскрывали в одном из выпусков недавних.
Ну, годик конечно лучше чем ничего.
Два годика. Год назад ввели дополнительный permission, ещё год — им можно будет пользоваться.

Собственно то, что вы воспринимаете это как «предупреждение за год» и показывает, что «депрекейтнуть и лет через 5 выпилить» — не работает.

Сколько бы ни было предупреждений и описаний — для большинства разработчиков «обратный отсчёт» начинается тогда, когда чётко становится известна дата «полного и окончательного выпиливания».

И многие начинают вопить вообще в тот день, когда это случается. Думаете когда в конце 2020 года Flash окончательно перестанет поддерживаться — не найдётся куча горе-разработчиков Web-сайтов который только тогда и начнут вопить, что им ничего вовремя не сказали? Я уверен, что найдутся…

Но в Android Dev Podcast неплохо эту тему раскрывали в одном из выпусков недавних.
А почему эту тему раскрывали «в одном из выпусков недавних», а не «в одном из выпусков прошлого года»?
А почему эту тему раскрывали «в одном из выпусков недавних», а не «в одном из выпусков прошлого года»?

В прошлом году тоже обсуждали, но переход на десятку с соответствующими изменениями и гадали что будет в 11. В этом году уже известно что в 11.
По поводу того что все на старом апи сидели, так ведь он и не депрекейтед был до десятки насколько помню.
Кстати еще немного в тему совместимости api — как раз сейчас сижу, думаю как обойти нерабочие ACTION_CREATE_DOCUMENT и ACTION_OPEN_DOCUMENT на сяоми и ванпласах. Которые между прочем рекомендуемый подход для выбора файла и сохранения файла.

Есть нюанс. Даже 5 лет не так уж много. Android 4.4 вполне живой даже у меня. Ну и теперь разработчикам нужно поддерживать два API для одного приложения.

Android 4.4 вполне живой даже у меня.
А у меня есть комп с живой Windows XP. И AmigaOS 3.5. Дальше? Подо всё это софт разрабатывать? Никто так не делает.

И даже «бессмертный» MS IE 6 уже прекратили поддерживать.

Даже 5 лет не так уж много.
Это огромный срок. Процент «живых» смартфонов старше 5 лет сравним с процентом пользователей Windows XP.

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

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

Ну мы в своем приложении даже 4.2 поддерживаем. И примерно 4% пользователей до сих пор на 4.2-4.4 сидит.
UFO just landed and posted this here
За что ж вы так коллег-то ненавидите, а? Я бы пришиб любого, кто пришел бы ко мне с приветом «а теперь поменяй все свои вызовы нашего микросервиса, потому что мы переписали API». Ну, не пришиб бы, но переписать набело с нуля с сохранением обратной совместимости — заставил бы точно.

Если вы действительно проектируете API так, что потом вы его никогда не изменяете, а только дополняете, то я вам искренне завидую, ибо я и большинство других программистов так не умеют. Сколько сталкивался — все развивающиеся API имеют версии и иногда ломают обратную совместимость. Например в Google Maps API фраза «removes deprecated features, and/or introduces backwards-incompatibilities» встречается чуть ли ни на каждую версию. Даже Windows, на мой взгляд образец обратной совместимости, иногда так делает, особенно в драйверах.
Ни в одной из своих OSS библиотек я не сломал обратную совместимость с версией v0.1.0. Среди них есть и довольно замысловатые.

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

Я бы пришиб любого, кто пришел бы ко мне с приветом «а теперь поменяй все свои вызовы нашего микросервиса, потому что мы переписали API».

Это вы своих коллег не любите :-) Кроме внешних интерфейсов с версионированием и обратной совместимостью бывают еще внутренние API сервисов, которые активно живут и меняются, вот интерфейсы классов, например. Они просто обязаны меняться, иначе проект либо обрастает адапторами-костылями, либо дизайн становится неуправляемым спагетти с подпорками.
вот интерфейсы классов, например

У нас нет классов, простите. И адапторов нет. И вообще ООП нет. И связанных с ним проблем — тоже нет.

Как будто отсутствие ООП спасает от проблем расширения API?
Наоборот, ООП способно их лечить.
UFO just landed and posted this here
Установка всяких начальных параметров это тоже часть API, местами очень существенная.
Замените интерфейсы классов на сигнатуры функций и структуры данных.

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


Да, так можно было.

чем ниже в иерархии находится тест, тем проще его писать

Это не так. В модульных тестах нужно много мокать в каждом тесте, а в компонентных достаточно просунуть тестовый контекст и всё. Например: https://github.com/hyoo-ru/todomvc.hyoo.ru/blob/master/todomvc.test.ts

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

Какие проблемы? Вот давайте на конкретном приведённом мной примере.

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

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

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

Возможно, мы по-разному понимаем термины «модуль» и «компонент», это может зависеть от языка, и менеджера пакетов или используемого фреймворка.
Было плохо, стало плохо. Но с интерфейсами и инверсией зависимостей.
Можно ли сказать что метод который читает данные из провайдера и проводит вычисления следует принципу единой ответственности?

В данном случае напрашивается чистая функция которая отвечает за вычисления. Она принимает два аргумента (координаты и смещение) и возвращает результат.

Писать тесты к таким функциям одно удовольствие. Ведь у неё нет зависимости и сайдэфектов. И моков никаких не надо.

Клас с бизнес логикой будет простым как пробка. Взять из сети координаты, передать их в вычисление. Тут без моков не протестить. Но и тестить то особо нечего: тест на позитивный сценарий и пара тестов на обработку ошибок. Тут будет прямо как в статье «Юнит-тесты зависят от подробностей реализации». Если функция имеет сайдэфекты, то их надо тестировать.
Во многом согласен. Возможно, мне не повезло видеть хороших юнит тестов, но в типичной enterprise LoB разработке они выглядели именно так, как описывает автор — хрупкими, не очень полезными и достаточно дорогими в разработке.

Moreover, я уже несколько лет веду неформальную личную статистику по багам, их причинам и «какой тест мог бы поймать этот баг». Багов, которые были бы легко пойманы юнит тестами замечал крайне мало. Разумеется, обратную ситуацию тоже рассматривал — смотрел на юнит тесты и прикидывал, а что они поймали бы. Зачастую это ошибки в роде «ну, если разработчик забудет вызвать функцию, то мы это поймаем».

С другой стороны, это сильно зависит от домена и характера разработки. Когда я ради интереса набрасывал простейший лексер в относительно ФП стиле с чистыми функциями, тестировать его было одно удовольствие. Никаких километровых моков, простые чистые функции input-output. Можно пойти дальше и попробовать описать некоторые свойства системы не в виде простого юнит-теста, а в виде property-based testing. Если пойти и ещё дальше, то вместо тестов у нас появятся типы и compile-time доказательства корректности программы.
но в типичной enterprise LoB разработке они выглядели именно так, как описывает автор — хрупкими, не очень полезными и достаточно дорогими в разработке.


Это также согласуется с моим опытом — такие тесты хрупкие, баги не ловят, стоят как чугунный мост. Гораздо лучше дергать живую систему, с живой БД. Тесты пишутся один раз, покрытие близко к 100% с минимум тестов, они ловят реальные баги и меняются только если изменилось API.
покрытие близко к 100% с минимум тестов

Вот как вы этого добиваетесь, мне очень интересно, конечно.

GIGO. Если код не пытается «защищаться сам от себя», а просто выдаёт чушь если на входе у него чушь, то почти все проверки в коде будут срабатывать на какое-то неправильные состояния в базе данных и/или неправильные действия пользователя. В крайнем случае — при неправильном поведении другого компонента.

Если вы написали какую-нибудь проверку, которую вы ну никак, кроме как с помощью юниттеста, не можете «покрыть» — то проверку следуюет удалить и всё.

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

Меня волнует не столько покрытие, близкое к 100%, сколько утверждение о "минимуме тестов".

А. Это я не знаю. Знаю что у нас, скажем, подсистема графики покрыта почти на 100% чисто интеграционными тестами… но там этих тестов — больше двухсот тысяч. Они пачками по тысяче прогоняются на ботах.

Так что про «минимум тестов» — было бы интересно узнать…

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


Есть сложные кейсы, типа «сломался коннект к БД», для этого есть тестовые коннекторы к внешним ресурсам, которые можно заставить имитировать такие ошибки. Обычно нужно иметь HttpClient, DbConnection и все.

Главное понимать классы эквивалентности на параметрах апи и состоянии системы и проверять каждый.

Ну вот например. Предположим, вы поддерживаете две разных БД. Как вы гарантируете, что система ведет себя одинаково на обеих?

Можно комплект тестов гонять на двух БД. Если БД дергается в каждом вызове апи, то вероятно, проще будет как раз так и делать.


Другой вариант — комплект тестов для интерфейса работы с БД, который гоняем для всех поддерживаемых БД. Мы так делаем для проверки работы с разными блокчейнами (которых около 20) и это удобно, потому что у этого интерфейса всего 3 метода.

Можно комплект тестов гонять на двух БД.

… ну то есть шли тесты 26 часов, будут идти 52. Хмм.


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

Но тогда же надо как-то проверить, что то, что работает с этим интерфейсом, делает то же, что и тесты?

… ну то есть шли тесты 26 часов, будут идти 52. Хмм.

Если вас беспокоит скорость работы тестов, то стоило об этом сказать в изначальном вопросе. Если тесты идут 26 часов то


  • можно заказать 260 спот инстансов на aws и прогнать их за 5 минут
  • можно в тестах и пользовать in-memory sqlite и делать вариант 2 (см. ниже)

Если 26 или 52 часа это проблема, то занимайтесь оптимизацией тестов. Но заменить медленные тесты, которые проверяют поведение реальной системы быстрыми, которые проверяют моки это так себе идея.


Но тогда же надо как-то проверить, что то, что работает с этим интерфейсом, делает то же, что и тесты?

Два комплекта тестов


  1. Проверяет API, в нем либо реальная БД1, либо in-memory sqlite
  2. Комплект тестов для БД, который запускается для БД1, БД2, in-memory SQLite и подтверждает, что они одинаково работают.
можно заказать 260 спот инстансов на aws и прогнать их за 5 минут

Не, нельзя. Денег нет.


Если 26 или 52 часа это проблема, то занимайтесь оптимизацией тестов.

Ну так замена медленных тестов быстрыми — это и есть оптимизация, не так ли?


Два комплекта тестов
  1. Проверяет API, в нем либо реальная БД1, либо in-memory sqlite
  2. Комплект тестов для БД, который запускается для БД1, БД2, in-memory SQLite и подтверждает, что они одинаково работают.

Но как гарантируется, что тесты из (2) покрывают все, что нужно в (1)?

Не, нельзя. Денег нет.

Ага, точно. Есть команда из десятков или сотен человек, которые эту систему пишут и как-то написали сотни тысяч тестов, которые идут 26 часов. Но денег нет. Ню ню.


Но как гарантируется, что тесты из (2) покрывают все, что нужно в (1)?

Есть интерфейс с методами, есть комплект тестов, который этот контракт проверяет. Этот комплект тестов запускается для каждой реализации интерфейса. В реальной системе используется одна из реализации, которая в силу тестов контракта эквивалентна всем остальным.

Ага, точно. Есть команда из десятков или сотен человек, которые эту систему пишут и как-то написали сотни тысяч тестов, которые идут 26 часов. Но денег нет. Ню ню.

На прошлом проекте именно так и было.

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

Ну да, а что вас удивляет? Деньги — они в какой-то момент заканчиваются.


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


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

Проблема в том, что она эквивалентна в объеме тестов. Но откуда вы знаете — и это именно то, что я спросил выше — что тесты эквивалентны использованию?


Простой пример: есть у вас в контракте две операции, А и Б. Хорошие операции, разумные, с описанным контрактом, покрытые тестами, все хорошо. Вы добавляете новую реализацию контракта, и удостоверяетесь, что она тоже эти тесты проходит. Тоже все хорошо.


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

Ну да, а что вас удивляет? Деньги — они в какой-то момент заканчиваются.

Если нет денег добавить поддержку новой БД в систему, то не добавляйте. О чем мы тут вообще говорим?


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

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


Вы хотели знать как получается высокое покрытие с минимумом тестов, не знаю зачем мы тут про часы и разные БД говорим. Покрытие хорошее получается когда программист корректно выделил классы эквивалентности на параметрах и состоянии системы и написал тесты. Тесты дергают внешнее апи и заодно проверяют все внутренности. Когда внутренности изменятся тесты упадут, если не упали, значит поведение апи не изменилось и не о чем беспокоится. Иногда будут неприятные сюрпризы, для этого есть другие инструменты вроде ручного тестирования, мониторинга ошибок и т.п.

Если нет денег добавить поддержку новой БД в систему, то не добавляйте.

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


Вы хотели знать как получается высокое покрытие с минимумом тестов, не знаю зачем мы тут про часы и разные БД говорим.

Вот только пока минимума тестов я так и не увидел. Увидел удвоение тестов при добавлении второй БД.

Вариант 1 вам не нравится, оказывается долго или дорого. Вариант 2 (внезапно) удвоение тестов, хотя я об этом не говорил. Ну значит судьба такая — страдать.

Да нет, удвоение тестов — это вариант 1. А вариант 2, доведенный до логического завершения, как раз и станет юнит-тестами.

Вариант 1 это удвоение времени тестов, не их количества или ресурсов на их поддержку. Вариант 2 никогда изолированным юнит тестом не станет.


  • система все ещё тестируется с живой БД
  • все ещё тестируется внешнее апи системы, внутренности не трогаем

То есть количество тестов пропорционально количеству вызовов АПИ, а не количеству классов в системе. В случае изолированных юнит тестов все наоборот.

Вариант 1 это удвоение времени тестов, не их количества или ресурсов на их поддержку.

Вот тут и вопрос, как вы считаете "количество тестов". Если только по их числу, то дальше можно долго спорить о том, как их делить, и придти к идее, что один тест покрывает систему полностью.


все ещё тестируется внешнее апи системы, внутренности не трогаем

Вы уже тестируете внутреннее API — то, которое между БД и системой, ее потребляющей.

UFO just landed and posted this here
По моему опыту, тесты (в частности, юнит-тесты) помогают не баги отлавливать (это делает QA команда), а дают хотя бы минимальную уверенность в том, что при рефакторинге или добавлении нового функционала что-то да не отвалится. Поскольку тесты пишет тот же человек, что и код, покрывает он только те случаи, о которых разработчик сам догадается. Тест тут магическим образом, увы, не поймает багу. :(
UFO just landed and posted this here
В одном проекте будет нормой минимальный набор юнит-тестов, в другом надо будет покрывать почти все.

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

Я встречал объяснение, что нужно покрывать юниттестами то, что нельзя покрыть другими видами тестов… но это бред!

Зачем вы, вообще, написали код, который никак иначе протестировать нельзя? Кому он нужен? Для чего он применяется?

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

Единственный вариант юниттестов, который я мог бы назвать осмысленным — это «мини-интеграционные тесты»: когда внутри одного компонента были две или три части, которые, теоретически, могли бы быть использованы независимо.

Но в этом случае гораздо полезнее просто разбить этот компонент на два — у вас появится некий API (с описанием, скорее всего), тесты немедленно превратятся в интеграционные… и да — они станут осмысленными.
UFO just landed and posted this here
И где вы тут видите юнит-тесты? Если все эти форматы заданы «снаружи» (как чаще всего и бывает) — то тут вообще ни одного юнит-теста нет. Чистая проверка требований стороннего сервиса.
UFO just landed and posted this here
Я сказал. Нет, бумагу с описанием можете и вы составить — но без подписи на ней контрагента это филькина грамота.
Частично согласен с мнением автора (если я его конечно правильно понял).
Часто встречал (и сам иногда так думал), что если есть автоматическое тестирование (одним из видов которого является unit-тестирование) — значит ошибок нет и задача пользователя решена.
«Обжегшись» пару раз пришел к выводу, что хотя бы иногда разработчиком полезно тестировать то, что они разработали с точки зрения пользователя, а не с точки зрения работают тесты или нет.
Но это не отменяет необходимость наличия unit-тестов (в основном для всяких рефакторингов, а не изменения кода из-за изменившихся требований).
Но это не отменяет необходимость наличия unit-тестов (в основном для всяких рефакторингов, а не изменения кода из-за изменившихся требований).
Для рефакторингов нужны интеграционные тесты, а не юнит-тесты.

Если ошибка в вашем коде никак не проявляет себя при взгляде «снаружи» — то это, извините, не ошибка.

В крайнем случае (да, такое бывает) — это ошибка в документации на «внутренности» компонента. И да, такое бывает, и нет, я не считаю что для того, чтобы такие ошибки в документации отлавливать — нужно тратить столько времени на создание и поддержание юниттестов, сколько обычно на них тратят…

Если в проекте есть отдельные компоненты, то их стоит протестировать юнит-тестами. Дабы потом было легко рефакторить и дорабатывать. Конечно, если код не отличается слабым связыванием, то юнит-тесты могут быть болью. Однако ж в процессе их написания может внезапно оказаться, что жёсткие связи можно ослабить, сделав приложение более гибким в отношении модификаций и доработок.


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


И не стоит забывать, что бизнесу нужна прежде всего бизнес-логика. Строго говоря, бизнесу до определённой степени наплевать, как у вас там все устроено. Так что функциональные, интеграционные и e2e тесты безусловно с точки зрения бизнеса важнее. Юнитовые тесты же скорее помогают самому программисту. Хорошо написанные юнит-тесты могут даже служить своего рода документацией.

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

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

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

Зачем вы хотите это делать? Какова цель?

Строго говоря, бизнесу до определённой степени наплевать, как у вас там все устроено.
Не только бизнесу. Вам, на самом деле, тоже наплевать. Поведение, не наблюдаемое «снаружи» ни на что не может повлиять — просто по определению.

Строго говоря, бизнесу до определённой степени наплевать, как у вас там все устроено.
Не только бизнесу. Вам, на самом деле, тоже наплевать. Поведение, не наблюдаемое «снаружи» ни на что не может повлиять — просто по определению.

Хорошо написанные юнит-тесты могут даже служить своего рода документацией.
Это, пожалуй, единственное для чего они хороши. Но если вы уже озаботились этим — то стоит для начала подумать о документации в принципе. А уже потом заморачиваться юнит-тестами…
Тесты, проверяющие API между компонентами обычно называются интеграционными. И да, разумеется они нужны.

А я не про них говорил. Вот вам пример: на входе имеем формат (а), на выходе имеем формат (б). Отличный кандидат на юнит-тесты.


Просто не нужно пытаться тестировать отдельные его части — и всё.

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


Зачем вы хотите это делать? Какова цель?

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


Вам, на самом деле, тоже наплевать.

Не надо за меня говорить… Мне не наплавать, как мой код написан и организован. Наплевать обычно тем, кто работает на короткой дистанции. Но это не про меня.

Если вдруг выяснится, что под новый бизнес-кейс можно заюзать уже имеющийся код с некоторыми модификациями для унификации подходов.
Это, извините, ни разу не цель. Бизнесу пофиг что вы там и куда «заюзали». А зато когда костыли, сделанные для заказчика A, внезапно, вызывают проблемы у заказчика B, и попытка это всё поправить ломает всё нафиг у заказчика C… то лучше уж без «юзания имеющегося кода» и юниттестов.

Хотите переиспользовать код — оформите его в отдельный компонент с продуманным и документированным API. После чего уже его и протестируйте.

Наплевать обычно тем, кто работает на короткой дистанции. Но это не про меня.
Это таки про вас. Потому если бы вы работали с кодом, которому 5-10 лет и который всё это время активно правился бы — то вы бы прекрасно понимали, что смешать в кучу два совершенно разных куска кода, только потому что у них логика процентов на 60 совпадает на сегодня — лучший способ порождения жёсткого легаси, которое, через не слишком большое время, можно будет только выкинуть.

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

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

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

Как правило, это означает добавление еще одной сущности, которая будет «дружить» вашу логику со сложными инвариантами и понятными входами и выходами, и ту сущность, которую раньше приходилось мокать, и написание некоторого количества boilerplate кода, который будет заниматься пробрасыванием вызовов. Но уменьшение количества сущностей любой ценой не должно быть самоцелью, если хорошая архитектура является приоритетом.

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

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

Моки к интеграционным тестам обычно не имеют отношения. Они используются в частности для тестирования кода, который использует Dependency Injection. В частности для того, чтобы не инициализировать кучу того, что для тестов не требуется. Ну и еще для того, чтобы избежать реальных запросов вовне.

Зависит от того, что именно понимается под интеграционными тестами.

Я же не просто так тесты назвал псевдоинтеграционными. Использование моков как раз делает юнит тесты похожими на интеграционные.

UFO just landed and posted this here

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

UFO just landed and posted this here
Доказательство тоже нужно правильно написать, что может быть ни разу не просто, а иногда вообще не понятно — возможно ли. Зато, если я правильно понимаю, его состояние часто более детерминировано — либо доказано, либо нет, в отличии от юнит-тестов, которые могут быть неполными. Не забыть обновить помогают административные меры в виде TDD и средства автоматизации, которые не дадут запушить в мастер, если тест сломался.
Кстати вопрос, как выразить в типах корректность функции, которая к моменту времени прибавляет рабочее время в часах и получается момент времени в будущем, отвечающий следующим требованиям: момент времени находится в промежутке с 9:00 до 18:00 в рабочий день, согласно производственному календарю, если момент времени попадает на конец рабочего времени(18:00 например), то его нужно перенести на начало следующего с учетом выходных и праздников(например на 9:00), если добавляемое количество времени меньше либо равно количеству рабочих часов в день(например 8), то добавлять час обеда, если точка отсчета находится в нерабочее время(ночь, выходной), то приводить ее к началу ближайшего рабочего дня, если день сокращенный, то учитывать его как полный. Может еще что-то забыл, давно было, но как вы догадались, это про расчет сроков некоего бизнес-действия.
Эту задачу с наскока пыталось решить три разных человека, все время находились какие-нибудь новые граничные случаи, которые забыли учитывать или ломали старые. Я написал классический юнит-тест и наконец победил ее, но не с первой итерации, потому что как раз не все описывал в юнит-тесте. Но чем тут могут типы помочь — я пока к сожалению даже приблизительно не понимаю, особенно учитывая наличие завимости в виде «производственного календаря».
UFO just landed and posted this here

Юнит-тесты, конечно, не панацея, но они имеют два важных преимущества, которых не дают другие виды тестов:


  • простота тестирования граничных случаев. Иногда, чтобы протестировать поведение системы в каком-то очень специфичном случае может потребовать тонкая подгонка входных параметров — зачастую очень нетривиальная, требующая знания деталей реализации каждого из звеньев цепочки вызовов. В случае юнит-теста мы просто передаем граничные параметры, на которых хотим проверить работу определенного метода, в этот метод.
  • устранение комбинаторного взрыва. Представим модельный пример, в котором есть 4 метода, вызывающих друг друга, в каждом из которых выполнение может идти по 4 разным путям. В случае, если мы захотим качественно покрыть e2e тестами все пути выполнения, нам придется подобрать 44=256 различных сочетаний входных параметров, что может оказаться весьма непростой задачей, и очевидно, выполнено в полном объеме не будет — разработчик покроет тестами наиболее вероятные сценарии, понадеявшись, что и остальные работают корректно. В случае юнит тестов, нам может хватить 4*4=16 тестов, чтобы проверить все ветви исполнения кода — и это уже выполнимая задача. Это, конечно, все еще не гарантирует, что система работает корректно (одних юнит-тестов недостаточно), но сильно повышает нашу уверенность в ней.
устранение комбинаторного взрыва.


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

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


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

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

Условно, для int32 у нас 4 миллиарда варианта входных параметров, но с т.з. конкретного метода могут быть различия для категорий «все отрицательные», «ноль», «положительные до тысячи», «положительные от тысячи и более» — их-то мы и проверяем.

Для этого вам нужно знать детали реализации, писать тест в соответствии с ними, и поддерживать соответствие между тем и другим. Если завтра у вас «положительные до тысячи» превратились в «положительные до 1001», а тест вы не изменили — то всё, ничего особо хорошего вы уже не тестируете.

Вот наиболее ярко плюсы TDD в таких случаях проявляются. Прежде чем изменять в коде 1000 на 1001 — нужно написать тест, который упадёт на 1000, а после изменения пройдёт.

При изменении бизнес-требований надо менять бизнес-логику И тесты. Ваш КО.


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

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

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

Ну вот было в тесте 500 для "положительные до 1000", тест не упал, когда изменили поведение на "положительные до 1001". Тест как фиксировал поведение на 500, так и фиксирует. Ничего не случилось. Ничего не сломалось.

Положительные до 1000 имеют 2 граничных условия: 1 и 999 — их и надо проверять, а не 500.

Далеко не всегда это возможно. Например если ваша функция имеет в 1000 особую точку, то может так получиться что время её вычисления быстро растёт. И попытка прогнать тест на 999 закончится тем, что он будет работать сутки. А если чисел больше 300-400 не ожидается — то проверить на 500 будет достаточно.

В общем случае тесты — это тоже часть написанного вами кода и они тоже участвуют в определении сложности того, что вы сделали…

Если вам не нужна поддержка чисел больше 500, то значение выше этого значения должны просто кидать ошибку или хотя бы варнинг. Если же вы хотите их поддерживать, то придётся-таки проверить и 999 и 1000. Если же вам не важно что там творится на 999, то ваш диапазон — "положительные до 500", что должно быть отражено в документации с указанием того, что значения выше 500 могут быть некорректными, ибо никем никогда не проверялись.

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


Далеко не всегда такая задача стоит.

А для чего ещё тесты писать?

Зафиксировать поведение кода на основных сценариях.

А на не основных пусть хоть в бесконечном цикле крутится?

Ага, пока баг-репорта не будет. :) Тогда зафиксируем желаемое поведение, увидим упавший тест, и поправим код, чтобы все тесты проходили.

Тестами доказать корректность всё равно невозможно — как ни крути, они описывают только небольшое количество случаев относительно всех возможных входов.

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

UFO just landed and posted this here
Это всё конечно хорошо, только невозможность доказать корректность тестами (с которой вы согласны) противоречит вашему предыдущему ответу:
А для чего ещё тесты писать?

на сообщение
Это если вы пытаетесь тестами доказать безошибочность кода…

Далеко не всегда такая задача стоит.


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

Ребят, вам вот заняться что-ли больше не чем, как доёбываться до формулировок? Учитесь понимать, что имел ввиду автор, а не триггериться на слово "доказательство".

UFO just landed and posted this here

У вас есть корневой метод и N классов эквивалентности их и нужно проверить и написать N тестов. «Внутренние» методы проверять не обязательно. Проверка внутренних методов не изменит количества тестов внешнего, их будет все ещё N.

На простых примерах это работает. В сложной системе может получиться ситуация, когда вызов такого корневого метода задействует несколько десятков других сущностей, и вместо ожидаемого результата 64578.65 мы получаем 123577.22 — и вот совершенно непонятно, на каком этапе вычислений получена ошибка. При хорошем уровне покрытия этих связанных сущностей модульными тестами, большая их часть будут зелеными, и исправить надо красные, после этого, если все ОК, то и e2e зазеленеют.


Из наиболее эпичных примеров, где плотное использование модульных тестов было настоящим life saver -ом — у меня был расчет перестраховочной премии. На входе всего три параметра — даты начала и окончания периода, да номер договора перестрахования, а на выходе — всего одно число. Зато неявных параметров — вагон и маленькая тележка. Поэтому отдельно проверяется логика (несамая тривиальная) отбора действующих договоров прямого страхования за период, отдельно — учет кумуляции в рамках застрахованного лица (там тоже кхм, интересные выверты в требованиях были), отдельно — логика преобразования валют (с учетом, что произвольный курс может быть зашит в договор перестрахования — а может не быть, и тогда надо брать курс ЦБ), отдельно — отнесение прямых договоров к тому или иному лееру договора перестрахования, отдельно — логика отбора бордеро предыдущих периодов, которые тоже участвуют в кумуляции и т.д.и т. п. E2e тесты просто непрерывно были бы красными, не давая никакой информации, на каком же этапе возникла ошибка. (Что не умаляет их нужности; они тоже нужны, но их одних тоже недостаточно).

Ах, ну и да, в чистом e2e я вынужден был бы создавать десятки договоров прямого страхования, подбирая параметры так, чтобы получить нужные страховые суммы, и при малейших изменениях в модуле учета договоров (добавлен новый обязательный параметр) они бы переставали работать, хотя в перестраховании не поменялось вообще ничего. При тестах на моках я просто заставлял репозитории возвращать данные, необходимые для теста.

На простых примерах это работает.

N классов эквивалентности на «состояние системы и параметры вызова API» все равно проверить нужно, другого пути нет, юнит тесты не снизят это число.


При тестах на моках я просто заставлял репозитории возвращать данные, необходимые для теста.

Какая разница положить в базу нужные данные или заставить репозиторий возвращать нужные данные? Кажется строчек кода будет столько же. В случае с базой заодно ещё код репозитория протестируем, как минимум happy path, часто этого хватает.


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

В перестраховании может ничего и не поменялось, но апи перестало работать и тест корректно отвалился.

Какая разница положить в базу нужные данные или заставить репозиторий возвращать нужные данные? Кажется строчек кода будет столько же.

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

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

От них этого и не требуется, от них требуется зафиксировать логику работу конкретного юнита, а не всей системы.

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

Внешнего апи чего? Я про тестирование внешнего апи юнита, например, публичных методов класса.

Фиксировать апи «юнита» довольно бессмысленно, чаще всего, потому что оно имеет тенденцию меняться по мере изучения предметной области и появления новых требований. А вот внешнее апи системы, оно важнее всего, его и нужно тестировать (фиксировать поведение). Все что внутри это ваше дело, тестам тоже совсем не обязательно знать, что там у вас внутри творится. Кроме некоторых случаев, когда белый ящик действительно нужен.

В моей практике часто наоборот: поведение "кирпичиков" редко меняется, а часто меняются процессы, из них составленные, и/или их параметры (в широком смысле слова). Вплоть до А/Б тестирования различных гипотез.

Вот для этого и нужны юнит-тесты — зафиксировать поведение этих самых «кирпичиков». Чтобы если где-то в них таки довелось сделать изменение (рефакторинг), сразу было видно, не повлияло ли это на их поведение.

Да я-то знаю это. Но общий смысл поста и многих комментариев "нам не нужно фиксировать поведение кирпичиков"

Я это уже заметил, потому и перестал здесь дальше спорить :)
Ну как сказать. Вот есть какой нибудь интерактор со своей логикой который что то из репозитория читает, обрабатывает как то и делает несколько записей. Мы можем удостовериться что при чтении определенных данных он для этих данных отправляет корректные запросы на запись в репозиторий и защищаем себя от того что какое то условие могло неправильно сработать и не записать данные, либо записать лишние данные. Плюс проверка что данные были корректно обработаны для конкретных входных данных.
З.Ы. в приложении над которым работаю тестов почти нет, но это потому что либо логика слишком тривиальная чтобы ее тестировать, либо слишком сложная для этого (UI), и для небольшого приложения которое разрабатывают полтора землекопа тесты не настолько нужны чтобы тратить процентов 30-40 времени на разработку тестов для этих сложных элементов.
UFO just landed and posted this here

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


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

UFO just landed and posted this here

Но это же другая задача. Есть даже мем на эту тему: 2 unit tests, 0 integration tests (про дверные ручки). Просто юнит-тесты и интеграционные тесты решают разные задачи. На разных уровнях. Если перевести язык в плоскость аналогий, то то, что машина может ехать на 4-х колесах, и что они не цепляют в ней другие детали, и что они монтируются хорошо (= интеграционные тесты), не отменяет важности проверки надежности колеса самого по себе (= юнитовые тесты), причем отдельно резину, отдельно диск.

Забавно, что отцы основатели XP и TDD совсем не такие unit тесты имели в виду. Тут Фаулер об этом пишет, в его терминологии они имели в виду Sociable unit test, это ближе к тому, что обычно именуется интеграционным тестированием. А потом набежали «толкователи книжек» и мы получили моки и solitary unit tests, которые хрупкие, дорогие, багов не ловят, зато канонические, соответствуют установившейся догме.

Indeed when xunit testing began in the 90's we made no attempt to go solitary unless communicating with the collaborators was awkward (such as a remote credit card verification system).

Это цитата из статьи Фаулера.
Ну то есть, в результате, приходим туда, с чего начали:
1. То, что «отцы-основатели» называли «социальные юнит-тесты» — сегодня мы называем интеграционными — и их, вроде как, все считают полезными.
2. «Solitary unit tests» (то, что сегодня и называется юнит-тестами) — есть типичное порождение «архитектурной астронавтики» и являются бессмысленной тратой времени и сил.

А дальше — да, не договорившись о терминах можно спорить «мимо» друг друга вечно…

Отец-основатель PHP — Rasmus Lerdorf — тоже видел будущее PHP иначе (если вообще видел). ;) Но это ведь не повод слепо следовать тому, что он говорит сейчас.

UFO just landed and posted this here
Юнит тестирование — это сфера где оверинжиниринг сделать проще чем добиться профита. А считать лучше деньги которые они должны экономить и тогда будет правильный стимул их не запускать.
В общем, конкретный пример. Есть у меня простенький самописный QueryBuilder (на PHP, пара десятков классов всего). Сложные запросы он не умеет, там надо уже SQL писать, но SELECT по какому-то полю, INSERT одной или нескольких записей, UPDATE и DELETE он умеет, — часто мне только это и нужно. Примеры (считаем, что $db уже создан, он наследует PDO):
$user = $db->users->id[12]; // получили пользователя, или null если нет

$db->users[] = [ // вставляем пользователя
    'name' => $name,
    'email' => $email,
    'password_hash' => password_hash($password)
];

$db->users->id[12] = [ // обновляем
    'name' => $anotherName
];

unset($db->users->id[12]); // удаляем

Так вот, мне надо протестировать всё это (на самом деле намного больше), для того чтобы при работе над очередным проектом не начали невовремя лезть непонятные баги. Объясните, какие тут интеграционные тесты я должен написать. Или не писать вообще? Или может таки юнит-тесты? За каждый тип запроса отвечает отдельный класс, а $db их в себе просто вызывает. И да, не советуйте мне уже существующие супернавороченные билдеры — мне не нужны пара десятков зависимостей и сотни файлов с ними впридачу.
Query Builder выдаёт SQL, его как конечный результат и проверяйте.
мне не нужны пара десятков зависимостей и сотни файлов с ними впридачу
(думаю о стоимости дискового пространства) Ваше время невероятно дёшево. Таки возьмите хотя бы Laravel или Yii, повысьте немного свою стоимость. Вопрос одного composer require чтототам.
0. Это не совсем чисто Query Builder. И не надо здесь про «чистый код», мне нужен был инструмент, я его написал. Я знаю, что там не все в порядке с архитектурой, но я и цели не ставил перед собой — написать код 100% соответствующий всем нынешним хайповым трендам.
1. Я не про дисковое пространство, очевидно же.
2. Laravel для небольшого скрипта командной строки, которому дается на вход CSV и по определенным критериям тот должен дописать/обновить БД? Или для веб-страницы с тремя формами, которая нужна только на несколько раз? Вы шутите? А если сам заказчик против использования чего-то тяжеловесного (и такое бывало)?
3. Вообще-то здесь как бы про юнит-тесты спор. А Вы о чем?
1. Я не про дисковое пространство, очевидно же.
Тогда я не понимаю в чем проблема. Количество файлов еще влияет на inode#, но навряд ли в наше время их не хватает.
Laravel для небольшого скрипта командной строки
Можно не весь фреймворк, а тот кусок, который для БД. Например, composer require doctrine/dbal.
А если сам заказчик против использования чего-то тяжеловесного (и такое бывало)?
Едва ли бизнес может так говорить в настоящий момент. Скорее всего, его неправильно информировали.
А Вы о чем?
О вашем странном комментарии про сотни файлов.
Объясните, какие тут интеграционные тесты я должен написать.

— Если вы сделали DELETE, то SELECT должен вернуть NULL
— Если вы сделали INSERT, то SELECT должен вернуть точно такой же объект (за вычетом id)
— Если вы сделали UPDATE одного или нескольких полей, то SELECT должен вернуть объект в котором изменены эти и только эти поля
По-моему неплохой стартовый набор для property-based-testing. Пусть тестовый фреймворк вам генерирует тесты.

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


Опишите комплект тестов типа «вот исходное состояние БД, вот набор вызовов QueryBuilder, проверить, что в результате из БД получили нужные данные» и запускать этот комплект для каждой БД. Так вы будете знать, что целевая БД действительно может ваш SQL переварить. Если же вы генерите, по большей части ANSI SQL, и уверены в том, что целевые БД его осилят, то генерите строки и проверяйте их. Но как только ваш QueryBuilder начнёт делать CTE, CONNECTED BY и прочие advanced штуки без реальной БД вам не обойтись.

Тесты давно написаны. Всё работает как минимум на SQLite, MySQL и PostgreSQL. И да, все запросы это ANSI SQL, там нет каких-то продвинутых штук для которых бы еще и драйверы для каждой БД писать.
Моки — это дешевый способ воспроизводства среды выполнения или состояния, окружающей метод/ограниченный граф вызовов. Когда моки перестают быть дешевыми, разумеется, придётся что-то менять. Скорее всего, тестируемый граф перерос возможности изолированного тестирования, или был изначально слишком широк.
Взаимодействие со сторонними сервисами, внешние контракты, не входящих в тестируемое приложение, стоит тестировать в отдельном контуре. И запускать тесты, к примеру, по крону, а не в общем CI. Потому что проблемы с сетью (они будут) или с самим сервисом не должны блокировать отдельный деплой, который с большой вероятностью никак не менял взаимодействие со внешним сервисом. Не стоит вводить крупные элементы индетерминизма в CI. Иначе будут регулярные красные билды и повышенный уровень кортизола у команды (мы сильнее стрессуем от факторов, которые не под нашим контролем).

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

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

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

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

Лично я, когда только попробовал юнит-тесты, осознал, что все что писал до этого — было «тяп-ляп и в продакшен». И очень рад, что начал писать тесты. Юнит, приемочные, интеграционные, в будущем планирую еще мутационное тестирование попробовать.

Огромная статья, вся суть которой сводится к следующим мыслям:


  1. Не используйте юнит-тесты для тестирования не-юнитов
  2. Не забывайте что пирамида состоит из нескольких уровней
  3. Люди забывают и получается плохо — не будьте как эти люди
Юнит-тесты прекрасная вещь… если у вас продуманная архитектура. Но если у вас хаос — то они вам не помогут.

Они могут помочь сделать продуманную архитектуру. Если хотя бы думать о том, как протестировать ту или иную функциональность перед тем как её реализовывать.

в большинстве случаев упор на юнит-тесты является пустой тратой времени.

Алилуйя! Не прошло и 10 лет, чтобы осознать это. Еще в недалеком прошлом адепты секты TDD буквально растерзали бы за подобного рода заявления.


Добавлю.


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


.6. Согласно Lean, юнит тесты не привносят value в конечный продукт. Это внутренняя инициатива разработчика, однако их написание и поддержка связаны с затратами.
Как правило проект состоит из компонентов, которые связываются в функциональные модули, которые в свою очередь используются в необходимых бизнес сценариях. С точки зрения заказчика именно правильное выполнение сценариев является изначальной задачей и критерием приемки работы. И, как правило если не работает какой-то компонент, бизнес сценарий также должен вальнутся. Юниты иногда помогают быстрее локализовать этот сбой.

Согласно Lean, юнит тесты не привносят value в конечный продукт.

Это где такое написано? Если это ваше мнение, то тогда это не "Согласно Lean", а что-то вроде "мне кажется, что юнит тесты не привносят value в конечный продукт, а это противоречит Lean"


Это внутренняя инициатива разработчика

Ох, далеко не всегда.


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

Проверить все области эквивалентности для более-менее сложного сценария если не невозможно, то сложно. Взять банальную валидацию формы из десятка полей, часть из которых зависима. Писать и гонять приёмочные тесты на каждую их возможную комбинацию с поднятием всё инфраструктуры приложения долго и дорого. Юнит-тест сделает это гораздо быстрее и дешевле, а приёмочных сценариях проверить только два кейса: форма валидна и форма не валидна.

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

  • Подняли систему
  • Пульнули в неё 200 запросов с неверной формой, проверили, что все 200 раз апи ответило адекватной ошибкой.
  • Вырубили систему.

По сравнению с юнит тестами потратили 10 сек на запуск системы, но проверили ещё


  • Cериализацию и десериализацию, вдруг кто-то навешал странные атрибуты на модель?
  • Error handling middleware, а правильно ли ошибки валидации превращаются в ответы API?
  • Убедились, что апи вообще доступно по указанному адресу.

Понятно, что валидацию довольно просто проверить с помощью юнит теста. После чего все равно нужно написать интеграционный тест, который в живую систему пульнет как минимум один корректный и один некорректный запрос. Но если у вас это есть, то к одному некорректному добавить ещё 199 не составит труда. А так как систему между тестами перезапускать не обязательно, то скорость 199 проверок вполне сопоставима со скоростью выполнения юнит теста.


Исторически, когда xunit появился систему между тестами никто не перезапускал. Система и IDE это был один процесс ОС, программа была запущена до тестов и продолжала работать после тестов. Поэтому «поднятие инфраструктуры приложения» не являлось проблемой. Проблемой это стало только после того, как идею юнит тестирования перенесли на Java.

По сравнению с юнит тестами потратили 10 сек на запуск системы, но проверили ещё

Вот тут и зарыта собака. Пока у вас поднять систему — это 10 секунд, все неплохо. А когда "поднять систему" — это несколько минут, и сами тесты тоже на порядок или два медленнее (т.е., каждый тест проходит секунд пять вместо полусекунды), разница в длине итераций становится ощутимой.

Это повод не тестировать публичное апи системы? А если у вас публичное апи покрыто тестами, то что вам добавят юнит тесты?

А если у вас публичное апи покрыто тестами, то что вам добавят юнит тесты?

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

И ради этого вы предлагаете тестировать каждый класс в системе изолируя его с помощью моков?

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

Чем это отличается, по вашему мнению, от моей позиции?

А я, вроде, ничего не успел сказать про вашу "позицию". Я всего лишь сказал, что тесты, поднимающие систему, удлинняют итерацию по сравнению с теми, которые этого не делают.

Именно поэтому и появилась вся эта хрень про юниты — в эпоху серверов приложений, когда билда и деплоя приходилось ждать по нескольку минут. Поэтому люди начали извращаться — мокать сервисы контейнера, чтобы хоть как-то тестировать куски кода локально без деплоя. И отсюда же пошло большинство современных (анти-)паттернов проектирования типа Repository, Context Object, etc. Более того, большинство кейсов вообще невозможно протестировать юнитами, ибо их логика зависит от контейнера и контекста выполнения (Transactional, RequestScoped, etc...).


Поднять систему за 10 секунд — это плохо. Система должна стартовать локально за 1-2 секунды в тестовой конфигурации, причем по возможности без моков и с персистенцией. И гнать надо ссаными тряпками все эти фреймворки, которые стартятся 10 секунд и больше.

Система должна стартовать локально за 1-2 секунды в тестовой конфигурации, причем по возможности без моков и с персистенцией.

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

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

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

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

Очередной не такой как все и не согласный со всеми + громкий заголовок
Тоже пришел к примерно таким же выводам, как и в статье.

Юнит-тесты слишком мелкие и хрупкие, но они отлично подходят для тестирования каких-нибудь алгоритмов и расчетов. Т.е. там где чистый код на языке программирования, без вызовов внешних систем/БД и так далее. Для таких случаев я их и использую. Моки в 95% там и не нужны.

В основном я пишу интеграционные (функциональные).

На одном из проектов видел следующий подход — утилита, которая кидается http запросами к web api. Утилита самописная, тесты оформляются в виде внешних json файлов. Оставляя удобство за скобками — мне этот подход не очень понравился. Это было больше похоже на smoke тестирование, нежели проверку конкретных сложносоставных кейсов.

Я же выбрал несколько другой — интеграционные тесты на бизнес-логику и все что ниже (обращения к БД). БД я подготавливаю специальным образом. Раньше делал восстановление БД перед тестами, в последнее время перешел на запуск тестов в транзакциях с откатом. Активно использую атрибут TestCase из NUnit, чтобы натравить на один блок кода несколько различных кейсов. Вызовы внешних чужих сервисов (погода, гео-кодинг и тому подобные) обычно делаю через моки. При таком подходе не надо поднимать целый веб-сервис. Достаточно зарезолвить из контейнера экземпляр тестируемого бизнес-кода. Но минусы все равно есть — такие тесты менее хрупкие, но не защищены от изменений структуры моделей и dto. Зато отлично защищают при рефакторинге и доработках.

Для тестирования обращений к внешним вызовам — пишу отдельные интеграционные тесты. Там непосредственно вызывается некий сервис по http. Эти тесты в отдельной сборке и не включены CI/DC, чтобы не тратить бабло платных внешних сервисов при каждом прогоне.

И еще надо взять за основу правило: пришел баг в техподдержку — дописал тест.

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

Обращение к внешнему сервису я оборачиваю классом оберткой. Внутри либо try catch с возвратом null/default, либо проброс исключения наверх.

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

А вот как бизнес-логика работает с хорошим/плохим ответом от внешнего сервиса — там да. Но там у меня тоже интеграционные + моки на вот эти классы-обертки.

Это работает только для тех случаев, если обработка отказов тривиальна. А если надо протестировать ретраи, переключение на другой инстанс, откат каких-то состояний?

Эти штуки я не делал. Потому что по ту сторону сервисы, которые обещают 99% SLA. А если сеть упала на проде — то у меня проблемы посерьезней, чем недоступность сервиса погоды.

В тех случаях, когда у меня был значимый сервис для бизнеса — тот же ретрай обеспечивался очередью или hangfire-ом, а откат — транзакцией БД, но они базировались на логике исключений (причем любых, не только во время обращения к внешним сервисам). В итоге обращение к сервису все равно было тривиальное, а вот, скажем так, «инфраструктурная» (не бизнесовая) обработка результата операции — не совсем.

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

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

Рассуждайте критически и подвергайте сомнению best practices

Это, конечно, хорошо, что автор начал мыслить критически и перестал тупо применять все известные best practices. Юнит-тестирование, безусловно, является средством из раздела best practices, но это не означает, что внедрение его в проект безусловно принесет пользу.

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

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


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


А для вненших сервисов куча МОКОВ но только с позитивными сценариями.


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

все системные типы и классы на свои обёртки и делигаты, только что бы их замочить
Прям все? Например?