Писать ли Unit-тесты до готовности MVP

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Писать ли Unit-тесты до готовности MVP (минимально жизнеспособного продукта)? Какие у вас есть аргументы ЗА и ПРОТИВ, поделитесь пожалуйста опытом.
— Команда, в которой практически нет опыта написания модульных тестов и которая перешла на scrum.

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному2
LinkedIn

Найкращі коментарі пропустити

Я считаю что Unit-тесты писать очень полезно — но нужно делать это правильно!
Во-первых тесты нужно писать вместе с кодом. Это не значит жестко TDD (оно на практике редко выходит), но обычно у меня получается так: сначала интерфейсы и пустая реализация, потом тесты для нее — потом уже имплементация.
Во-вторых Unit-тесты должны быть максимально простыми и краткими. Иначе они не просто не помогают — а вредят! Написать и потом поддерживать тест на сотни строк и десятки проверок — это та самая потеря перфоманса, из-за которой все время пытаются отказаться от тестов.
Правильные тесты оказывают как минимум 2 огромных пользы:
Во-первых что-бы тесты были маленькими и простыми они должны тестировать маленькие и простые куски кода. Это заставляет девелопера делать декомпозицию, выделять интерфейсы, уменьшать связность и рефакторить. Сложно писать тест, он получается большим? Значит разбиваем монолитный код и пишем 5 маленьких простых тестов. Не получается подсунуть моки? Значит нужно выносить зависимости и делать интерфейсы. Даже не думая о SOLID девелопер поневоле его применяет!
Во-вторых если интерфейсы это контракт декларации, то тесты — это фактически контракт поведения! Тест гарантирует что через месяц кто-то не «починит» и не начнет возвращать, например, 0 вместо NULL. Только автор метода знает как он должен работать — и вот это свое знание он покрывает тестами что бы потом другие, которые недопоняли — не поломали.
Если же пойти чуть дальше то можно вспомнить Open-Closed принцип. Суть которого в том, что однажды написанный код не должен меняться! Это очень логично: один девелопер написал и протестировал метод именно таким, как ему был нужен. Этот код работает! Приходит другой девелопер, который не знает что было в голове у автора — и начинает менять под свои нужды. Где логика? Теория говорит нам что код нужно расширять и повторно использовать — но не менять! Если код работает — не трогай. Если нужно другое поведение — напиши новый код и покрой тестами.
Такой подход полностью снимает проблему «дорого поддерживать много Unit-тестов»! А проблема «с Unit-тестами писать код в 2 раза дольше» — придумана теми, кто не умеет в ООП. Да — им сложно потому что они не понимают как разбить код так что бы и он и тесты были простыми. Но нужны ли такие девелоперы, которые умеют быстро писать простыни спагетти-кода без тестов?

Правильна відповідь у 2020 році: писати. Не слухайте людей, які кажуть, що не потрібно. Вони або плутають MVP із прототипом і думають, що ви його викинете, або просто неправі.

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

Зависит от того, что ваша команда называет «Unit-тесты», и какие еще тесты пишутся.

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

Что имеет смысл писать всегда (ну кроме разработки простого скрипта одним человеком), это более высокоуровневые функциональные тесты (интеграционные, E2E) — они менее ломкие, имеют лучший ROI, и в целом помогают разрабатывать быстрее даже на этапе PoC. Если сделаны умеючи.

Намного лучше и подробнее описано здесь: tyrrrz.me/...​unit-testing-is-overrated

Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Коментар порушує правила спільноти і видалений модераторами.

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

Дальше нужно понимать что будет выполнять данное MVP.
Если внутри какая-то хитрая сложная логика — написания кода вместе с юнит тестами только поможет это все написать более правильно.

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

Юнит, интегрешин или e2e?
Мое мнение — для какого-то небольшого прототипа (именно блек-бокс который имеет какой-то протокол взаимодействия и выполняет пару функций) золотая середина — интеграционные.
Если внутри хитрая логика — дополнительно покрываем часть unit.

Если есть UI — на первых парах хватит и мануального тестирования — если полетит — потом это можно автоматизировать.

Но технарь-технарь конечно же всегда скажет что надо чтобы было не меньше 80% покрытия — потому что так правильно))

Первое что нужно понять что такое MVP

Это точно, поскольку кое-кто кажется не совсем понимает, что MVP это не

прототип

Писати. Бо не буде часу переписувати.

Писать или не писать, вот в чем вопрос.
Достойно ль смиряться с тестами в несчастном ЭМ-ВИ-ПЫ,
Иль надо оказать сопротивленье
И сделать быстрое и грязное решенье?

Так погибают замыслы с размахом
В начале обещавшие успех

Один великий человек сказал приблизительно такое:

музыка — это архитектура движущаяся в танце

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

Отдельно, хотелось бы отметить позицию ТС: набросила — и в кусты.

Шо там, Юлия?
дайте угадаю..
Клиент требует покрывать тестами (восемьсят порцэнтоф), а вы об этом не договаривались? Вы и без тестов еле успеваете? И бюждет тю-тю уже?

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

Та зачем нам ТС, и так идет неплохо :)

нет. Простейшие интеграционные на критическую функциональность — максимум

І юніт-тести на критичні компоненти інфраструктури. Якщо такі є.

Конечно писать! Можно заранить сонаркюб (или что там щас в моде вообще?) Сгенирировать красивых многостраничных графиков о том какое высокое покрытие, как много тестов, и вообще все круто. И идти с этим репортом к заказчегу, мол вон у нас как все по высшему разряду, высшего качества, паралельно подмигивая галерному менеджеру что самое время пересмотреть ЗП...

Це тобi не фрiланс)
де що заробив — те й з’їв

Конечно писать! Можно заранить сонаркюб (или что там щас в моде вообще?) Сгенирировать красивых многостраничных графиков о том какое высокое покрытие, как много тестов, и вообще все круто. И идти с этим репортом к заказчегу, мол вон у нас как все по высшему разряду, высшего качества

И автотестами г*внокод покрыть в три слоя!1 Чтобы не пах.
Для этого впихнуть в тощий бюджет 3-х автотестеров: сеньора, мидла и джуна. Подёрнувшийся жирком ленивый мидл будет одной рукой в игры тыцать на смарте, зато в целом какой фронт работ получится, только успевай таски открывать-закрывать

А работать кто будет, пока мид играется?

Не писать. Большая трата времени. Unit-тесты — просто один из инструментов улучшить качество кода, если в этом есть острая надобность. Они не идут бесплатно, это замедление скорости разработки где-то в 2 раза.

Для MVP важнее сделать побыстрее основную функциональность, чтобы понять нужен ли продукт в принципе...

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

Они не идут бесплатно, это замедление скорости разработки где-то в 2 раза.

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

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

Unit-тесты — просто один из инструментов улучшить качество кода

нет, быдлокод наличие тестов лучше не зделает.

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

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

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

Вообще не писать. Они не нужны.
Можно писать интеграционные и вам-функциональные. И да это надо доделать до реализации MVP.

Я однажды встретил жутко сырой MVP, который продали в виде продукта очень важному клиенту. Покрытия тестами не было, поэтому вполне здравое изменение в коде, одобренное на двух очень внимательных код ревью при участии CTO и Head Engineering и Head Product, привело к очень неприятному и напряженному разговору с этим клиентом. Был бы тест — стало бы понятно, почему реализовано было именно так изначально. Это беда роста стартапов, когда mvp вроде фурычит, баблище инвесторы вкинули, наняли спецов а те давай делать «как надо».

Если это core feature, нечто важное и часто вызывемое — то однозначно стоит. Если нет — то нет. Помните вы не код пишете, и не задачу решаете, вы делаете бизнес с помощью кода.

вы делаете бизнес с помощью кода

Еще можно бочку сделать www.google.com/...​search?q=do a barrel roll.

Приведу аналогию для тех, кто умеет работать паяльником. Когда вы собираете схему, то перед впаять каждый транзистор вы его «прозваниваете» прибором. Что бы проверить что он не бракованный. Вот это и есть «юнит-тест» — он делается как часть сборки, а может и до нее (TDD). Если что-то не работает — вы можете на ОТКЛЮЧЕННОЙ схеме опять проверить прибором элементы. Так сразу видно, например, что конденсатор пробился.
Когда вы собрали, ВКЛЮЧИЛИ питание и смотрите осциллографом — это уже совсем другое, интеграционное тестирование. Тут уже сложнее определить правильно работает или нет (нужны эталонные осциллограммы) и если не правильно — то сложно понять какой именно элемент дал сбой. Нужно проверять каждый (дебажить).

Можно аналогию проще,
Берём плату, и ведро компонентов,
Греем плату,
Высыпаем на неё ведро конденсаторов, резисторов, микросхем,
Плавим стакан олова,
Выливаем сверху.

Как-нибудь да припаяется, а в продакшене тестеров больше, там сообщат, если что не так прилипло.

Бобёр про радиолюбительские схемы, которые в конце 1980х-начале 1990х зачастую собирались «из чего Бог пошлёт». В тех условиях было важно проверять работоспособность деталей

Ну и это тоже, да.

а если у меня операционник в корпусе DIP-14 какие лапы звонить? а если я попутал компоненты местами?

а если у меня операционник в корпусе DIP-14 какие лапы звонить?

Вот не надо уже так по-тупому утрировать.
Плату с панелькой и 3-4 базовых схемы тупо накидкой — сейчас каждый может себе позволить.

а если я попутал компоненты местами?

Наверно, они подписаны? И можно прочитать?

Понятно, что аналогия с софтом не идеальная, но не до такой же степени.

Зависит от того, что ваша команда называет «Unit-тесты», и какие еще тесты пишутся.

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

Что имеет смысл писать всегда (ну кроме разработки простого скрипта одним человеком), это более высокоуровневые функциональные тесты (интеграционные, E2E) — они менее ломкие, имеют лучший ROI, и в целом помогают разрабатывать быстрее даже на этапе PoC. Если сделаны умеючи.

Намного лучше и подробнее описано здесь: tyrrrz.me/...​unit-testing-is-overrated

Автору +100500, уже не раз пропагандировал подобный подход в других темах.

Очень странное мнение! Я всегда считал наоборот:
Юнит-тесты это тесты минимальных блоков программ — классов или методов в изоляции. Они пишутся девелопером вместе с кодом, помогают интегрироваться с чужим кодом, экономят время на проверку и дебаг своего кода (не нужно запускать все). После написания они не требуют поддержки, редко меняются, быстро выполняются и могут даже запускаться в фоне при написании кода (live unit tests in VS):
docs.microsoft.com/...​unit-testing?view=vs-2019
Функциональные тесты (интеграционные, E2E) — тестируют бизнес логику, требуют сложных тестовых данных и сложных проверок результата. В коде они проходят по всей иерархии обработчиков. Писать такие тесты непросто: нужно четко понимать требования, нужно перед каждым прогоном готовить данные, при проверке нужно учитывать «внешние» факторы (например текущее время или внешние зависимости). Такие тесты обычны проходят долго и могут иногда падать случайным образом. При изменении в любых модулях интеграционные тесты обычно приходится менять — и не один а сразу много. Добавление новой функциональности зачастую так же требует править старые тесты.
Когда юнит-тесты пытаются писать для уже написанного кода с сильной связностью — то обычно получаются именно интеграционные тесты, которые иногда даже лезут в базу. Именно такой тип тестов больше вредят, чем приносят пользы! Многие девелоперы, которые ненавидят писать юнит-тесты именно «покрывали тестами», а не писали настоящие юнит-тесты. Очень часто после изменения пары строк в коде десятки таких тестов отваливаются по не очевидным причинам. В результате получаются эстимейты «без тестов — день, а с тестами — три». Логично что менеджеры стараются сократить эту бесполезную трату времени.
При этом автоматическое тестирование — это совсем другая история. Это black-box тесты должны писать и править совсем другие люди, а не девелоперы.

Очень странное мнение! Я всегда считал наоборот:

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

Юнит-тесты... После написания они не требуют поддержки, редко меняются, быстро выполняются и могут даже запускаться в фоне при написании кода (live unit tests in VS):

Не меняются? Код не рефакторится? Бизнес требования не меняются? В чем их польза тогда, кроме покрытия?

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

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

нужно перед каждым прогоном готовить данные,

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

при проверке нужно учитывать «внешние» факторы (например текущее время или внешние зависимости).

Время — это 101 курса авто-тестирования.

Внешние зависимости фиксируются как есть для изолированного прогона. Дополнительно contract testing на интеграционных средах если меняются очень непредсказуемо и катастрофично. Хотя в 80 % случаев не окупается, как я пока наблюдаю.

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

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

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

Добавление новой функциональности зачастую так же требует править старые тесты.

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

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

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

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

Может у вас какая-то инженерная культура особая, но в моем опыте вниканий в кодобазы новых (для меня) проектов, даже кашистые и тормознутые высокоуровневые тесты приносили больше пользы чем 99% быстрых и безболезненных юнит тестов.

При этом автоматическое тестирование — это совсем другая история. Это black-box тесты должны писать и править совсем другие люди, а не девелоперы.

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

Ниже уже пытался объяснить:
dou.ua/...​rums/topic/31443/#1933683
Попробую еще раз подробнее:

Бизнес требования не меняются?

Юнит тесты не зависят от требований. Юнит тест тестирует, например, разворот массива или проход по дереву. Чаще всего такой тест просто тестирует что один метод правильно вызывает 3 других метода.

Не меняются? Код не рефакторится?

Еще раз: в соответствии с Open-Closed принципом написанный и протестированный код НЕ меняется. Он Закрыт для изменений, но Открыт для расширения и повторного использования. Соответственно юнит-тесты то же не меняются: пишутся новые, а старые выбрасывают если они уже не нужны.

В чем их польза тогда, кроме покрытия?

Я считаю так: от них 80% пользы пока девелопер пишет код. Потому что они помогают девелоперу сделать свой код изолированным и продебажить без всего остального. Именно изоляция (она же — инкапсуляция) — главный способ борьбы со сложностью большого приложения. 20% пользы от юнит-тестов потом в том, что они не позволяют другим девелоперам случайно или намеренно поменять (читай — поломать) работающий код (опять вспоминаем что он Закрыт для изменений!).
Про интеграционные и другие тесты не буду повторяться. Это совсем другие тесты по сравнению юнит — тестами! Одни не заменяют другие. Юнит-тесты для девелоперов на этапе написания кода. Они априори white-box и не могут быть другими. Функциональные тесты наоборот — почти всегда black-box и именно поэтому хорошо, когда проверочные тесты пишет другая команда или хотя бы другой разработчик. Потому что их делают на основании требований, не зная кода и не подгоняя тесты под него.
Поэтому меня так напрягает когда пишут «юнит-тесты бесполезны, лучше используйте интеграционные». Это разные инструменты для разных целей! Это все равно что писать «маленький молоток бесполезный — он слабо бьет, хотите результата — всегда лупите кувалдой».

в соответствии с Open-Closed принципом написанный и протестированный код НЕ меняется.

И когда заказчик хочет новую фичу, ты его посылаешь на Open-Closed principle.
Пример из телефонии: Был себе звонок. У него был тот, кто звонит (source) и тот, кому звонят (destination). И были стадии жизни от initial до disconnecting. И он пару лет как довольно стабильно работал. И тут пришел ПО и говорит: на шнурковых АТС есть такая фича: когда у трубки звонок на удержании, если трубку кладут, этот звонок перезванивает на трубку. Потому что человек мог забыть, что там звонок на удержании. И потому что так легче на него переключиться, чем искать кнопку flash. Надо нам сделать такое же. Итого: звонок теперь может звонить на source, скакать назад по стадиям жизни, логировать перезвон не нужно, и прочие плюшки.
Можно послать ПО, сославшись на Open-Closed principle. Типа, мы не можем такое заимплементить. А можно — переписать готовый класс звонка нафиг, и оно через пару месяцев будет нормально работать. Но это нарушит Open-Closed principle.

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

Ну не для всего, а ниже описал условия dou.ua/...​ign=reply-comment#1933732
Просто напоминаю, что не везде эффективно пихать тесты.

На проектах, где только програмеров человек 20 он уже не работает.

Ты же сам знаешь, что эти 20 человек будут раза в 2 производительнее, чем 5 человек. И это — если повезет.

Я скорее вспоминаю Organizational Patterns of Agile Software Development.
С людьми как с ядрами процессора — если сможешь правильно разбить работу, и сделать всех независимыми — получится ускорение. Если будет много коммуникации — может стать медленнее, чем было.
А вот получается, что если все работают независимо, то есть быстро, то и на это можно попробовать натянуть опыт маленьких команд с редкими релизами и отсутствием тестов. Потому что тесты либо защищают от чужих людей, лезущих в код, либо нужны, когда бизнес не дает времени нормально пройти все фазы тестирования.

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

Вот книжка как раз об этом.

Еще раз, юнит-тесты, это не этап тестирования, это элемент разработки.

Тогда почему

На проектах, где только програмеров человек 20 он уже не работает.

если мы смогли

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

Вспоминаем другой принцип — Single Responsibility. Если им пользовались — то за стадии жизни жизни отвечает какой-нибудь один класс (State Machine). Нужен другой порядок — пишем другую реализацию + юнит тесты и подставляем в DI контейнер. Все старое остается работать как раньше и не ломается.
Все эти SOLID принципы, паттерны и «столпы» ООП придумали не для красоты. А во-первых что бы «съесть слона по частям» — т.е. разбить сложные задачи на маленькие и простые. Во-вторых что бы прятать эту сложность за интерфейсами. Т.е из 10 простых кусочков собираем 1 сложный. Прячем его сложность внутри — наружу торчит простой интерфейс, из 10 таких модулей опять собираем модуль посложнее... Это могут быть компоненты или модные микросервисы — суть в том что у человека ограниченный разум, который может удержать 10 элементов, но пасует если ему показать схему из сотен зависимостей.
Если все сделано правильно то каждый отдельный класс маленький и простой — на любом уровне! Он может, например, вызывать 10 микросервисов и собирать результат — но тест для него будет то же простой, потому что тестируется только код в нем, а не все те сервисы.
НО как только девелопер не умеет в ООП — у него получаются большие и сложные классы, для которых приходится писать большие тесты, которые по-любому приходится менять потому что не возможо взять «не все» из монолитного класса, потому что нету мест что бы «подсунуть» другую реализацию... И рефакторить потом это сложно и больно. И, конечно, юнит-тесты для монолита то же придется переписывать.
TDD подход придумали именно потому, что намного проще сразу делать маленькими кусочками, чем быстро налабать все в одном классе, потом руками проверить, а потом уже, в следующий раз пытаться как-то зарефакторить.

State Machine

также известный как Classes for States последние пару десятков лет относят к антипаттернам. как раз из-за того, что его пихают куда не нужно. Можно почитать конкретнее в Pattern Oriented Software Architecture, vol. 4.

А во-первых что бы «съесть слона по частям» — т.е. разбить сложные задачи на маленькие и простые.

Сложные задачи на то и сложные, что не бьются на простые.

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

Ну вот есть звонок. Над ним можно сделать пару десятков операций, выставить пару десятков полей данных, и вычитать десяток измерений состояния. Думаешь, это будет лучше выглядеть как call.GetSubset1().GetFieldA() чем call.GetA1()? И кода больше, и куча взаимосвязей между дробленными модулями через аксессоры, и энкапсуляции все равно никакой. Не все в мире иерархично.

Если все сделано правильно то каждый отдельный класс маленький и простой — на любом уровне!

Фиг. У меня в звонке сейчас больше 150 публичных методов. После выделения логирования, голоса и набора номера в отдельные классы. Дальше уже доменная область не декомпозится. При желании можешь потренироваться на котиках www.pjsip.org/...​oup__PJSUA__LIB__CALL.htm

Правильно, когда в классе не больше 7 методов (причем часть из них вспомогательные).

Сходи по линку www.pjsip.org/...​oup__PJSUA__LIB__CALL.htm и предложи, как разбить функциональность на группы с «не больше 7 методов (причем часть из них вспомогательные)»

Это не я, а сишная либа для телефонии, поэтому ид.
У меня уже после выделения сабклассов осталось 150 публичных методов.

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

Посмотри реальные библиотеки, даже стандартные библиотеки С++, Питона или Джавы. Посчитай публичные методы классов. 7 не будет. Будут десятки.

Возьми что-то более сложное www.javascripture.com/Document и посчитай публичные методы

На вход этому поступал текст, а на выходе был звук.

Вот я об этом. 2 метода (начать запись, остановить запись) и конструктор (с аргументами аудио устройства для ввода, файла для вывода, и параметров распознавания). Математика с кнопкой «сделать зашибись».

Сравни с чем-то реальным не из математики. Например, строка в Питоне docs.python.org/...​types.html#string-methods Похоже на 7 публичных методов? Больше не будешь пользоваться строками?

Посмотри реальные библиотеки, даже стандартные библиотеки С++, Питона или Джавы. Посчитай публичные методы классов. 7 не будет. Будут десятки.

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

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

Код приложения в идеале должен делать каждую вещь одним способом

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

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

Например, вот 20 строк геттеров:

	LongString			GetHs() const;
	const char*			GetLine() const {return logger_->prefix();}
	LongString			GetNumberForActive() const;
	ShortString			GetNumberForHistory() const;
	Endpoint			GetClipForParty(OUT ShortString& clip, const CallParty* const party) const;
	const char*			GetClipOfRole(const PartyRole role) const {return logger_->number(role).ToString();}
	Endpoint			GetCnipForParty(OUT LongString& cnip, const CallParty* const party) const;
	ActiveCallType		GetType() const {return type_;}
	ActiveCallStatus	GetStatus() const;
	time_t				GetStartTime() const {return start_time_;}
	time_t				GetConnectTime() const {return connect_time_;}
	uint16_t			GetDuration() const;
	uint8_t				GetHsId() const {return hsid_;}
	uint8_t				GetLineId() const {return lineid_;}
	unsigned			GetExitCode() const {return last_error_code_;}
	const LongString&	GetExitReason() const {return last_error_reason_;}
	CallHistoryEntry&	GetHistory() const;
	const BridgeSide&	SourceBridgeSide() const;
	bool				SourceLacksChannel() const {return voice().LacksChannelFor(SourceBridgeSide());}
В приложении телефонии все крутится вокруг звонков, которые связывают разные типы клиентов/сервисов, и всем должны обеспечить возможность просчитать бизнес логику и выполнить нужные действия. Также в звонке хранятся данные для истории, логирования и для отображения одним клиентом данных о другом.

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

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

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

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

А еще посчитай публичные методы тут en.cppreference.com/w/cpp/container/vector
Или тут docs.python.org/...​s.html#mapping-types-dict
Или здесь www.tensorflow.org/api_docs/python/tf/image
И они все дураки, потому что у тебя

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

И сколько публичных методов в векторе?

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

Ты ошибся с 7 методами, теперь пытаешься сменить тему

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

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

Кстати, у меня тоже телефония, и в центральном классе >200 методов. Это уже после многих лет, потраченных на вынос всего, что только можно, за его пределы.
Ну нельзя его нормально урезать ещё до пределов по Мартину или кто там ещё текущий верховный проорк.

Кстати, у меня тоже телефония, и в центральном классе >200 методов.

У вас соревнование какое-то, что ли?

У вас соревнование какое-то, что ли?

«Когда я окончил школу, я считал себя самым лучшим программистом в мире. Я мог написать непобедимую программу игры в крестики-нолики в трехмерном пространстве на пяти различных языках программирования, а также написать программу, состоящую из 1000 строк, которая бы работала. Затем я попал в реальный мир. Моей первой задачей было прочитать и понять фортрановскую программу емкостью 200000 строк, а затем увеличить скорость ее
работы в 2 раза.»

Нет, у нас всего лишь задачи с реальными сущностями сложнее «модуля подсчёта налога».
Хотя и аналоги таких попадаются, конечно.

Нет, у нас всего лишь задачи с реальными сущностями сложнее «модуля подсчёта налога».

Эти аргументы мы уже слышали в этом треде. Можно начинать составлять arrogant programmer bingo:

  1. вы теоретики нихрена не шарите как пишут код настоящие мужчины
  2. в нашей области это неприменимо
  3. вот у нас два девелопера в команде, которые уже работают шесть лет, на основе этого я делаю вывод, что все эти ваши юнит тесты, солид и прочее — херня.
  4. ты, эцсамое, не понимаешь!
  5. ну потомушо так.

Что-то поинтереснее будет? Например, «мы пробовали применить SOLID, сделать то-то и то-то, и у нас не получилось потому-то и потому-то». Аргумент «я пытался урезать god class на 200 методов и у меня не получилось» не канает.

Можно начинать составлять arrogant programmer bingo:

Так давно уже составлено. И состоит из пунктов:
1. TDD forevah!
2. Мартин велик, и GoF — пророки Его.
3. Ваши тесты — не настоящие юнит-тесты!
4. Ваша архитектура — не настоящая архитектура, если у вас есть хоть один метод толще 10 строчек!

Ну и так далее согласно этой дискуссии.

«Что-то поинтереснее будет?» ([некто Андрей Балагута])

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

«мы пробовали применить SOLID, сделать то-то и то-то, и у нас не получилось потому-то и потому-то».

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

в нашей области это неприменимо

Именно так.

Так давно уже составлено. И состоит из пунктов:
1. TDD forevah!
2. Ваши тесты — не настоящие юнит-тесты!
3. Ваша архитектура — не настоящая архитектура, если у вас есть хоть один метод толще 10 строчек!

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

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

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

Покажете знание хотя бы базового RFC3261 — с радостью! будет с кем поговорить о проблемах.

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

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

Да я и не претендую.

Если покажешь, где я такое говорил, пришлю тебе пирожок.

А я где-то сказал про тебя лично? Сказано же — arrogant programmer bingo, вот оно и получается.
Но да, сейчас открыто говорю: ты присоединился к этому же хору, без хотя бы попытки разобраться, насколько всё это применимо и в чём местная специфика. Почему-то:) все эти мартиноиды за пределы «internal» не лезут — наверно, понимают, что оно неприменимо.

Вот в чём, возможно, моя ошибка — что при том, что внутренний софт это >80% разработок мира, влез в посвящённую ему тему со своей спецификой. Но это уже потому, что Денис и Виктор тут же, а у них тоже не внутреннее.

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

Мой опыт можешь увидеть в моём профиле.

Увидел. Таки да, типовой комплект задач и инструментов «внутреннего» софта.

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

А кто сказал про «сложнее» разработки продукта? Опять домыслы?
Твой подопечный продукт может быть сколь угодно сложен в целом, но он явно не будет иметь какие-то естественно выделенные точки сверхконцентрации этой сложности — а если они и есть, они у вас уже вынесены куда-то во внешние компоненты. Как-нибудь посмотри, например, код используемой БД...:)

И не грузи меня самыми сложными словами из своего лексикона

Где самые сложные-то? Я самую базу вспомнил. «Самая сложность» начинается выше, но чтобы хотя бы вчерне объяснить проблемы, нужно, чтобы слушатель понимал домен.

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

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

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

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

Сказано же — arrogant programmer bingo, вот оно и получается.

Для меня arrogant programmer — это человек, который заведомо отметает идеи, даже не пытаясь применить их к своей области. Например, я вполне допускаю, что некоторые техники, упомянутые здесь, неприменимы во всех контекстах. Вы же с Денисом пока говорите только «это невозможно применить у нас» и всё, я не вижу попытки хотя бы _объяснить_, почему это невозможно применить. И мне на самом деле не так интересно, что вы делаете с вашим конкретным продуктом, мне интересно как раз понять границы применимости инструментов, но пока я только получаю «это невозможно».

он явно не будет иметь какие-то естественно выделенные точки сверхконцентрации этой сложности

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

Не знаю, за что ты Дениса решил походя тут пнуть (тебе ещё воздастся),

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

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

Согласен, извини, погорячился. Ты тоже высокомерно отнёсся ко мне и моему посту («некто Андрей Балагута», прозрачные намёки на то, что такие как я занимаются какой-то лёгкой хренью).

Для меня arrogant programmer — это человек, который заведомо отметает идеи, даже не пытаясь применить их к своей области.

Так а спросить вначале?
На самом деле у нас пару месяцев назад как раз закончился большой рефакторинг, где классические идеи были применены к основной части кода. Все эти low coupling + high cohesion, SRP, разбиение всего чего можно на мелкие понятные методы (местами, да, очень сильно помогло читаемости, а заодно нашли с десяток скрытых багов), и тому подобное.

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

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

Это состояние звонка, со всем, что к нему относится:
— участники (сущности класса SIP UA с нашей стороны, 2 в стабильном состоянии, от 1 до овердофига при дозвонах)
— действующие авторизации (накапливаются в процессе, ведутся своим менеджером, могут вызывать остановку звонка)
— действующие акаунтинги (аналогично, но кроме того надо релеить их модификации при смене применённого медиа)
— генераторы авторизаций и акаунтингов по приходящим событиям (первый старт, инициации трансферов, пикапов и т.п.)
— текущий активный раутинг (при дозвоне), дерево раутинга (отдельный модуль)
— очередь сообщений и её обработчики
— состояния медиасессий (через отдельный менеджер, но переход на него должен быть таки в звонке)
— процессор изменения (если активен; трансферы, паркинги и т.п.) — ссылка на отдельный объект и код для его нотификаций о критичных изменениях
— методы для применения процессорами изменений (перестановки UA, слияния/разделения звонков...)
— API хэндлеры (после того, как уже добрались до объекта конкретного звонка), на команды типа инициации трансфера или старта видео
— зачистной код (UAʼшки остановить, пнуть остановить реавторизации и отправить акаунтинги...)
— и прочая и прочая (много мелких сущностей).

Даже в этом неполном списке уже более 10 групп (а в каждой ещё может быть больше десятка полей или методов).

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

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

Ты тоже высокомерно отнёсся ко мне и моему посту («некто Андрей Балагута», прозрачные намёки на то, что такие как я занимаются какой-то лёгкой хренью).

OK.

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

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

У нас же получается специфика обратная — архитектура верхнего уровня очень простая (у неё есть проблемы, но это проблемы преодоления ограничений домена), >95% софта (по количеству, грубо говоря, классов и модулей) — тоже, зато есть одна ключевая точка, разрезание которой чудовищно дорого и неэффективно, если вообще возможно. И я даже соглашался, что это мы чего-то не умеем (но скоро таки одолеем), пока Денис не огорчил, что у них то же самое ;)
Видимо, придётся смириться.

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

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

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

Вы же с Денисом пока говорите только «это невозможно применить у нас» и всё, я не вижу попытки хотя бы _объяснить_, почему это невозможно применить.

Берем класс звонка. Выделяем из него подклассы: набор номера, голос, история, логирование. На каждый подкласс делаем 2 аксессора: константный и неконстрантный. Имеем 8 публичных методов в классе звонка. Добавляем конструктор и деструктор. Итого: 10 публичных методов в классе, который еще ничего не умеет. Упрощай до 7 публичных методов.

2. Мартин велик, и GoF — пророки Его.

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

Вот приписывания другим своих домыслов типа

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

они точно не делали, за что им спасибо. Если бы они ещё описали рамки применимости своих подходов — было бы совсем хорошо, но и так в общем неплохо...

Примени к интерфейсу управления железкой:

class Si32287Transport {
public:
	Si32287Transport() {}
	virtual ~Si32287Transport() {}

	virtual void Init() {}

	virtual void OnUsbRead(const void* buffer, size_t size) {ASSERT(false);}

	// Voice
	virtual void EnableExtVoice(const PortId 	port,
								const Channel 	channel,
								const bool 		enable,
								const bool 		dtmf_muted = false) = 0;
	virtual void EnableIntVoice(const bool enable) = 0;
	virtual void GetVoiceStats(const Channel channel, OUT api::VoiceStats& stats) {memset(&stats, 0, sizeof(stats));}
	virtual void GetRxDebugData(OUT api::VoiceRxDebug& data) {memset(&data, 0, sizeof(data));}
	virtual void StartCid(const PortId port, const uint8_t* const data, const uint8_t length) = 0;
	virtual void StopCid(const PortId port) = 0;
	virtual void SetMute(const PortId port, const uint8_t mode) = 0;

	// Control
	virtual uint8_t		ReadReg(const PortId port, const uint8_t address) = 0;
	virtual void		WriteReg(const PortId port, const uint8_t address, const uint8_t value) = 0;
	virtual void		ReadRegArray(	const PortId 		port,
										const uint8_t 		address,
										const uint8_t		datalen,
										OUT uint8_t* const 	data) = 0;
	virtual void		WriteRegArray(	const PortId 			port,
										const uint8_t 			address,
										const uint8_t			datalen,
										const uint8_t* const 	data) = 0;
	virtual void		ReadRegSerial(	const PortId 		port,
										const uint8_t 		address,
										const uint8_t		datalen,
										OUT uint8_t* const 	data) = 0;
	virtual void		WriteRegSerial(	const PortId 			port,
										const uint8_t 			address,
										const uint8_t			datalen,
										const uint8_t* const 	data) = 0;
	virtual void		RegSetMask(const PortId port, const uint8_t address, const uint8_t mask) = 0;
	virtual void		RegClearMask(const PortId port, const uint8_t address, const uint8_t mask) = 0;
	virtual uint32_t	ReadRam(const PortId port, const uint16_t address) = 0;
	virtual void		WriteRam(const PortId port, const uint16_t address, const uint32_t value) = 0;
	virtual void		ReadRamArray(	const PortId 		port,
										const uint16_t 		address,
										const uint8_t		datalen,
										OUT uint32_t* const data) = 0;
	virtual void		WriteRamArray(	const PortId 			port,
										const uint16_t 			address,
										const uint8_t			datalen,
										const uint32_t* const 	data) = 0;
	virtual void		ReadRamSerial(	const PortId 		port,
										const uint16_t 		address,
										const uint8_t		datalen,
										OUT uint32_t* const data) = 0;
	virtual void		WriteRamSerial(	const PortId 			port,
										const uint16_t 			address,
										const uint8_t			datalen,
										const uint32_t* const 	data) = 0;
	virtual void		RamSetMask(const PortId port, const uint16_t address, const uint32_t mask) = 0;
	virtual void		RamClearMask(const PortId port, const uint16_t address, const uint32_t mask) = 0;

	virtual void	BatchStart(const PortId port) {}
	virtual void	BatchCommit() {}

	// Misc
	virtual void EnableLed(const PortId port, const bool enable) {}
	virtual void EnableReset(const bool enable) = 0;

	// FW
	virtual uint16_t	GetFwVersion() = 0;
	virtual void		GetFwDate(OUT ShortString& date) = 0;
	virtual bool 		Reboot(const bool bootloader) = 0;
	virtual bool 		GetLastAssert(OUT ShortString& file, OUT uint16_t& line) {return false;}
	virtual bool		GetServiceTag(OUT HugeString& tag) {return false;}
	virtual bool		GetCalibration(OUT uint32_t* const data) {return false;}
	virtual bool		WriteCalibration(const uint32_t* const data) {return false;}

	virtual uint8_t		VoiceRxShift() const {return 0;}
	virtual uint8_t		VoiceTxShift() const {return 0;}
	virtual bool		HasExtVoice() const {return true;}
	virtual bool		NeedsPolling() const {return false;}

protected:
	static bool Rebase(const PortId port) {ASSERT(port.Valid()); return si32287::PortToChannel(port);}
};
Чтобы там осталось 7 методов, не упала скорость работы, и не увеличилась сложность кода.

Как минимум, здесь просится interface segregation примерно на следующие группы (заранее прошу прощения, если называю их не так, как принято в в телеком-домене):

  • Voice
  • Reg
  • RAM
  • Batching
  • Servicing
  • Calibration (как вариант, объединить с Servicing, если семантически это близкие вещи)
  • USBDevice

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

Все клиенты из одного модуля — по сути, класс выше — это транспортный уровень (RPC over USB). Его клиенты — это уровень управления железкой (можно сказать — юзер спейс драйвер).

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

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

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

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

Если так же можно и в C++, то — да, в итоге, получится один довольно большой класс с одной ответственностью — инкапсулировать детали RPC over USB интерфейса с устройством.

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

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

Можно. В результате получим несколько лишних заголовков с интерфейсами. Когда все использование транспорта — в рамках одного модуля. ИМО — ненужное разведение сущностей.

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

Вот если бы подобное торчало как публичный контракт, видимый за пределами модуля — «тады ой!»

Даже с публичным контрактом — можно обойтись разбивкой одного API на секции коментами вместо создания нескольких файлов. Система проще получается. Иначе — близится raviolli code, в котором вроде все красиво, но неясно, как это все собрать вместе, кто в данный момент имплементит какой интерфейс, и как он будет себя вести при запуске.
Возможно, граница — в coupling между модулями и уровнями системы. Для (микро)сервисов или публичных библиотек контракт должен быть жестким и стабильным, а для какой-то телефонии с кучей взаимосвязей — будет меняться даже при багфиксе.

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

Например, если у тебя клиент, реализующий логику калибровки девайса, работает только с теми RPC вызовами, которые к калибровке, собственно, и относятся — нет смысла тащить в этот клиент один огромный заголовочный файл, в котором вместе с API для калибровки притащится «a gorilla and a whole jungle»

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

Особенно когда интерфейс и клиентов пишет один человек.

И этот инвариант сохранится и в ближайшем будущем?

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

Другое дело, что «мир» (а-ля Спольски), в котором возможно долговременное существование таких команд — выглядит фантастикой для многих, воспитанных реалиями «кровавого аутсорса»

И этот инвариант сохранится и в ближайшем будущем?

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

Пример того, как можно делать «правильно» или «быстро»:
У нас больше 200 настроек. Почти все идут через конфиг. Добавление поддержки настройки в конфиге — 4 строчки кода. Начальство пару раз наезжало, что у нормальных людей настройки через CLI, и не нужно перегружать программу, чтобы применить изменения. Но там — огромное количество работы, в том числе — чтобы привести программу к самосогласованному состоянию после изменения настроек. Например, если меняется список поддерживаемых кодеков во время установки звонка. Поэтому KISS — никаких серьезных изменений в рантайме.

Поэтому KISS — никаких серьезных изменений в рантайме.

этот метод применяется только к реализации, никогда к требованиям

Если заказчик разумный и слышит фидбек, небольшие изменения требований могут сильно ускорить разработку — либо уменьшением количества special cases, либо выкидыванием сложно реализуемых требований (как в нашем случае — отказались реализовывать изменение большинства настроек в рантайме).
Если заказчик не слушает разработчиков — он платит объемом проекта и размером команды.

это все здорово, но кисс не применяется в работе с требованиями

почему?
80% работы можно сделать за 20% времени. Оставшиеся 20% можно попытаться отложить на потом, или не делать.
Если бюрократия такая бюрократия — тогда ничего не сделаешь, все будет медленно.
Если эджайл и рядом продакт овнер, и деньги считают — тогда требования можно изменить так, чтобы их было легко заимплементить.

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

а еще закон парето никак не связан с кисс

Сведи к 7 публичным методам.

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

Сконцентрируюсь на следующих методах, поскольку весь класс разбирать времени нет, и писать буду в псевдокоде, поскольку на C++ сто лет не писал:

	// Control
	virtual uint8_t		ReadReg(const PortId port, const uint8_t address) = 0;
	virtual void		WriteReg(const PortId port, const uint8_t address, const uint8_t value) = 0;
	virtual void		ReadRegArray(	const PortId 		port,
										const uint8_t 		address,
										const uint8_t		datalen,
										OUT uint8_t* const 	data) = 0;
	virtual void		WriteRegArray(	const PortId 			port,
										const uint8_t 			address,
										const uint8_t			datalen,
										const uint8_t* const 	data) = 0;
	virtual void		ReadRegSerial(	const PortId 		port,
										const uint8_t 		address,
										const uint8_t		datalen,
										OUT uint8_t* const 	data) = 0;
	virtual void		WriteRegSerial(	const PortId 			port,
										const uint8_t 			address,
										const uint8_t			datalen,
										const uint8_t* const 	data) = 0;
	virtual void		RegSetMask(const PortId port, const uint8_t address, const uint8_t mask) = 0;
	virtual void		RegClearMask(const PortId port, const uint8_t address, const uint8_t mask) = 0;
	virtual uint32_t	ReadRam(const PortId port, const uint16_t address) = 0;
	virtual void		WriteRam(const PortId port, const uint16_t address, const uint32_t value) = 0;
	virtual void		ReadRamArray(	const PortId 		port,
										const uint16_t 		address,
										const uint8_t		datalen,
										OUT uint32_t* const data) = 0;
	virtual void		WriteRamArray(	const PortId 			port,
										const uint16_t 			address,
										const uint8_t			datalen,
										const uint32_t* const 	data) = 0;
	virtual void		ReadRamSerial(	const PortId 		port,
										const uint16_t 		address,
										const uint8_t		datalen,
										OUT uint32_t* const data) = 0;
	virtual void		WriteRamSerial(	const PortId 			port,
										const uint16_t 			address,
										const uint8_t			datalen,
										const uint32_t* const 	data) = 0;
	virtual void		RamSetMask(const PortId port, const uint16_t address, const uint32_t mask) = 0;
	virtual void		RamClearMask(const PortId port, const uint16_t address, const uint32_t mask) = 0;

Итого, 16 методов. Прежде всего, я замечаю что все эти методы принимают в качестве первых параметров port и address. Предположу, что эти параметры можно вынести в конструктор класса. Затем, вижу две группы методов по 8 штук, только одна имеет Reg в себе, другая — Ram. Это наталкивает на мысль о том, что это две похожие абстракции, просто выполняющие свои функции по-разному. Учитывая read/write в именах, назовём абстракцию TransportAccessor. Затем, замечаем, что большая часть методов считывает или записывает данные, просто разного вида. Эти данные тоже можно энкапсулировать в абстракцию — назовём её, например, TransportData. Эта абстракция будет иметь интерфейсы для каждого типа данных: Word (для простого Write), Array, Serial. Это публичные интерфейсы. Внутренне мы можем достичь производительности путём экспозиции внутренних интерфейсов (Spi), которые позволят классам внутри одной линии абстракции обмениваться данными так, как им надо. К сожалению, не зная внутренней структуры, невозможно сказать, как именно наилучшим образом это реализовать.

Итого:

class TransportAccessor {
    TransportData read(type: enum)
    void write(data: TransportData);
    void setMask(mask: uint8_t)
    void clearMask()
}

interface TransportData {
}

class Word implements TransportData {
}

class Array implements TransportData {
}

class Serial implements TransportData {
}

class Si32287Transport {
public:
    Si32287Transport(port: PortId, address: uint8_t)

    // Control
    getReg(): TransportAccessor
    getRam(): TransportAccessor
}

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

А теперь код клиента этого безобразия — метод, который заставялет телефон, подключенный к плате, звонить:

void	ChanPhys::SetupRing() {
	BatchLock		lock(transport_, portid_);

	WriteRam(RTPER, Si3228x_Ring_Presets->rtper);
	WriteRam(RINGFR, Si3228x_Ring_Presets->freq);
	WriteRam(RINGAMP, Si3228x_Ring_Presets->amp);
	WriteRam(RINGPHAS, Si3228x_Ring_Presets->phas);
	WriteRam(RINGOF, Si3228x_Ring_Presets->offset);
	WriteRam(SLOPE_RING, Si3228x_Ring_Presets->slope_ring);
	WriteRam(IRING_LIM, min(Si3228x_Ring_Presets->iring_lim, (uint32_t)SI3228X_IRING_LIM_MAX));
	WriteRam(RTACTH, Si3228x_Ring_Presets->rtacth);
	WriteRam(RTDCTH, Si3228x_Ring_Presets->rtdcth);
	WriteRam(RTACDB, Si3228x_Ring_Presets->rtacdb);
	WriteRam(RTDCDB, Si3228x_Ring_Presets->rtdcdb);
	WriteRam(VOV_RING_BAT, Si3228x_Ring_Presets->vov_ring_bat);
	WriteRam(VOV_RING_GND, Si3228x_Ring_Presets->vov_ring_gnd);
	WriteRam(VBATR_EXPECT, min(Si3228x_Ring_Presets->vbatr_expect, Si3228x_General_Configuration->vbatr_expect));
	WriteRam(DCDC_VREF_MIN_RNG, Si3228x_Ring_Presets->vbat_track_min_rng);
	WriteReg(RINGCON, Si3228x_Ring_Presets->ringcon & ~0x18);
	WriteReg(USERSTAT, Si3228x_Ring_Presets->userstat);
	WriteRam(VCM_RING, Si3228x_Ring_Presets->vcm_ring);
	WriteRam(VCM_RING_FIXED, Si3228x_Ring_Presets->vcm_ring_fixed);
	WriteRam(DELTA_VCM, Si3228x_Ring_Presets->delta_vcm);

	// Automatically adjust DCDC_RNGTYPE
	WriteRam(DCDC_RNGTYPE, Si3228x_Ring_Presets->dcdc_rngtype);

	WriteRam(SMART_RING_COUNTER, 0);

	if (Si3228x_Ring_Presets->smart_ring_period != 0) {
		WriteRam(SMART_RING_PERIOD, Si3228x_Ring_Presets->smart_ring_period);
		WriteRam(SMART_RING_PHASE, Si3228x_Ring_Presets->smart_ring_phase);
	}
}

void 		WriteReg(const uint8_t regid, const uint8_t value) {transport_.WriteReg(portid_, regid, value);}

void 		WriteRam(const uint16_t addr, const uint32_t value) {transport_.WriteRam(portid_, addr, value);}

class BatchLock {
	BatchLock(Si32287Transport& transport, const PortId port)
		: tr_(&transport) {tr_->BatchStart(port);}
	~BatchLock() {if(tr_) tr_->BatchCommit();}
}
(Батч локально сохраняет изменения, а потом их отсылает одним пакетом через ЮСБ)
После введения дополнительных абстракций код клиента становится проще? В чем проще? Если он стал сложнее — то зачем вводить абстракции?

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

Я считаю, что 1 класс с 16 методами проще, чем 4 класса по 4 метода каждый. Особенно, когда эти 16 методов используются вперемешку.
И код для записи в регистр станет сложнее, больше и медленнее, чем сейчас.
И код пользователя транспорта станет сложнее.
Тогда какая польза от изменений?

мы находимся в каких-то разных системах координат.

This! Метафорически говоря, ты строишь складское помещение с удобными подъездами-разъездами, а он — бункер с дзотами и сухпайком тушонки на 2 недели боевых действий

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

«Оппоненты» могут быть сколь угодно умны, но в своей специфике.
А так — ищем. И ловим любую ценную идею, если она есть.

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

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

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

Учитывая read/write в именах, назовём абстракцию TransportAccessor.

Всё это очень напоминает какое-нибудь bus_write_word в BSD ядрах. Но там есть суровые причины для обобщения (драйвер может и не знать, каким образом замаплена I/O область у какого варианта устройства).

Но 16 методов обычно ещё не тот уровень, из-за которого так срочно разобрать цельный класс (и нарушать SRP, к слову).

со 170 методов

И что?
Для 170 надо разрезать (если получается), для 16 — как-то не очень.

Но у меня нет дизеля, есть только овес.

Нет, твоя аналогия некорректна. Моя была ближе.

И сколько публичных методов в векторе?

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

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

Если доменная область легко делится на аспекты.

А еще посчитай публичные методы тут en.cppreference.com/w/cpp/container/vector

Слушай, ну там же блин даже жирным выделено — 4 секции методов (считай, каждая секция это интерфейс, отвечающий за некий аспект класса), в каждом не более 10 методов.

Так и у меня секции выделены — но вы ведь о публичных методах в классе, а не о секциях?
Можешь еще разбить строку в Питоне на секции)

Бо «сучасні стандарти» та «best practices» пишуть теоретики, а код — практики.

Concatenates the elements of a specified array or the members of a collection, using the specified separator between each element or member.
Статичний тому, що в нього купа варіантів, де сепаратор не є строкою а, наприклад, є Char. А з’єднує він теж строки з будь-якої колекції. Себто, в результаті оцей статичний метод з однаковою назвою може приймать як аргументи що завгодно, саме тому його не можна було зробить методом масиву — бо там є варіанти без масивів docs.microsoft.com/...​ing.join?view=netcore-3.1
Та й зав’язувати строки й масиви недобре як раз з архітектурної точки зору — масиви не мають нічого знати про строки. І не мають вміти джойнитись в строку. Якщо треба строку — то хай строка і джойнить.

Буде швидше працювати з char

Це не буде ботлнеком, це призведе до виділення пам’яті та доступу в хіп. Виділення пам’яті може працювати з м’ютексом, що зазвичай призведе до бар’єра пам’яті. Доступ в хіп затягне в кеш процесора непотрібні тобі дані й викине з нього потрібні. В результаті просяде загальна швидкість виконання програми «на рівному місці» бо тобі ліньки зразу нормально написати швидкий код, коли це нічого не варте.

В дотнеті char аллокейтиться на стек, а стрінг в heap. Аллокейшн на стек означає менше роботи для GC, також JIT вміє оптимізувати платформ код при доступі до структур заалокейчених на стек.

Курочка по зернятку.

Further reading: prodotnetmemory.com

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

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

Охренеть, конечные автоматы смузихлебы уже записали в антипаттерны???

hillside.net/...​oceedings.ps/palfinger.ps 97 год. Уже начались проблемы. Код, который пишут на конечных автоматах в явной форме, вообще не читаемый. И вот интересно, ты пробовал писать конечные автоматы с несколькими (5-6) степенями свободы?
Народ придумал себе серебряную пулю, и за пару лет от нее начали огребать. Как и от синглтона.

Код, который пишут на конечных автоматах в явной форме, вообще не читаемый.

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

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

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

Как и от синглтона.

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

Юнит тесты не зависят от требований. Юнит тест тестирует, например, разворот массива или проход по дереву.

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

Чаще всего такой тест просто тестирует что один метод правильно вызывает 3 других метода.

Это типа по классике, такие методы моками обкладывать?

  fun aMethod(param:Int): Int { return if (param >= 0) { dependency1.callMethod1(param);          } else { dependency2.callMethod2(param);          } }

20% пользы от юнит-тестов потом в том, что они не позволяют другим девелоперам случайно или намеренно поменять (читай — поломать) работающий код (опять вспоминаем что он Закрыт для изменений!).

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

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

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

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

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

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

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

Я вообще лично не против юнит-тестов на любых уровнях. Правильно примененные могут хорошо работать в TDD. Но если нужно выбирать или/или, то интеграционные (юнит — то есть в изоляции от других систем) всегда лучшее начало.

Это типа по классике, такие методы моками обкладывать?

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

тут немного стало непонятно как и зачем ломать код «Закрытый для изменений».

Да потому что не получается объяснить большинству джунов, а иногда и мидлов как это нельзя менять метод! Вот он возвращает 1 или 2 а мне же теперь нужно добавить 3 ... я добавлю там ифчик и поправлю юнит-тесты. Как нельзя? А как по другому? Перезрузить, сделать полиморфизм, вынести в стратегию? Да еще новый тест написать? Ооо — это слишком сложно! У меня это займет в 3 раза дольше, а мне нужно перфомить...

White box or black box — не важно для результата.

Для результата (оттестировать) — это может и не важно. А вот процесс написания тестов от этого абсолютно другой!

Другой командой писать тесты противопоказано.

У некоторых ИТ компаний это прямо в тест плане записано: приемочные тесты пишет другая команда, которая имеет на вход только требования и готовый продукт. Один клиент даже нанимал в Украине 2 разных аутсорс компании: мы писали код, а другая ИТ компания из того же города — приемочные тесты. Надо объяснять почему?

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

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

Но если нужно выбирать или/или, то интеграционные (юнит — то есть в изоляции от других систем) всегда лучшее начало.

Я тут много раз расписывают одно и тоже что бы донести мысль что НЕТ ИЛИ-ИЛИ ! Это разные тесты для разных целей. Наличие одних не заменяет другие.
И если всякие разные типы тестов можно делать или не делать — все зависит от проекта, то юнит-тесты это просто правильный стиль написания кода. Их не надо «делать» отдельно! Писать с юнит-тестами или без это как писать с переносами сток в коде или без? Ну вот в C# можно все написать в одну строку — вообще без переносов. Но при этом хорошая практика делать не больше 80 символов в строке.

Вот он возвращает 1 или 2 а мне же теперь нужно добавить 3 ... я добавлю там ифчик и поправлю юнит-тесты.

А почему это он возвращает 1 или 2? Значит, у вас уже в коде сделан if по ситуации. Ну так чего вы возмущаетесь, что джун хочет это расширить? Сами себе подножку подставили.

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

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

Писать с юнит-тестами или без это как писать с переносами сток в коде или без? Ну вот в C# можно все написать в одну строку — вообще без переносов. Но при этом хорошая практика делать не больше 80 символов в строке.

И в результате 90% этих юнит-тестов это просто проверка на то, что у метода действительно два параметра, int и string. Вы так стремитесь потратить бюджет заказчика?

А почему это он возвращает 1 или 2? Значит, у вас уже в коде сделан if по ситуации.

Потому что очень многие методы включают if и делают то или это — тут нет проблем.

Ну так чего вы возмущаетесь, что джун хочет это расширить?

Не расширить — а изменить! Если он сделает новый метод и там if вызывать предыдущий для 1 и 2 или делать свое для 3 — это расширение. А вот если поменяет старый метод — нужно давать по рукам! Потому что изменится поведение уже работающего кода. И все, кто этот метод раньше использовал, могут внезапно получить новый ответ, которого они раньше не ожидали!
Сошлюсь тут на еще один принцип из SOLID — как раз самый сложный — Liskov substitution principle. Он о том, что любые изменения в коде не должны менять уже используемое поведение. Если у нас есть метод, который возвращает рыб, то он не должен начать возвращать дельфинов. Да — дельфин может делать то же, что и рыбы ... но не может «дышать водой». И вся логика, которая работала с рыбами просто «убьет» дельфинов.
LSP — еще одна причина использовать юнит-тесты. Потому что компилятор может проверить декларации, реализует ли класс интерфейс и т.д. Но он не проверяет поведение! Только юнит-тест может гарантировать что при изменениях в коде LSP не будет нарушен.

И в результате 90% этих юнит-тестов это просто проверка на то, что у метода действительно два параметра, int и string.

Да — поэтому я и пишу что юнит-тест это такой же инструмент разработчика, как, например, писать обработку и логирование ошибок в каждом методе. Девелоперу нужно проверить свой код (даже что бы проверить что параметры передаются) — он пишет простой тест. Он мог бы проверить и руками, без теста. Но запустить все приложение в дебаг зачастую дольше, чем написать короткий тест. Плюс тест останется в коде и будет полезен другим девелоперам. И никакого «потратить бюджет заказчика» тут нет. Для нормального заказчика вопрос «хотите вы с юнит-тестами или без? — с тестами дороже!» это хороший повод послать таких халтурщиков подальше.

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

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

Он о том, что любые изменения в коде не должны менять уже используемое поведение.

Ключевое слово — используемое, да.

Это даже лучше, чем «определённое контрактом», потому что до чёрта случаев, что кто-то на что-то заложился и вам это уже сложно изменить.
Хотя всё равно надо стараться, если это даёт существенное преимущество.

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

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

Хотя кто-то мог и заложиться на то, что это были только рыбы.

Мне больше нравится пример с кенгуру со Стингерами :)

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

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

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

90% методов типичного корпоративного кода это «принял X и Y, вызвал foo с bar(X,Y) и zoo с kar(X,Y)». Вы уверены, что на это надо рисовать юнит-тест?

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

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

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

Мнэээ... смотрю на текущий проект... 17 лет, в пике до 10 человек... ну не так уж и много мест, где изменив одно надо смотреть, не посыплется ли всё вокруг (и чтобы тесты этого не показали).

И откуда про «удерживать в голове»? Никто не держит, есть куча средств: в IDE можно сказать find usages, в консоли можно вызвать grep... неужели вам про них не известно? что-то сомневаюсь...

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

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

Я понимаю подход «насколько возможно, пишите так, чтобы потом надо было меньше менять», но 1) не надо его абсолютизировать, 2) повторюсь, это вопрос публичности контракта и ООП только придаёт ему свою специфическую форму.

Посмотрите сколько людей пишет «юнит-тесты не нужны».

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

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

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

А почему это он возвращает 1 или 2? Значит, у вас уже в коде сделан if по ситуации. Ну так чего вы возмущаетесь, что джун хочет это расширить?

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

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

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

Всегда? Везде?

Вот у меня этот класс используется в 5 местах, я их всех нахожу тривиальным поиском. Логика тоже тривиальна. Я не могу её изменить? Почему?
Прибегут 100500 голодных индусов крича «Шакал, сын шайтана, мне фичу запорол»?

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

У вас есть машина времени или хрустальный шар? Поделитесь.

А то я вижу, что обычно за год устаревает 10-15% всех предположений, из которых строились предыдущие ТЗ и расчёты функциональности. В некоторых отраслях — ещё быстрее.

Але для цього треба прогнати всі тести. А якщо не знаєш, де?
Ось наприклад: вважаємо, внутрішнє API головного компонента (яке реалізує SIP стек) використовується у тестах ще декількох компонентів. Два з таких я знайшов, а два дні тому вибухнули кронові (раз на добу вночі) тести logindexerʼа, про який навіть не гадав, що там теж є шматки з таким кодом.
Ну нічого, виправив, справи на 2 хвилини без перекуру.

Саме тому краще навіть банальний grep, але треба не забути його запустити :)

Але для цього треба прогнати всі тести.

Да, и в чём проблема? Одним из требований к юнит-тестам является их очень быстрое выполнение (порядок десятков секунд — максимум единиц минут)

порядок десятков секунд — максимум единиц минут

Единицы минут — уже до чёрта.

Я за это время скорее скину коммит в Jenkins и займусь чем-то другим.

Але яким чином баг в коді повинен виявитися в клієнті того коду при юніт тестуванні, якщо всі залежності по правилам юніт тестування мають мокатися, в цілях розділення тестів на дрібні частинки для в тому числі швидкого їх виконання?

Я щось не зрозумів, що саме неясно, і тому відповім максимально дескриптивно :) Це продовжуватиметься у наступному коменті, і дивитись краще обидва разом.

Ну ось приклад у класичному дусі:
якщо прийшла команда закипʼятити чайника — треба віддати команди налити води, запалити газ (на потрібній конфорці), поставити чайника на конфорку, зачекати кипіння.
Вам не треба реального чайника, плити і чекати 5 хвилин до закінчення тесту, ви мокаєте
1) функцію налити води (одразу повертає «налили 1л»)
2) функцію включення газу (одразу повертає «готово», перевіряєте вибір конфорки)
3) функцію чекання (мокається таймер)
І якщо треба перевірити реакцію на те, що щось зламалось (газу нема і т.п.) — мок відповідно адаптується.

(NB дехто наполягає, що мок це щось всередині, а ззовні це стаб чи щось таке... тут я всіх називаю однаково.)

Зрозуміло, що треба перевірити, що мок імітує саме потрібні заміни реальности — але, як і в усьому, треба думати:)

Такі моки реального світу для вас «залежності» чи ні?

Чи ви саме так і мали на увазі? Ну тоді питання незрозуміле... але все єдино хай буде такий опис — ще для когось.

PS: але це все не означає, що юніт-тести будуть виконуватись миттєво. Може, їх у проекті сотня тисяч :)

І перетестовувати треба лише змінений код.

А IDE (чи що там запускає тести) може зрозуміти, які частини треба тестувати? Те, що я бачив, втрачає розуміння навіть у простих випадках; особливо коли не всі залежності мокаються.

Які там у нього клієнти і де не має мати значення, допоки ділянка коду протестована на всі юз кейси.

В теорії — так. Практика ж показує, що доволі часто знаходиш випадки з залежністю на те, що не декларувалось. Якби транслятор перевіряв, що код використовує тільки те, що вписане в контракти... поки, наскільки я знаю, навіть Irdis/Coq/etc. дуже мало таке вміють.

Саме тому в сусідній дискусії про Open/Closed я підкреслив про «використану» поведінку.

Але важливіше те, що ви загубили контекст: я якраз казав про те, що завжди незмінний контракт це нереальна ситуація, і треба не апологетувати її, а бути готовим (морально і інструментально) до ситуацій, коли контракт змінюється і відповідно може бути треба змінити усі місця, де використовується функція або клас. Тут вам ніякі юніт-тести не допоможуть, бо їх теж треба міняти.

Вот у меня этот класс используется в 5 местах, я их всех нахожу тривиальным поиском. Логика тоже тривиальна. Я не могу её изменить? Почему?

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

У вас есть машина времени или хрустальный шар? Поделитесь.

Моё личное мнение на этот счёт совпадает с идеями TDD — не добавляй чего-то, если оно не нужно в данный момент. Переходя на сверхупрощения, для кейса, описанного выше, если метод должен возвращать «1», то просто верни «1» и не парься. Но если поступило требование, чтобы он возвращал «2», то задумайся, не появится ли в будущем требование вернуть «3» или «4» (самый простой случай — когда в код добавляется switch или if, уже можно подумать, какова вероятность расширения этого свича, и если она кажется вероятной, абстрагировать этот код так, чтобы в следующий раз этот switch не пришлось менять, а можно было только расширить путём добавления нового класса или ещё чего-то).

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

Вот именно Open/Closed на длительных сроках действует больше во вред, чем на пользу. Насколько это для вас «в статистически значимом масштабе» — не знаю, синхронический объём и диахронический строго ортогональны друг другу. Но я плохо представляю себе работу типа «вот сейчас привлекли 100500 гребцов, реализовали и забыли, развивать это никто не будет» — поэтому ценить Open/Closed как-то не получается.

не добавляй чего-то, если оно не нужно в данный момент

Именно! Поэтому придётся добавить позже — и, с хорошей вероятностью, именно в базу.

Вот именно Open/Closed на длительных сроках действует больше во вред, чем на пользу.

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

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

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

Когда я читал книгу GoF, у меня главный вопрос после чтения возник — почему именно такой набор был отобран — конкретные 25 (AFAIR) из нескольких сотен? Например, есть паттерн «объект как хранитель контекста». Его фиг где опишут, но он крайне важен в моей суботрасли (где в коде может коллбэк коллбэком погонять), в отличие от каких-нибудь мостов и адаптеров, и который таки надо было объяснять джунам, даже тем, которые показали вызубренные паттерны GoF.

Так и тут — вы как-то постулировали «базовые практики ООП» в виде набора SOLID и теперь не сходите с этого откровенно религиозного постулата. Что будет без O — аббревиатура испортится? Ну и хрен с ней. Почему держаться этой пятёрки, а не включить сюда, например, какой-то из принципов GRASP? Как по мне, Information Expert, HC/LC или Demeter law важнее Open/Closed в разы.

Кто их определил именно в этом комплекте и почему мы должны ему беспрекословно следовать?

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

Когда я читал книгу GoF, у меня главный вопрос после чтения возник — почему именно такой набор был отобран — конкретные 25 (AFAIR) из нескольких сотен?

В GoF-е 400 страниц. Предлагаешь сделать книгу на 4000 страниц, чтобы описать все эти сотни? :) Никто не говорит, что описанные там шаблоны — единственные шаблоны, которые нужно использовать (как ты сам пишешь, есть шаблоны из предметных областей, есть ещё интеграционные шаблоны, я уж не говорю о специфических блюпринтах внутри каждого вменяемого фреймворка). Просто авторы посчитали эти шаблоны _наиболее общими_.

Так и тут — вы как-то постулировали «базовые практики ООП» в виде набора SOLID и теперь не сходите с этого откровенно религиозного постулата.

Я не знаю, может я как-то криво объясняюсь, но я наверное уже пятый раз говорю о том, что применять инструменты надо там, где они подходят. Ничего с миром не случиться, если вы не будете применять Open/Closed, но если попробуйте — возможно (возможно!), ваш код будет более управляем. Если вы чувствуете, что код и так достаточно управляем для вашей ситуации — ради бога, пишите как нравится.

Почему держаться этой пятёрки, а не включить сюда, например, какой-то из принципов GRASP?

И правда, почему? Я вроде нигде и не говорил, что нужно придерживаться исключительно SOLID, и GRASP — тоже отличный источник идей по поддержанию кода в хорошем состоянии.

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

Ну когда эта мировая практика чуть более чем полностью относится к internal enterprise — ничего удивительного, что те, кто в этом не участвует (как я), смотрит со стороны с офигением.

Собственно как и начался этот субтред.

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

Ну почитайте POSA (которое Денис рекомендовал). Там 5 томов, однако. Но уже как-то ближе к практике, которую я вижу.

В GoF-е 400 страниц. Предлагаешь сделать книгу на 4000 страниц, чтобы описать все эти сотни?

POSA. И не только.

Просто авторы посчитали эти шаблоны _наиболее общими_.

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

что применять инструменты надо там, где они подходят.

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

GRASP — нужны. Даже «абстракции и спецификации» по Лисков конца 70-х — нужны. Из SOLID — O не нужно, I очень специфично (сами интерфейсы в этом виде это специфика поколения Java/C#, и новое развитие может их вообще убить), D контекстноспецифично, S вообще подлежит переформулировке. «Что осталось на трубе?» ©

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

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

и GRASP — тоже отличный источник идей по поддержанию кода в хорошем состоянии.

Вот в том и дело, что это SOLID «тоже». И не отличный, а хороший с уточнением.

Их определили эксперты в области software engineering, а следовать им беспрекословно не нужно, я уже не знаю, в какой раз я это повторяю.

Просто кто первый публикнулся, тот и стал известным. Через год вышла более серьезная книжка Pattern Oriented Software Architecture, но о ней почти не слышали. А так — паттернов уже тысячи, но оно не систематизировано.

Вот у меня этот класс используется в 5 местах, я их всех нахожу тривиальным поиском. Логика тоже тривиальна. Я не могу её изменить? Почему?

В таком тривиальном случае — можете, конечно. Но, Beaver Green правильно подметил — такие «тепличные» условия, как правило, встречаются только на небольших проектах. В более крупных — усилий на то, чтобы найти все места, откуда используется класс, и убедиться в том, что после изменения существующей реализации ничего не сломалось — уйдёт намного больше, чем если сразу следовать open-closed principle.

У вас есть машина времени или хрустальный шар? Поделитесь.

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

Например, в старом классе вынести логику, вычисляющую результат как 1 или 2 в отдельный виртуальный метод (но не менять саму эту логику!). А, в тех новых юз кейсах, где понадобилось возвращать ещё и 3 — сделать класс-наследник и переопределить этот метод.

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

Ну это если какой-то JavaScript, где нормальный поиск фиг найдёт все случаи monkeypatchingʼа объектов по живому в рантайме... может, даже согласен. Юзеры такого чуда должны страдать. Впрочем, они и так готовы каждый год переписывать с нуля на новый фреймворк, и у них эта проблема вообще минимизирована.

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

И вы всё равно не о том думаете. Менять код придётся по-любому, это к гадалке не ходи. Уже писал: за год внешние требования меняются на 10-15%, если не сильнее, и это будет отражаться на всём коде. Думать надо не о том, как предсказать на 100 лет вперёд все потребности — это вы даже в libc не сделаете — а о том, как при необходимости изменения сделать это максимально бескровно. Вот для этого остальные принципы и полезны.

Хотя, если вы имеете твёрдый план на ближайший год типа «мы введём ещё 20 животных NPC и 10 скинов на живых игроков» — да, под это можно и заложиться, и с вероятностью >90% у вас даже получится.

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

Расширяемой _куда_? Вы не сможете опять же дальше чем на один короткий срок (считаем, год) предугадать направление расширения.

А, в тех новых юз кейсах, где понадобилось возвращать ещё и 3 — сделать класс-наследник и переопределить этот метод.

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

OCP — самый малозначимый (и вредный при абсолютизации) принцип из этой пятёрки.

Юзеры такого чуда должны страдать.

Споко, юзеры такого чуда уже давно освоили TypeScript и не парятся :)

Расширяемой _куда_?

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

и вредный при абсолютизации

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

Сам писал и е2е и юнит тесты и интеграционные.

В моем опыте с точность до наоборот, интеграционные и особено гребыне селениум е2е тесты это всегда кошмар который постоянно падает, сложно настраивать и интегрировать в CI/CD, кучу тестовых данных, тестовых аккаунтов и прочей фигни которую надо долго готовить и поддерживать...Особенно если у вас клауд архитектура с кучей токенов, апишек, баз...
Все это выглядит как очень много документов сваленый в папку «перебарть» с шокирующим размером 69Гб. Сначала там один автоматизатор потом два, потом свой фреемверк для автоматизации, потом ахитектор для этого фреемверка(реальный кейс)...Галеры одобряют ;-)

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

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

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

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

А вот от этого аж бомбануло, сам то хоть раз рефачил код без юнит тестов? Я — да. Это адъ и Израиль.

Сам писал и е2е и юнит тесты и интеграционные.

В моем опыте с точность до наоборот, интеграционные и особено гребыне селениум е2е тесты это всегда кошмар

Так, прежде чем спорить, еще раз: что мы называем юнит тестами, а что е2е и что интеграционные?

Для меня все три выполняются в изоляции, то есть внешние зависимости замоканны (WireMock etc.), а приватные зависомости (база, мессаджинг) запущены в изолированном докер-контейнере или замещенны in-memory implementation. Разница в фокусе теста.

Потому как селениум с токенами, адом и гоморой — это похоже про Integrated Environments — за них я вовсе не агитирую, а даже наоборот.

А вот от этого аж бомбануло, сам то хоть раз рефачил код без юнит тестов? Я — да. Это адъ и Израиль.

Какие проблемы, если это не js или другой язык без строгой типизации?

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

Это, кажется, к Bot Bot вопрос, но я попробую ответить.

Проблем никаких и без строгой типизации пока объем изменений невелик. С возрастанием же скоупа все быстро усложняется, и желательно опираться на «safety net» авто-тестов покрывающих всю область изменения. И целиком, а не маленькими кусочками, которые при сложении вдруг начинают работать немного не так, как из отдельных «юнит-юнит»-тестов казалось.

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

зависимости замоканны (WireMock etc.), а приватные зависомости (база, мессаджинг) запущены в изолированном докер-контейнере или замещенны in-memory implementation

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

Сказал — так отрезал. Ну пойди, погугли что такое настоящие юнит тесты как в оригинале было, и что такое настоящее TDD.

Ну пойди, погугли что такое настоящие юнит тесты как в оригинале было

О! Ссылку на оригинал — в студию. Только не фантазии поздних перепевцов типа Бека или Мартина, а оригиналы из 1960-х. Интересно почитать, как это тогда формулировалось.

На, держи: www.youtube.com/watch?v=EZ05e7EMOLM . Аудио не очень, но в целом в тему.

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

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

В 90х началась ажиль-революция, с тех пор эти теории в принципе не состоятельны, но умами Бота-Бота и пр. продолжают властвовать.

Найдешь что полезное — пиши тут.

На, держи

То ли я что-то существенное пропустил, то ли он так ничего полезного в плане истории и не сказал. Что ATDD более ранняя методика, из которой возникли TDD и BDD обострением подхода в определённых направлениях — было известно и ранее. В остальном доклад полезен только с целью учить английский на слух — ибо час тупой мути.

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

Тогда было модно много разных теорий. Но я что-то не помню, чтобы такие теории господствовали.

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

 да, только не работает не фига как ты тут расписываешь. Сколько на митапах / конференциях / onboardings не спрашиваю кто следует TDD — 20 процентов максимум. Почему? Основой ответ «а я не знаю заранее куда, все поломается, а потом времени нет».

Открываешь унаследованный проект — 80% покрыто тестами тобой описанными, никому кроме писателя не нужными и не полезными. Начинаешь с выкашивания нах большинства тестов — особенно тех что на моках взращены, покрываешь нормальными блек-бокс тестами на нужных уровнях — и можно начинать делать TDD.

не спрашиваю кто следует TDD — 20 процентов максимум. Почему? Основой ответ «а я не знаю заранее куда, все поломается, а потом времени нет».

20%? Уже слишком много для метода, который без извращения основных принципов пригоден только для клеевого кода на выброс.

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

На существующем коде? Смешно.

На существующем коде? Смешно.

смысл смеха от меня ускользает тут. Работаешь только с green-field?

смысл смеха от меня ускользает тут.

TDD на уже существующем коде невозможен по определению. Возможны всякие еретические отклонения, но не канон.

Работаешь только с green-field?

Если я правильно понял смысл этого термина, то совсем наоборот.

TDD на уже существующем коде невозможен по определению.

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

Слегка сложнее с рефакторингом, где в идеале одни и те же тесты остаются зелеными до и после.

Если код не меняется, то естественно TDD тут нету по определению, так как нету и девелопмента.

не канон

канона тут вообще нет — каждый выдумывает сам себе по созвучности услышанных терминов.

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

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

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

Не надо лепить из юнит тестов святой грааль и сербрянную пулю

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

Открываешь унаследованный проект — 80% покрыто тестами тобой описанными, никому кроме писателя не нужными и не полезными. Начинаешь с выкашивания нах большинства тестов — особенно тех что на моках взращены, покрываешь нормальными блек-бокс тестами на нужных уровнях — и можно начинать делать TDD.

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

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

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

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

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

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

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

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

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

Да, не очень просто, особенно поначалу в кодобазе где «все замокать и забыть». Но в принципе с опытом — не смертельно, и в последние 5-10 лет появилось много инструментов и подходов, позволяющих построить относительно легко поддерживаемые реалистичные блек-бокс с базами и всем, гоняемыми на каждый коммит. Один докер чего стоит.

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

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

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

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

Полистал коменты внизу и решил добавить.

Когда не писать тесты:
1) Редкие внешние релизы (раз в пол-года) + ручное тестирование + ассерты + А/Б тестирование. В этом случае ассерты выполняют роль юнит и интеграционных тестов во время работы приложения (тестирование мануальщиком и А/Б). Ship with asserts enabled — offensive programming wiki.c2.com/?OffensiveProgramming За месяцы релизного цикла почти все баги найдут свой ассерт, повалят приложение, и будут пофикшены.
2) Strict module ownership + низкая текучка. В твой код никто чужой не лезет, и ничего там не меняет, не понимая, как оно должно работать.
3) Прототип не пойдет реальным пользователям.

Когда писать тесты:
1) Люди хотят TDD.
2) В коде есть алгоритмы. По моему небольшому опыту ассерты + мануалка их плохо покрывают.
3) Частые релизы — нет возможности полного цикла ручного тестирования на каждый релиз.

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

Ship with asserts enabled — offensive programming wiki.c2.com/?OffensiveProgramming

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

У нас летает. И в 90х летало — разве не видел, как на виндовс вылазили поп-апы об ассертах?

У самой винды не видел. Но у некоторых программ видел (редко) — обычно, было какое-то наколенное неработающее г.

А потом, зачем ассерты в релизе? Что с ними делать пользователю?

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

Знание строки кода, где программа вылетела — мало помогает, без воспроизведения действий с программой, приведших к этой ошибке.

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

Строка кода с текстом ассерта показывает, в чем именно проблема (что пошло не так). А «программа зависла» — вообще ни о чем. Такое можно годами вылавливать.

Тогда, накрайняк, логгинг. Но выдавать релиз, где сообщения плюются именами файлов с номерами строк — это как-то несолидно...

Когда багов у пользователей мало — то это не страшно, и помогает стабилизировать продукт. Ну 1% пострадает, зато 99% будет счастье. И оно не будет глючить почти никогда — прога или правильно работает, или уже упала и перезагрузилась.

Я считаю что Unit-тесты писать очень полезно — но нужно делать это правильно!
Во-первых тесты нужно писать вместе с кодом. Это не значит жестко TDD (оно на практике редко выходит), но обычно у меня получается так: сначала интерфейсы и пустая реализация, потом тесты для нее — потом уже имплементация.
Во-вторых Unit-тесты должны быть максимально простыми и краткими. Иначе они не просто не помогают — а вредят! Написать и потом поддерживать тест на сотни строк и десятки проверок — это та самая потеря перфоманса, из-за которой все время пытаются отказаться от тестов.
Правильные тесты оказывают как минимум 2 огромных пользы:
Во-первых что-бы тесты были маленькими и простыми они должны тестировать маленькие и простые куски кода. Это заставляет девелопера делать декомпозицию, выделять интерфейсы, уменьшать связность и рефакторить. Сложно писать тест, он получается большим? Значит разбиваем монолитный код и пишем 5 маленьких простых тестов. Не получается подсунуть моки? Значит нужно выносить зависимости и делать интерфейсы. Даже не думая о SOLID девелопер поневоле его применяет!
Во-вторых если интерфейсы это контракт декларации, то тесты — это фактически контракт поведения! Тест гарантирует что через месяц кто-то не «починит» и не начнет возвращать, например, 0 вместо NULL. Только автор метода знает как он должен работать — и вот это свое знание он покрывает тестами что бы потом другие, которые недопоняли — не поломали.
Если же пойти чуть дальше то можно вспомнить Open-Closed принцип. Суть которого в том, что однажды написанный код не должен меняться! Это очень логично: один девелопер написал и протестировал метод именно таким, как ему был нужен. Этот код работает! Приходит другой девелопер, который не знает что было в голове у автора — и начинает менять под свои нужды. Где логика? Теория говорит нам что код нужно расширять и повторно использовать — но не менять! Если код работает — не трогай. Если нужно другое поведение — напиши новый код и покрой тестами.
Такой подход полностью снимает проблему «дорого поддерживать много Unit-тестов»! А проблема «с Unit-тестами писать код в 2 раза дольше» — придумана теми, кто не умеет в ООП. Да — им сложно потому что они не понимают как разбить код так что бы и он и тесты были простыми. Но нужны ли такие девелоперы, которые умеют быстро писать простыни спагетти-кода без тестов?

Не получается подсунуть моки? Значит нужно выносить зависимости и делать интерфейсы.

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

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

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

да нет там никакой ситуации, он прямым текстом пишет, что юнит-тесты ничего ему не дают, кроме проблем, а интеграционные ему помогают

Одна из глупых вещей это 100% покрытие кода которое потом приводит к тому что кроют что надо и не надо, типа экшенов в котроллерах и в итоге ИБР. Нахера котроллеры вообще крыть не понимаю, там все в итоге больше похожее на интеграционные есты по итогу.

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

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

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

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

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

Не совсем. Сама идея правильного ООП в том, что бы разбиваем сложную бизнес-логику или алгоритм на простые, изолированные «кирпичики». И каждый тестируем отдельно. Например: один класс умеет только посчитать НДС, другой — посчитать скидку, третий — ковертировать валюту. В каждом по 10 строк кода и юнит-тесты то же по 10 строк. Дальше есть класс, в котором то же 10 строк кода и он просто вызывает 3 предыдущих в правильном порядке. И юнит-тест для него тестирует именно это: что 3 мока (!) вызовутся в правильном порядке. Как только мы напишем тест, который вызовет всю цепочку что бы посчитать цену — это будет уже не юнит-тест, а интеграционный!
В отличии от простых юнит-тестов интеграционный уже придется менять при изменении любого из входящих в реализацию классов. Это противоречит уже single-responsibility принципу — потому что он требует что бы у каждого куска кода была только единственная причина для изменения.
Соответственно такие неправильные юнит-тесты, которые на самом деле интеграционные, замедляют разработку потому что часто отваливаются после измениний.

Тест для бизнес-логики или алгоритма не будет маленьким и простым

Как раз правильность алгоритмов вообще никак не проверить, кроме юнит тестов. Пример: сортировка или поиск или regexp.

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

В идеальном мире. В реальном — кирпичики зависят от контрактов своих компонентов и даже соседей.

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

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

В каждом по 10 строк кода и юнит-тесты то же по 10 строк. Дальше есть класс, в котором то же 10 строк кода и он просто вызывает 3 предыдущих в правильном порядке. И юнит-тест для него тестирует именно это: что 3 мока (!) вызовутся в правильном порядке.

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

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

Так юнит-тесты и не для выявления багов бизнес-логики. Для тестирования продакшина есть всякие другие тесты — которые тестируют именно конечный результат. А юнит тест как раз тестирует это «взять А или Б» и ничего больше.
Похоже что все эти холивары именно из-за того, что девелоперы не понимают разницы между юнит-тестами и прочими тестами. Получается путаница: одни делают юнит-тесты как интеграционные и говорят что с тестами код писать в 2 раза дольше. А другие жалуются что юнит-тесты бесполезные, потому что не находят баги в функциональности.
Ну вот как объяснить что юнит-тест это не методика тестирования, а просто методика написания кода?! Как, например, проверять параметры на входе в функцию и кидать ArgumentException. Или как ставить ассерты. Или как ловить и логировать ошибки. Этого всего можно и не делать — но это не занимает много времени, а приносит реальную пользу. В первую очередь для самого девелопера, у которого код после первого запуска сразу не заработает и придется искать что не так.

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

Куча времени (и часто нервов) тратиться на эти тесты

Это первый признак того что Вы не понимаете как их писать правильно! Вот вы написали любой метод — его надо хоть раз вызвать что бы проверить. А если в нем есть ветвления — то несколько раз что бы проверить обе ветки. Без теста: запускаем все приложение в дебаг, смотрим что пришло в наш метод, проходит по нему дебагом и смотрим что он вернул. С тестом: пишем простой тест на 10 строк для вызова этого метода с нужными параметрами и проверки результата. Запускаем — тест проходит за секунды. Не работает — сразу дебажим тест. Бонус: видим какую часть кода этот тест покрыл, а что осталось непроверенным.
Особенно удобно если нужно писать код для новой фичи, для которой еще не готов UI. Тут уже из приложения не «достучишься» до своего нового кода — нужно что-то придумывать... А вдруг не готова база? Тогда вообще придется ждать. А тест позволяет проверить все в изоляции и не ждать других частей.

Как я понимаю, вы с Grez просто по-разному понимаете термин «бизнес-логика».

Например, если вот это:

если в нем есть ветвления — то несколько раз что бы проверить обе ветки.

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

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

Чтобы словить такого рода баг, нужна креативность багхантера и понимание им продукта и софта.
Тесты 1- не ловят такие баги и 2- не доказывают, что таких багов нет.
Это редкий случай, когда (юнит\авто)тесты реально находят появившийся баг.
Зато специалист ручками может наловить багов сколько надо и каких надо, да ещё впрок «насушит», чтоб когда команде удастся выбить у заказчика спринт на стабилизацию продукта для устранения технического долга, то пофиксить кучу «найденных багов», чему клиент будет рад, и его не будет давить жаба за такую роскошь как стабилизационный спринт.

и ещё, мне кажется, что обсуждение уходит вдаль от темы «Писать ли Unit-тесты до готовности MVP»

Ключевое слово — MVP. Никто не сомневается в необходимости и полезности юнит-тестов.

Но, в ключевом слове MVP первая буква «Minimal», что означает минимальные сроки\стоимость.
Поэтому всё, что не является не-обходимым надобно обойти и похерить, чтобы побыстрее-подешевле выкатить заказчику\инвесторам работающую идею, пока они не остыли (после удачного РОС) и не потратили свои денежки на другую-нть другую идею (MVP) чья лошадь оказалась шустрее.

Вот позвать бы сюда сейлзов\ манагеров, или кто там рулит продвижением РОС \ MVP, послушать их, а не разработчиков.

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

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

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

Тесты 1- не ловят такие баги

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

и ещё, мне кажется, что обсуждение уходит вдаль от темы «Писать ли Unit-тесты до готовности MVP»

Это доу, когда в последний раз обсуждение было по теме топика ? :-)

РОС \ MVP,

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

Хм, даже не задумывался что между ними есть разница

Ну я бы привел такой пример: мы делали VoIP getaway в роутере, который должен соединить трубки от радиотелефонов и ИП-телефонию.
MVP: приложение, на которое можно зарегать трубку, позвонить на сервер, и принять входящий звонок.
POC: сделать звонок как угодно через реальные библиотеки на роутере, чтобы посмотреть, сколько проца он сожрет (может не влезть).
Можно даже сказать, что у нас MVP был раньше POC, так как на компе запустили за 3 месяца, а в роутер заэмбедились — за 6. Но тут можно поспорить, считать ли прогу на компе MVP.

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

Разве не бывает что на бумег проект POC\MVP, но (галерные) менеджеры подходят как к полноценному продукту, с тестами итд. Ибо продать то что типа уже «высокого качества», легше чем просто слепленую на коленке подделку.

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

если говорить об mvp то «продают» скорее «техическую реализацию пруфа идеи позволяющую наочно оценить глубину самой идеи и получить первые цифры» само качество реализации действительно имеет не существенное значение кроме случаев когда сама реализация и есть core самой идеи без которой сама идея чисто технически бессмысленна ибо попросту не реализуема опять же ж в этом месте могут быть какие-то конкретные лабораторные наработки до промышленной реализации которым ещё далеко но которые таки делают poc и вот на промышленную уже реализацию и ищутся инвестиции и тут надо понимать что это два совершенно разные «инвестиционных подхода» среди которых скажем твиттер техническую реализацию которого спрашивают сегодня на «дизайн интервью» и термоядерный реактор обще теоретические пруфы которого существуют но на саму разработку и техническую реализацию которого уйдут всё ещё годы и миллиарды на r&d в то время как в первом случае ни об каких r&d и речи не идёт а «пивоты» они вообще не про это ))

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

в первом случае об качестве речи не идёт ну просто потому что там и так и сяк простите говно во втором случае об качестве пока речи не идёт просто потому что до промышленного концепта ещё годы

ЗЫ: в качестве годного примера можно предложить машину теслу которая в общем смысле на самом деле говно и не может конкурировать с реальными производителями но фишка быть первым и сорвать куш любой ценой ну а дальше пивот )) и вот тут вложения не в исследования но в акции по спекулятивному принципу а «качество» ну такое там либо шах либо ишак селяви ничего личного просто бизнес

но (галерные) менеджеры подходят как к полноценному продукту, с тестами итд.

потому что а) жопочасы б) обещания от которых на самом деле никто с галерных менеджеров даже приблизительно не знает сколько на самом деле стоит покрытие 80+ ну записали в контракт fixed price и ладно идея ровно та же ж либо шах либо ишак а так снова ничего личного просто бизнес селяви ))

на бумег проект POC\MVP, но (галерные) менеджеры подходят как к полноценному продукту, с тестами итд

Обычно, на тестах и прочих зрелых практиках разработки даже на этапе MVP настаивают не менеджеры, а тех. лиды и архитекторы. Потому, что именно они прекрасно знают на своей шкуре, что слепленный из говна и палок MVP в 99% процентах случаев им никто потом не даст переделать по-нормальному.

что слепленный из говна и палок MVP в 99% процентах случаев им никто потом не даст переделать по-нормальному.

та нормально всё будет )) после раунда си всё это г. прекрасно аутсорсится в какую-нибудь восточную европку ну вы понели ))

что слепленный из говна и палок MVP в 99% процентах случаев им никто потом не даст переделать по-нормальному.

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

ну дык чувак какие тесты если тесты это уже к реализации а не к самой идее ))

Ну вот как объяснить что юнит-тест это не методика тестирования, а просто методика написания кода?!

Вы из которого мира-то прилетели? (хотя бы по схеме Спольского, но лучше ещё чуть детальнее)

Тест для бизнес-логики

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

Ось видно людина жизнь пойняла, респект.

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

Вот кстати много раз пробовал такой ТДД подход, не получаеться ну никак :-)
Даже хардкорное ФП, с его 100% иммутабельностью и референшал трасперенси — не помагает...

В C# все просто и очевидно. Пишем новый класс — сначала продумываем интерфейс. Когда интерфейс есть — можно вызвать его из теста. Пока пишем тесты — продумываем что каждый метод должен принимать и возвращать. Тут же хороший девелопер задумается о граничных значениях и ошибочных данных и напишет пару тестов на ожидаемые исключения.
Интерфейс + юнит-тесты полностью описывают желаемое поведение. Остается только его заимплементить. В процессе инмлепентации скорее всего потребуются зависимости и в тест придется добавить моки.
Т.е. жесткое ТДД когда сначала готовы все тесты а потом весь код вряд ли получится. Скорее будет параллельный итеративный процесс. Написали простые тесты — начали писать код — поправили / добавили тесты — поправили код. Плюс тестов в том, что они позволяют быстро вызывать свой новый код после каждого изменения. Без запуска всего приложения.

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

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

Я считаю что Unit-тесты писать очень полезно — но нужно делать это правильно!

Бесспорно, но вопрос был MVP

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

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

Ну, как бы да. Но правильный код без ошибок имеет свойство быстро становится неправильным и с ошибками. Особенно когда в него со временем вносят изменения пара-тройка десятков человек разных уровней компетенции и понимания контекста.
У нас git blame в среднем будет выдавать порядка десяти имён на файл. Я сильно сомневаюсь, что без юнит-тестов все эти люди бы написали правильный код без багов. Я бы точно не смог, потому что я регулярно валю юнит-тесты в процессе внесения изменений, особенно в малознакомый модуль.

Да я ж постебался выше.

Пардон, не распарсил.

А были бы юнит-тесты на парсере...

Пиши интеграционные, юниты очень редко бывают нужны

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

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

Они говорят — что сломалось, но не где сломалось

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

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

юниты (в понимании большинства людей), просто тесты ради тестов

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

тогда у тебя в «23 летние сеньоры» может попасть много «35 летних архитекторов» тоже.
большинство пишут тесты для покрытия.

как и почти никто не понимает что такое аджайл, и морально не в состоянии его воспринять, только делать иммитацию

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

так как бы в этом и вся суть тестов.

Это очень узкое понимание целей автоматического тестирования. Помимо «говорить что сломалось», тесты ещё служат документацией, например. Тут подробнее — xunitpatterns.com/...​s of Test Automation.html.

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

юниты очень редко бывают нужны

даже не буду переубеждать — пока сам не хлебнешь, не узнаешь.

даже не буду переубеждать — пока сам не хлебнешь, не узнаешь.

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

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

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

Можно иметь 6 лет опыта, а можно иметь шесть раз по 1 году.

За 9 лет работы на фуллтайм в офисе (за фриланс я вообще молчу) у меня был только раз когда после 6 месяцев я начал их повторять. Через 3 месяца меня там не было. Так что опыта у меня хватает

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

Наоборот — хрупкие! Поменял 1 метод — нужно поменять несколько юнит-тестов. И все интеграционные тесты, которые по нему проходят!
Ну вот любая банальная штука: например раньше даты возвращались из базы в таймзоне сервера, а потом починили на UTC. Изволь править все интеграционные тесты, потому что они все тянут данные из базы.
Это не значит что интеграционные и функциональные тесты не нужны — они как раз полезны именно тем, что

потому если такой тест упал — у тебя правда что-то не работает

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

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

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

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

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

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

Правильна відповідь у 2020 році: писати. Не слухайте людей, які кажуть, що не потрібно. Вони або плутають MVP із прототипом і думають, що ви його викинете, або просто неправі.

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

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

Нет, вот википедия: en.wikipedia.org/...​ki/Minimum_viable_product
„A minimum viable product (MVP) is a version of a product with just enough features to satisfy early customers and provide feedback for future product development.
Gathering insights from an MVP is often less expensive than developing a product with more features, which increases costs and risk if the product fails, for example, due to incorrect assumptions.”

just enough features to satisfy early customers

Сам же цитируешь. И MVP, и POC имеют одну цель — ускорить доставку продукта и получить фидбек. MVP достигает этого путём урезания фунцкионала до минимума (minimum viable), но при этом не должно накапливаться технического долга, потому что MVP — это реальный продукт, просто маленький. POC же в свою очередь достигает цели путём закрывания глаз на технический долг, поэтому правило хорошего тона — выкидывать POC как только получен результат (например, получили фидбек от кастомеров или результаты A/B-тестирования).

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

POC тестирует один аспект (например, производительность) а MVP — показывает все аспекты.

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

На каждой из этих стадий возможно увидеть, что гипотеза ложная (например, что идея технически нереализуема может показать РОС). Но то, что гипотеза правдивая и проект может приносить деньги можно провалидировать только на стадии MVP.

Первая ссылка гугла: appinventiv.com/...​totype-the-best-strategy

возможно ли это реализовать с технической точки зрения.

Причём тут вообще техническая возможность? POC вообще может использовать жёсткие техники наподобие painted door test. POC должен проверить feasibility _идеи_.

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

Як можна провалідувати гіпотезу бізнесу із забагованим продуктом? MVP має бути простим, але якісним.

Можливо клієнти не будуть користуватися програмою не тому, що ідея погана, а тому що для свого MVP ви завжди використовували лише рядки із малими латинськими літерами, а користувачі почали писати капсом, використовувати пробіли, підкреслення і дефіс, або і взагалі порожні рядки?

Нет, многолетний продукт на то и многолетний, что он уже даёт деньги и гипотеза, очевидно, верна, раз он уже прожил какое-то кол-во лет.

Это типичное заблуждение. Провалидировать гипотезу можно не то что с забагованым продуктом, в некоторых случаях можно вообще не создавать продукт, обойтись лендингом, который покажет насколько интересна идея целевой аудитории, а забагованый продукт уж тем более справиться с этой целью. Писать об этом можно очень много, но я здесь не являюсь первоисточником, потому отсылаю к книге Эрика Риса The Lean Startup.

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

Я поки не зустрічав жодного успішного MVP, яке б викинули :) Завжди на його основі розбудовують більший продукт, а тому займатися технічним боргом старого коду, коли терміново потрібно додавати новий, як на мене, не досить цікаво і розумно. Та ще й спрацює ефект розбитих вікон: якщо не дбали про якість MVP, то не будуть дбати і про якісь готового продукту.

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

А я ніколи не працював у аутсорсі, лише у дродуктових компанія :) Тому тут у кожного свій досвід, який може бути різним.

Для MVP ще дуже важливий час розробки. День розробки MVP для бізнесу може бути більш значимим, ніж тиждень роботи над продуктом, коли вже виділений бюджет та планується інформаційна компанія.

Абсолютно згоден. Але ключове слово — це «може». Автор поста питає, чи варто писати тести, значить час для написання знайти можна, інакше б і не питали :) А якщо час є, то я обома руками за юніт тести.

Я поки не зустрічав жодного успішного MVP, яке б викинули :) Завжди на його основі розбудовують більший продукт, а тому займатися технічним боргом старого коду, коли терміново потрібно додавати новий, як на мене, не досить цікаво і розумно. Та ще й спрацює ефект розбитих вікон: якщо не дбали про якість MVP, то не будуть дбати і про якісь готового продукту.

Смелое заявление учитывая что ~99.9% стартапов проваливаются.

Працював лише у одному стартапі (та й то успішному) в далекому 2011, поки шановні інвестори із Донецька не відібрали компанію у власника та ще й із моїм персональним ноутбуком :)
Мій досвід базується на великих продуктових компаніях, та й писав я про

успішного MVP, яке б викинули

Тому про провали мова не йде :)

Тести потрібні для того, щоб легше підтримувати та розвивати ваш продукт.
Спочатку ви фігачете швидко і воно працює. За тиждень розробляєте половину продукту. А пізніше ваш темп падає. Потрібно писати нові костилі, підпирати існуюючий код, баги лізуть зі всіх дір і час на розробку росте. І у вас вже не стильний та модний молодіжний стартап, а legacy яке було би добре переписати
Тести, можуть вам зменшити подібну біль

Ответ есть в книге Эрика Риса и он однозначен. Не думал, что тут так много людей с обратной точкой зрения)

Я книгу не читал. А какая у него точка зрения по этому вопросу?

Я отношусь к книгам как в личному опыту автора и не воспринимаю как догму — у всех разная точка зрения, он просто книгу написал :)

Опять путаете Unit-тесты с функциональными! Юнит тест по-определению не тестирует никакие требования. Он просто проверяет что метод возвращает именно то, что разработчик задумал. Юнит тест должен быть простой «как двери». И менять юнит-тесты не надо от слова совсем.

Может ли продукт быть minimal viable без юнит-тестов? Если да, то не писать.

Можно ли написать MVP не принимая душ?

Только зря время потратите и я сейчас объясню почему. Абсолютно все те два с половиной модуля которые я покрыл юнит тестами работают как часы — написал и забыл. Ретроспективно посмотреть, так мог бы и не писать: оно ж все равно работает! С другой стороны у меня есть колега, так он не то шо юнит тесты не пишет: он тестирует по большим праздникам: на Пасху и Рождество. Так вот ему юнит тесты писать некогда: то хотфикс на продакшн выкатывать, то багу инвестигейтать. Если бы он еще и юнит тесты писал, то вообще ничего не успевал

Ретроспективно посмотреть, так мог бы и не писать: оно ж все равно работает!

«а че б*я если нет?» ©

Если бы он еще и юнит тесты писал, то вообще ничего не успевал

Нахрена так жить?

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

Писать ли Unit-тесты до готовности MVP (минимально жизнеспособного продукта)?

А какие аргуметы могут быть против, интересно?

Команда, в которой практически нет опыта написания модульных тестов

Если они не умеют писать тесты, умеют ли они писать продукционный код?

Мы 6 лет (4 в продакшне) без юнит тестов. Эмбедед/телеком.

Размер продукта, частота изменений, какого рода изменения вносятся (багфикс, полностью новые модули, постоянное изменение старых), количество людей, текучка?

2 dev + qa + po
размер, думаю, 150 KLoC
изменения очень жесткие — уже в продакшне ввели поддержку другого типа телефонии и железа, сейчас доделываем поддержку нескольких устройств. Постоянно добавлялись фичи и менялась логика в течении 4 лет.
текучка нулевая.

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

Опять же, 4 года в продакшне особо не говорит о том, умеют ли люди писать хороший код. Нужны метрики, например, количество закрываемых сторей к количеству появляющихся багов, средняя сложность сторей, время внесения изменений и легкость их внесения, time to fix, etc.

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

Нет метрик

О чём тогда разговор. Как ты оцениваешь качество своего кода? Так невозможно оценить ничего.

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

Я уже написал. Прежде всего, есть метрики качества самого кода (типа тех, которые показывает Sonar). Они конечно же не идеальны, но они могут дать общую статистическую картину качества кодовой базы. Наблюдение — почему-то чем меньше тестов в кодовой базе, тем хуже её общее состояние. Далее, maintenance. Можно оценивать количество появляющихся дефектов к количеству заделиверенных ченжей. Правда, без определённой каденции (например, как спринты в скраме) это довольно сложно оценить. Ещё — time to fix, или сколько времени проходит от обнаружения дефекта до его закрытия? Ещё — time to deliver, или сколько времени проходит от появления требования до его реализации в продукции. Думаю, можно найти ещё. Но в твоём сетапе это не выглядит очень нужным, учитывая что у вас два дева, из которых один уровня тех лид.

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

Цена зависит от скорости разработки и количества людей. Module ownership на это влияет в разы, наличие юнит тестов — тормозит разработку и серьезные рефакторинги.

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

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

Module ownership на это влияет в разы, наличие юнит тестов — тормозит разработку и серьезные рефакторинги.

Не доказано. Я могу привести обратные аргументы. Большой рефакторинг кода всегда можно разбить на серию мелких операций рефакторинга (см. «Refactoring» by Fowler). Каждая такая операция достаточно мала, чтобы не требовать полной переработки юнит тестов. Если юнит тесты написаны хорошо (т.е. если к ним применялись такие же высокие стандарты качества, что и к продукционному коду), то такие тесты будут гибкими и не будут сильно мешать рефакторингу. В то же время делая рефакторинг очень легко упустить мелкие детали и в итоге получить кучу багов. В этом случае юнит- и другие тесты служат в качестве safety net. Исходя из всего этого, я могу сделать вывод, что юнит тесты _ускоряют_ рефакторинг, а не тормозят.

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

Количество фич как минимум частично зависит от качества кода, от его гибкости, связности и прочих презираемых вами показателей :) Количество появляющихся дефектов в большой мере зависит от двух вещей — 1) от качества тестового покрытия (regression bugs), 2) от правильности понимания требований (грубо говоря, правильно ли вы поняли требования и правильно ли вы их реализовали). Регрессионные баги _напрямую_ зависят от автоматических тестов в целом, и юнит тестов в частности. Баги недопонимания требований зачастую также уменьшаются с улучшением тестовых практик. Например, если вы пишете тесты в технике TDD, вы сильнее задумываетесь о краевых условиях, которые зачастую ускользают из внимания.

Итого, я написал два больших абзаца обоснований, основа которых — признанные авторитеты в мире software engineering (такие как Robert Martin, Martin Fowler, Steve Freeman, Gerard Meszaros). В твоём посте я вижу только личное, недостаточно обоснованное мнение и презрение к сабжу :)

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

Смотрите, есть программа, связывающая клиентские учетки ИП-телефонии (SIP client) и базу радиотелефонов (DECT base station) с доступом через USB. Нужно сделать возможность поддержки шнурковых телефонов (FXS) вместо радиотелефонов. Протоколы разные. Железо разное. Прошивку для USB-FXS устройства надо самим писать.
Как Вы предлагаете разбивать такой рефакторинг на мелкие операции, когда затрагивается треть кода, и появляется иерархия классов там, где ее не было?
Юнит тесты не протухнут при разбивке классов в иерархию?

В то же время делая рефакторинг очень легко упустить мелкие детали и в итоге получить кучу багов. В этом случае юнит- и другие тесты служат в качестве safety net.

Не страшно при редких релизах, когда есть время оттестировать.

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

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

По продуктивности команд можете почитать Organizational Patterns of Agile Software Development с реальными исследованиями. Возможно, расширите кругозор.

появляется иерархия классов там, где ее не было?

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

Не страшно при редких релизах, когда есть время оттестировать.

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

Скорее, от архитектуры, которую мерять не научились.

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

По продуктивности команд можете почитать Organizational Patterns of Agile Software Development с реальными исследованиями. Возможно, расширите кругозор.

Спасибо, посмотрю. В свою очередь тоже рекомендую хотя бы ознакомиться с основыми трудами на тему юнит тестирования: «xUnit Patterns», «Growing Software Guided by Tests». Даже если в вашем текущем проекте это не находит применения, возможно, будет полезно в будущем или поможет иначе взглянуть на вещи.

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

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

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

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

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

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

Тут отметает кто-то другой

Если они не умеют писать тесты, умеют ли они писать продукционный код?

dou.ua/...​ign=reply-comment#1933189

Тут отметает кто-то другой

Какой конкретно инструмент я там отметаю? :) Это моё глубокое убеждение, что умение писать тесты делает программиста лучше, и лично не раз видел корреляцию между умением писать чистые читаемые тесты и умением писать чистый, гибкий код. Наверное, причина в том, что человек не может быть аккуратен в чём-то одном и неряшлив в другом, человек «равномерно» аккуратен, и если тесты неаккуратны, то и продукционный код имеет схожее качество. Это выполняется не всегда, но я видел это в моей практике довольно часто. Можно считать личным мнением.

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

«продукционный код» as opposed to «тестовый код». Сорри, последние три предложения — я не могу понять, как они относятся к тому, что я сказал.

Если они не умеют писать тесты, умеют ли они писать продукционный код?

Мы не писали тесты, написали продукционный код, и за 4 года в продакшне меняли функциональность до неузнаваемости. Без покрытия юнит- и интеграционными тестами.

наличие юнит тестов — тормозит разработку и серьезные рефакторинги

Вменяемые тесты не мешают рефакторнгу, наоборот. Тесты, которые ломаются при рефакторинге — первые кандидаты на выход.

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

Я топлю за тесты, которые не мешают рефакторингу 😊

У нас полный тест перед любым внешним релизом. Поэтому можно без юнит.

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

Уже поздно — проект почти завершен. Теперь мне пора призадуматься о новой работе.

Если при рефакторинге цепляет control flow, или каждый класс превращается в 3 (в моем случае — из классов, работавших с DECT, выделился общий для телефонии Phone с кучей шаблонных методов, в которых блоки имплементились производными классами DECT и FXS). Интеграционные тесты тут еще могли бы выжить, но пришлось бы еще столько же новых понаписывать. Юнит — вряд ли.

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

Насколько я понимаю:
Интеграционные тестируют интерфейс (функциональность) собранного модуля или всей программы. Например, делаем 2 звонка, потом call transfer, и проверяем, что остался один звонок, и в нем связаны нужные участники.
Юнит тесты — это когда мы с одной стороны дернули метод «входящий звонок» а с другой — словили сообщение «сделать звонок» с нужными параметрами. Тестируется один маленький кусочек интерфейса конкретного модуля.

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

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

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

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

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

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

Рефакторинг — изменение организации кода без изменения поведения системы. Если при этом падают тесты, то что-то с подходом к тестам не то.

Как Вы называете изменение организации кода с изменением поведения системы? В моем опыте это намного чаще происходит.

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

Тогда для чего тесты?

Чтобы видеть, какое поведение неожиданно зацепила новая фича. То же, для чего и ассерты. Поэтому у нас ассерты + тестировщик заменили юнит тесты.

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

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

Интеграционные тесты тут еще могли бы выжить, но пришлось бы еще столько же новых понаписывать. Юнит — вряд ли.

Я, честно говоря, слабо улавливаю важность того, чтобы тесты «выжили». Вы ж не стремитесь к тому, чтобы каждая строчка продукционного кода выжила. Наоборот, рефакторинг преподносится как что-то, что мы делаем постоянно. Так почему тестовый код должен быть исключением? Он тоже должен рефакториться. Но чтобы с ним было удобно работать и его рефакторить, ему нужно уделять достаточное внимание, а не писать просто на отвали.

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

Только некоторые не считают это рефакторингом.

И совершенно справедливо не считают.

Мне как раз помогал другой подход — не трогать код, пока это не нужно.

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

Тогда почему не объединить работу и рефакторинг? Это займет меньше времени.

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

Просто этот подход более структурированный, и он неплохо работает в проектах с хорошим покрытием тестами и для людей, которые привыкли подходить к работе более структурно. Отделяем мух от котлет.

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

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

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

Если делать фичу сразу, то посещается 1-2 раза:
Нулевой проход — оценка общей архитектуры и как делать фичу.
Первый проход — изменение кода, влепливая фичу и меняя структуру одновременно.

Мы 6 лет (4 в продакшне) без юнит тестов. Эмбедед/телеком.

Работал я недавно плотно с эмбедерами. Нужно было написать код по адской спецификации, с нашей стороны C#-клиент, с эмбедовой код девайса на C. Контора купается в деньгах и швыряется ими, мировой лидер в своей области (определённых видов сенсоров).

У нас плотное покрытие спеки тестами (для простоты понимания другими, на совещаниях называл их «юнит-тесты», но в реальности интеграционные). Эмбеды работали без тестов.

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

1) Возможно, клиент проще девайса
2) На С тяжело написать внятный код
3) В эмбедеде народ лучше паяет, чем архитектит, обычно
4) Фиг проюниттестишь, когда код завязан на железо
5) Само железо выбрасывает такие штуки, что можно неделю биться лбом об стенку

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

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

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

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

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

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

Часто в железке нет места на тесты. RAM и ROM очень мало.

А вот на абстракцию от ОС — есть. Она легкая.
У нас тогда, на первом проекте, делали трубку для радиотелефона. Там 8 или 16 КБ оперативы. Что-то такое. При этом девборда могла работать как туннель по эзернету — что пришло с радио — пихала в кабель, а на компе бежала логика (весь код трубки), и на C# был ЮИ, имитирующий трубку. На компе нажимаешь кнопку — прога обрабатывает логику на компе, и посылает команду для радио через кабель на девборду. И мы дебажили эмбедед код в вижуал студии на виндовс. И это было в разы быстрее, чем у соседей, разрабатывающих прямо на плате.

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

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

Писать ли Unit-тесты до готовности MVP

Смотря что вы планируете с этим MVP делать. Если выбрасывать и переписывать всё с нуля, то, наверное, можно и без unit-тестирования. Да и вообще без тестирования.

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

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

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

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

Но я никак не затрагиваю экономическую сторону вопроса. Если вы аутсорсеры, то крайне не рекомендую unit-тестирование. Эта инженерная практика напрямую конфликтует с экономической моделью аутсорсинга.
Для вас регрессия в средних масштабах — это прямая прибыль. Это возможность продать ещё 5 мануальных QA и 3 джуниора на багофикс. А в какой-то момент, с определённой долей фарта, даже продать переписывание проекта новой командой с нуля, потому что так «проще».

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

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

— избыточны — т.к. тестируют то же, что тестируется и интеграционными тестами (которые обязательны)

Это настолько далеко от реальности, что у меня возникает подозрение, что это неудачный троллинг, а не серьёзное заявление.
У меня не проходит, наверное, и дня, чтобы unit-тесты не ловили мелкую регрессию, которую я создавал внутри модуля над которым я активно работаю. Интеграционные тесты огромную категорию дефектов в принципе не покрывают. А даже если и покрывают, то попытка их исчерпать приводит к комбинаторному взрыву. Кроме того, на определённом этапе разработки и интегрироваться может быть не с чем. Моки и стабы в такой ситуации — это не более чем конвертация интеграционных тестов в unit.

Да, если мы говорим о разработке какого-нибудь middleware, где напрочь отсутствует сложная бизнес-логика, а весь код — это, по-сути, «клей», то утверждение об избыточности unit-тестирования и возможность его замены интеграционным — абсолютно справедливо. Вы случайно не круды пишите? Это бы объяснило позицию.

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

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

Изменение интерфейсов совершенно не является областью ответственности

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

Но реализация интерфейсов, как правило, состоит из декомпозиции различных классов. «юнит-тестеры» пишут тесты к этим отдельным классам (поскольку в ОО-языках именно класс — это минимальный модуль, являющийся субъектом юнит-тестирования).
Эти тесты — мусор. T.к. их приходится выкидывать (или, как минимум, глобально перерабатывать), в случае изменений реализации/декомпозиции. А такие изменения, это норма разработки.

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

Ок, я немного отвык от ОО идиом, каюсь. Для меня sut — это почти всегда функция, а значение типа (полей класса?) — не более чем I/O функций и assert’a.

Эти тесты — мусор. T.к. их приходится выкидывать (или, как минимум, глобально перерабатывать), в случае изменений реализации/декомпозиции. А такие изменения, это норма разработки.

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

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

Отдельно скажу, что на моей практике этого:

их приходится выкидывать (или, как минимум, глобально перерабатывать),

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

В каждодневной же практике, unit-тесты зеркально изменяются параллельно с реализацией. Обычно это добавление новых функций и типов (вместе с unit-тестами) или изменения в I/O существующих функций и параметрах типов (компилятор удобно ругается и unit-тесты падают) или инкрементальный рефакторинг на уровне выделения новых функций и типов из существующих (в unit-тестах это выражается вынесением без изменений существующих assert’ов в новые тесты или целых тестов в новый test case). Опять же, попытка покрыть такие изменения интеграционными тестами заканчивается либо огромнейшими дырами, либо комбинаторным взрывом.

Я говорю от лица стремительно изменяемой код базы с десятками тысяч unit-тестов и тысячами интеграционных.

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

Ок, я немного отвык от ОО идиом, каюсь. Для меня sut — это почти всегда функция, а значение типа (полей класса?) — не более чем I/O функций и assert’a.

Ок, если речь идёт о чистой функциональщине (без ОО/классов) — можно в качестве минимального модуля рассматривать отдельную функцию и соответственно, писать для неё юнит-тест.

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

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

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

Я разделяю понятия unit-тест (набор assert‘ов, где субъект — функция/метод) и test case (набор unit-тестов, где субъект — тип или «класс»). Возможно ООП фреймворки для тестирования называют unit-тестом то, что я называют test case и тогда понятно, почему субъектом они называют класс. Правда это противоречит здравому смыслу, но я не стану критиковать чужие идиомы.

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

Совершенно определенно нет. Поэтому я и не люблю перевод слова «unit» как «модуль». То, что я тестирую unit-тестами, является минимальной функциональной единицей. Под модулем я подразумеваю некий «пакет» из функций и типов. Если это вызвало замешательство, то извиняюсь.

Эти тесты — мусор. T.к. их приходится выкидывать (или, как минимум, глобально перерабатывать), в случае изменений реализации/декомпозиции. А такие изменения, это норма разработки.

А как ты будешь рефакторить систему, если у тебя нет юнит-тестов? Будешь все краевые условия тоже интеграционными тестами тестить? Почему бы тогда сразу не e2e.

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

А как ты будешь рефакторить систему, если у тебя нет юнит-тестов

Есть интеграционные тесты. Тесты при этом — пишутся не к кускам реализации (классам), а к функциям интерфейса. Реализации же этих функций — могут являться декомпозицией классов.

При этом, в тесте может использоваться инстанс лишь одного конкретного класса, а остальные моки (такое редкость и, в принципе, может рассматриваться как «юнит тест»). Либо чаще инстансы нескольких различных классов + моки (интеграционный тест).

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

То, что ты описываешь — лишь частный случай юнит-теста.

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

Я видал проекты, где кодеры массово писали юнит-тесты к отдельным классам (как по конспекту) — это всё бесполезный мусор. И выше я объясняю, почему.

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

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

Но да, часто эти интеграционные тесты — считают и называют «юнит-тестами».

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

Это твоё мнение, которое отличается от общепринятых определений.

Я видал проекты, где кодеры массово писали юнит-тесты к отдельным классам (как по конспекту) — это всё бесполезный мусор. И выше я объясняю, почему.

Это всего лишь твой частный опыт, основанный на неумении писать тесты.

Это твоё мнение, которое отличается от общепринятых определений.

Напротив, это единственно-возможное определение. Иначе, чем определение «юнит-теста» будет отличаться от определения «интеграционного теста»?

Вот, к примеру, определение из вики:

" In procedural programming, a unit could be an entire module, but it is more commonly an individual function or procedure. In object-oriented programming, a unit is often an entire interface, such as a class, but could be an individual method."

In object-oriented programming, a unit is often an entire interface, such as a class, but could be an individual method.

Это потому что в ванильных ООП языках (Smalltalk, Objective-C) минимальными функциональными единицами являются объекты, которые принимают сообщения. Поэтому субъектом unit-теста будет объект (инстанс класса), а утверждением — его состояние после отправки ему сообщения. Можно в одном unit-тесте поместить assert’ы на каждое сообщение, которое объект может обработать. Но на практике я такого почти не видел, потому что такой unit-тест производит невнятный failure report. Поэтому стандартном, де-факто, является именно
«In object-oriented programming, a unit is often [...] an individual method.»

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

Вот это уже попахивает ООП головного мозга или, как минимум, устаревшим лет на 40 определением. Даже из заданного вами контекста, где «реализация интерфейса — это декомпозиция классов» следует наличие некоего публичного интерфейса (допустим, у пакета/фреймворка) и его internal классов с internal методами. Даже если я в одном unit-тесте создаю инстанс одного internal класса, чтобы передать его аргументом в метод инстанса другого internal класса — это никак не будет интеграционным тестом в наиболее общепринятом понимании. Только если мы опустимся до уровня абстракции ООП примитивов (объектов и классов), но такой подход имеет мало смысла в современной разработке и сильно искажает понятие «интеграция».

Поэтому субъектом unit-теста будет объект (инстанс класса), а утверждением — его состояние после отправки ему сообщения. Можно в одном unit-тесте поместить assert’ы на каждое сообщение, которое объект может обработать.

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

Если в тесте участвуют инстaнсы более 1 класса — это не отвечает определению юнит-теста (как тестирующего лишь 1 юнит кода). Соответственно, такой тест интеграционный.

Ну так я об этом и написал — could be an entire module.

Это всего лишь твой частный опыт, основанный на неумении писать тесты.

Это не мой частный опыт. Это опыт крупного западного концерна, выкинувшего в мусорку проект стоимостью ~150 млн баксов. С кучей кодеров и ~100% тест-покрытием кода юнит-тестами.

Проблема была в том, что проект писался «с нуля» — и тамошние хипстеры решили применить лучшие практики юнит-тестирования. :)

Ни до, ни после (в успешных работающих проектах с очень большими объёмами кода) — чистого юнит-тестирования я не встречал.

Это не мой частный опыт. Это опыт крупного западного концерна, выкинувшего в мусорку проект стоимостью ~150 млн баксов. С кучей кодеров и ~100% тест-покрытием кода юнит-тестами.
Ни до, ни после (в успешных работающих проектах с очень большими объёмами кода) — чистого юнит-тестирования я не встречал.

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

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

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

Это лишь твоё понимание, отличное от общепринятого. Нигде не говорится, что юнит-тест тестирует лишь один класс. Хотя зачастую это и так.

Я видал проекты, где кодеры массово писали юнит-тесты к отдельным классам (как по конспекту) — это всё бесполезный мусор. И выше я объясняю, почему.

Это лишь твой опыт, основанный на неумении писать тесты.

Плюс, если тестируется интерфейс, а не реализация (и так делать правильно)

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

Интерфейс невозможно протестировать.

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

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

Так ты ж пишешь, что реализацию не тестируем?

Так ты ж пишешь, что реализацию не тестируем?

Реализация тестируется в виде «блэк бокса». То бишь, создаётся инстанс, реализующий интерфейс, вызывается функция интерфейса (некими тестовыми параметрами) — проверяются состояние инстанса/данные на выходе (на соответствие спеке).

Но ничего кроме функций интерфейса — смысла тестироватъ нет.

Чтобы я просто понимал, о какого масштаба модулях, скрытых за этими интерфейсами, ты говоришь? Десяток классов? Сотня?

Просто я немного не понимаю твоей позиции полного отбрасывания юнит тестов. Если честно, для меня вообще странно звучит, что инженер просто откидывает какой-то инструмент и говорит «я буду пользоваться только тем инструментом», потому что лвиная доля работы программиста заключается в правильном выборе компромисса. Например, интеграционные тесты более устойчивы к изменениям системы, но они более медленные, более сложны в настройке предусловий (test fixture), проверке своих постулатов. За счёт того, что SUT в данном случае гораздо больше, им приходится тестировать некую суперпозицию функциональности, за счёт чего они подвержены дупликации кода и хуже выполняют роль документирования бютребований более мелкого масштаба (таких как краевые условия). Соответственно, у юнит тестов тоже есть свои преимущества и недостатки, и их надо знать, чтобы правильно пользоваться инструментом.

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

Чтобы я просто понимал, о какого масштаба модулях, скрытых за этими интерфейсами, ты говоришь? Десяток классов? Сотня?

Скорее всего, мы говорим об одних вещах, но ты называешь «юнит-тестами» многое из того, что я называю «интеграционными тестами».

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

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

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

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

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

Юнит-тесты экономически нецелесообразны ни в проектах, ни в продуктах.

С каких это пор кодерки научились экономическую целесообразность считать?

Смотря что вы планируете с этим MVP делать. Если выбрасывать и переписывать всё с нуля, то, наверное, можно и без unit-тестирования. Да и вообще без тестирования.

Если же планируется из MVP плавно развиваться в просто P, то

такое предвидеть нереально. Просто потому что заказчик захочет свистелок и переделок.
И, как правило, для экономии ставят задачу делать сразу такое MVP, "чтоб чуток допилить и в продакшен"(ТМ).

Само по себе разделения фаз разработки на дизайн, проектирование, пототипирование не зря возникло. Каждая фаза даёт возможность откатить созданное и пойти другим путём. Это важная опция при скрамной разработке, когда документацию и требования создаёт на лету

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

Кмк, хорошо бы прогнуть заказчика на полноценный MVP (а не Р, потому что Р сразу после РОС = 3.14) и тесты писать только по критичным моментам, чисто для себя, чтоб ноги друг-другу не оттоптать не поломать чего-то в спешке.

Но я никак не затрагиваю экономическую сторону вопроса. Если вы аутсорсеры, то крайне не рекомендую unit-тестирование. Эта инженерная практика напрямую конфликтует с экономической моделью аутсорсинга.
Для вас регрессия в средних масштабах — это прямая прибыль. Это возможность продать ещё 5 мануальных QA и 3 джуниора на багофикс. А в какой-то момент, с определённой долей фарта, даже продать переписывание проекта новой командой с нуля, потому что так «проще».

Жесть какая-то. Бегите подальше от таких «контор». Это грабительство и паразитизм

Жесть какая-то. Бегите подальше от таких «контор». Это грабительство и паразитизм

Да, а ещё это реалии >60% разработки на территории Украины (доля аутсорсинга по последней статистике на DOU). Там unit-тестирование, в лучшем случае, «продают» заказчику, а в большинстве случаев делают как я описал.

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

Достали нот, баса, альта, две скрипки
И сели на лужок под липки -
Пленять своим искусством свет

Зальём в продакшен — тестировщиков там больше
©

Делать что-то имеет смысл, если вы понимаете, зачем вы это делаете.
А судя по вашему вопросу — понимания у вас нет.

а спеки есть по которым педалят код?

«тест-дривен-девелопмент» — это лишь хипстерские фантазии. Нежизнеспособные.

Тесты можно писать и до готовности продукта. Главное, чтобы уже были готовы соответствующие части спеки (по которой и написан код).

Юнит-тесты ацтой. Интеграционные тесты рулят.

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

Какие-то объяснения уровня хотя бы пятого класса будут или ты просто потроллить? :)

В абсолютній більшості проектів в індустрії тдд не використовується, а юніт тести пишуться лише на нетривіальний код.

То, что в твоей практике этого не было, говорит только о скудности твоего опыта.

Підписатись на коментарі