Тимофей, спасибо за хорошо проработанный материал. Возможно будет интересно. Вам или Вашим читателям. Кросс-ссылочка. Мы тоже пытались показать, как автотесты экономят именно время разработчика. Не так красиво получилось, как у Вас, зато есть конкретные цифры, не сильно отличающиеся от Ваших.
И снова соглашусь с первым комментарием. Захардкоженная навигация только у Вас. У меня вот проблем нет. И вся статья дальше тоже теряет актуальность в связи с этим.
Почему нет проблемы? Все просто. Перенесите первый же Ваш код сниппет (с экшеном кнопки) в расширение UINavigationController. Все. Ни один Ваш унаследованный контроллер не знает ни об этом экшене, ни о других контроллерах, ни об их взаимосвязи...
Попробуйте, это крайне просто. Сильно проще, чем координатор. И эстетичнее. Как команды в системном меню...
Сам тоже сейчас редактирую статью про "правильную готовку" MVVM. Думал уже не публиковать, но, видимо, теперь придется :)
В статье Вики про MVVM указано, что вьюмодель содержит всего 3 ответственности:
абстракцию для разделения вьюхи и модели (слабая связность).
преобразование данных из вида модели к виду отображения (и возможно, но необязательно, обратно).
биндинг - вариация абстрагирования и/или преобразования данных.
Вот, в принципе и все, что имеет право быть во вьюомдели. Все остальное, что вы перечислили, это уже либо модель, либо представление. Без вариантов! Например, навигация – ответственность представления. Если Вы меняете систему представления (например, с iOS переезжаете на мак, часы или телевизор), то в Вашем приложении не нужно бы менять ни вьюмодель, ни модель... а если Ваша модель зависит от роутера или координатора, то вам этого не избежать... а вот если роутер или координатор дергаются напрямую из вью, то остальные два слоя вашего приложения изменений не требуют – достаточно поменять только представление...
Unidirectional Data Flow – это пункт 2 из обязательных ответственностей. Преобразование данных. В одну и в другую сторону. Они в 142% случаев всегда независимы друг от друга, да. Поэтому реализовать их в двух разных классах не представляет сложности никогда. Но при этом оба класса все же остаются внутри слоя вьюмодели. Это слой. Он не обязан состоять из одного класса... В вашем примере Вы просто лишь одну часть этой ответственности осуществляете через биндинг. Но можно и обе, проблем не будет, это все еще будет UDF, если вью будет писать в одно свойство, а модель в другое (в одном или разных объектах – уже неважно)...
Но в принципе биндинг для преобразования данных не обязателен, как и для абстракции. Но с его помощью можно реализовать и то, и другое. А можно и не реализовывать. Биндинг для MVVM не обязателен, но тогда оно выродится в MVP или MVC с пассивной моделью.
В остальном, Вы молодец. Копайте глубже и проблем у Вас не будет ни с какой архитектурой. Еще и на лету их сможете менять, если ответственности представления (типа навигации) не будете опускать ниже.
ПС: неплохо Вы статьи генерите – по одной в день – ИИ помогает?
Соглашусь с первым комментарием по поводу состояния.
"Состояние"("State") – это прям отдельный шаблон из книжки Банды Четырех, чтобы с ним работа была комфортна, он должен реализовываться через набор классов с одним и тем же интерфейсом. Иначе, рано или поздно столкнетесь с проблемами. Например, enum'ы не расширяемые. Добавить состояние без нарушения OCP у вас не выйдет. В мелких проектах это и не важно, но в крупных...
Ну, и Вы тут много говорите о том, что не нужно смешивать ответственности, а "Состояние" (с переходами между ними, которые довольно часто не тривиальные! и не все вообще разрешены!) – вполне себе цельная отдельная ответственность...
Попробуйте её инкапсулировать по шаблону Банды Четырех. Не с первого раза, но получится всю прелесть прочувствовать...
Подскажите, пожалуйста, а зачем инъекции осуществлять через всю цепочку зависимостей? Очевидно же, что описанная проблема с "прокидыванием зависимостей" возникает исключительно из-за этого. Серьезно, ну зачем?
У Гугла, например, библиотеки инициируются как-то так:
let myLogger = MyLogger(bla-bla)
Firebase(logger: myLogger)
При таком подходе вы убираете проброс из всех промежуточных зависимостей и инжектируете конкретно туда, куда нужно. Реализация - простейшая, тестируется - элементарно.
Сначала сами себе создаем лютейшую связность (high coupling) там, где её никогда не было, а потом героически своих же действий последствия преодолеваем.
Если смущает, что в реализации будет Singletone, то вспомните про шаблон "Strategy" + "Фабричный метод" той же Банды Четырех, которые делегирует создание зависимости кому-то стороннему. Прямая цель первого шаблона, кстати, и есть подмена алгоритма без увеличения связности, а второго – как раз подмена создания нужного объекта без увеличения связности. Как-будто судя по цели - то, что Вам нужно. Дешево и сердито. Нет? Не подходит "Фабричный метод"? – возьмите любой другой пораждающий шаблон. 🤷
У Мартина вообще 3 уровня, и четвертого, как-будто, и не нужно никогда (если вспомнить о назначении каждого слоя). Как-будто логика приложения – это и есть то, как бизнес-модель проецируется в представление и обратно. Как-будто логика приложения – это содержимое уровня контроллеров.
Вообще, конечно, узнать бы у автора, что подразумевается под логикой приложения. @Ognev_Anton Определения в статье нет, термин используется только на картинке. Но можно предположить, что под этим уровнем имеется ввиду вот это из статьи: "Настройка окружения, заголовки, работа с запросами и сессией, кодирование и декодирование пишутся один раз и являются общими для всего приложения."
В таком случае, это действительно уровень адаптера и преобразования данных – только с другой стороны кольца – не со стороны представления, а со стороны сервера. По Мартину входов в эти кольца может быть сколько угодно.
Но так исторически сложилось, что в иос (да и андроиде, и вообще в мобилах) – общение с сервером воспринимается, как самая главная часть бизнес логики, на которую и идет все равнение (т.е.от нее зависит все). Сервер для мобильной разработки – главный закон, решения сервера не оспариваются, а выполняются... У нас в приложении так же. Поэтому это более глубокий уровень, что, похоже, и отображено на диаграмме.
Но вот вопрос: какой из подходов лучше и почему (традиционный используется уже лет 15)? Стоит ли нам инвертировать зависимости между бизнес-логикой и адаптером к серверу (то, что в статье названо логикой приложения)? От чего это зависит? Как это замерить?
Что думаете? Сможете нам помочь в ответах на эти важные для нас вопросы?
Зря я "мегатест" упомянул в вопросе. Давайте забудем про него.
Конкретные примеры:
Вот у вас последний тест - там в 1 тесте сразу 2 скриншота генерируется.
Пример с "all" - там в 1 тесте вообще 4 скриншота проверяется.
Если падает 1 тест, в котором сразу с десяток (или 2 или 4, неважно, главное – что не одна) картинок генерируется, насколько просто искать потом место, которое нужно править?
______________
Кстати, "не больше 130 картинок на компонент", а тестов при этом сколько на компонент?
Скриншот-тесты - это не юниты, хоть они и близки к юнитам. Для юнитов необходимое условие – это изоляция. Если для скриншотов по аналогии использовать изоляцию, то в 1 тесте должна быть 1 картинка. Но мне, например, понятно, что скриншоты – все же выше юнитов, и поэтому, возможно, это правило не должно выполняться прям полностью. Вы его не выполняете. Но мне хочется понять, какие это издержки привносит...
Пока вы отметили, что приходится копаться в именах, но это происходит нечасто... Может быть что-то еще?
Вы в одном тесте пишите сразу кучу модификаторов состояний. В итоге количество скринов возрастает экспоненциально. В примере у вас лишь удвоение дважды, но как в итоге после полугода использования? Наверняка ж есть и компоненты у которых и самих изменяющихся свойств сильно больше, чем 2 и состояний у каждого из них – тоже может быть сильно больше чем 2. Писать явно проще, но добавление 1 свойства и всех его значений может сильно замедлить тесты. Можете подсказать по практике, сколько у вас больших компонентов, сколько в них вариантов, сколько в итоге времени гоняются тесты?
И еще вопрос: насколько удобно держать столько вариантов в 1 тесте? в итоге получается по сути 1 мегатест на 1 компонент. Если что-то падает, то насколько легко разобраться, куда идти и что править?
простите, но Вы забыли постусловие с переполнением и тест на него. а еще тесты бы граничными условиями дополнить, чего не написано даже в ТЗ (а должно быть, как программа себя ведет в граничных случаях?). sum(-MAX_INT, -MAX_INT) - это переполнение или успех? а в остальном - все отлично!
Но все же пирамида тестирования утверждает, что тест должен быть реализован на самом нижнем уровне, на котором это возможно. (Иначе пирамиды не выйдет, очевидно же?)
Пример. Валидация введенной суммы перевода.
Можно проверять прям от UI через сервер и до появления красной картинки обратно на UI. Получится сложный, медленный, дорогой, хрупкий e2e-тест, который тестирует поведение.
Но самый ли нижний это из возможных уровней, где проводится валидация?
Нет, конечно!
Мы можем убрать UI и проверять, допустим, что вход в функцию валидации какого-то значения, возвращает или не возвращает ошибку (которая потом бы в UI превратился в красную картинку). Так мы опустили проверки на уровень ниже. Проверяется ли при этом нужное поведение? Да, если к нему добавить 2 интеграционных теста, которые проверят, что значение из UI корректно в функцию записывается, а её возвращаемое значение корректно в UI устанавливается.
Что мы получили? Интеграция проверена 1 раз. Валидация введенного знания - 1 раз. Многократного покрытия нет.
Что касается TDD, то, наверное, Вы правы, что сначала будет написан один высокоуровневый e2e тест. Но ничто не мешает Вам на стадии рефакторинга рефакторить не только код, но и тест (или мешает? вопрос автору статьи, кстати @iv660 подразумевает ли рефакторинг и рефакторинг ранее написанного теста? ), разбивая 1 большой тест на несколько меньших, опуская все возможные проверки на более низкие уровни. Так вы избежите многократного тестирования и получите надежные, дешевые, быстрые тесты, которые не боятся рефакторинга, ибо каждый ваш тест будет соответствовать тому уровню, на котором реализовано соответствующее поведение.
А может, лучше "суп отдельно, а мухи отдельно" - в смысле спецификация отдельно, а проверка ее отдельно? Потому что объективно нужны эти вещи в разные моменты времени: спецификация - в процессе создания программы, а тесты - уже на этапе контроля, когда программа создана.
2 момента:
Тесты нужны ДО создания программы и ВО ВРЕМЯ её создания тоже. Об этом вся статья про TDD, в комментах к которой мы общаемся (например, пп.1,6,7). Об этом же и Ваш пункт про СПЕЦИФИКАЦИЮ, который мы сейчас обсуждаем. СПЕЦИФИКАЦИЯ подразумевает, что она должна быть написана ДО. Значит, и тесты должны быть написаны ДО. Также Вы выше пишите, что тесты нужны только на стадии ФИНАЛЬНОГО контроля. Но вот теория менеджмента подразумевает, что контроль бывает не только финальный. Он еще бывает входящий (ДО начала выполнения задачи, т.е.разработки программы), промежуточный и переодический (последние 2 - ВО ВРЕМЯ разработки программы). Отсюда снова следует, что тесты должны быть созданы как минимум ДО начала работы программы, если мы их хотим использовать для входного контроля, или ВО ВРЕМЯ, если хотим использовать промежуточный и/или переодический. Автору в п.4 и отдельным пунктом можно добавить про типы контроля и как TDD помогает про них не забывать...
Конечно нужно разделять! Разделение мух от котлет выглядит так: фиксируем мы требования и поведение через написание теста, а контролируем потом через его исполнение. Вас не устраивает такое разделение?
Код по сравнению с текстом слишком многословен, то есть имеет больший объем, причем объем кода тестов (в строках и знаках) обычно в разы больше объема покрываемого ими кода приложения (правда сложность - цикломатическая и т.п. - кода тестов обычно меньше). В коде, опять же, надо уделять внимание соблюдению церимониалов используемых языков/библиотек/фреймворков (boilerplate),. Код, даже "самодокументируемый", труднее читать, чем текстовое описание, наконец.
Ниже в этой же ветке уже ответили, что это не всегда так. Я лишь добавлю, что перечисленные Вами "недостатки" давно придумано как нивелировать и устранять. Учитывая это я бы даже сказал, что запись спецификации в виде тестов ДОВОЛЬНО ЧАСТО получается даже дешевле и проще, чем обычным человеческим текстом. Отсюда мы возвращаемся к тезису про более дешевую сумму записи спецификации и её автоматизации и получаем, что автотесты становятся практически самым дешевым и эффективным вариантом, как бы со стороны не казалось иначе. Такой вот парадокс, да.
А она точно нужна, эта автоматизация?
Про это п.4. И мои комментарии выше. Я тоже LLM не использовал для генерации тестов, но прекрасно знаю некоторые своды правил, которые позволяют сделать так, что требования из ТЗ, записанные в виде кода теста, получаются проще, чем в тексте самого ТЗ. Возможно, Вы умеете писать ТЗ круче наших аналитиков (в этот вопрос я с вами в дискуссию вступать не стану).
А забыли про овраги: в процессе начальной разработки - всего приложения или какой-то отноительно самостоятельной его функции, проводимом без предварительного детального проектирования (то есть - в обычном практическом случае) меняться может многое, почти всё. В том числе и поведение, и разбиение его на единицы. И уж всяко - интерфейсы, через которые взаимодействуют модули. И чем менее тривильна разрабатываемая программа, тем чаще такие изменения происходят.
Ну, я же не зря Вам целую книгу порекомендовал, наверное? Вот Вы спорите даже не прочитав, похоже. Собственно, как и всю статью... че уж я про книгу-то сетую...
Пожалуй, на этом я остановлюсь, на дальнейшие Ваши комментарии отвечать не стану. Спасибо Вам за содержательную дискуссию.
Ну, Вы сами написали, что TDD можно писать с любыми тестами. Но пирамида тестирования утверждает, что модульных (юнитов) должно быть больше всего. Поэтому замените в названии статьи модульные тесты на TDD - логическая цепочка не нарушится, смысл не поменяется.
Нет, не кажется. Это лишь частично спецификация. Второй частью – это автоматизация проверки соответствия результата этой спецификации. Т.е. заменять тесты только спецификацией – в корне неверно. Вы еще обязаны для полного замещения организовать автоматизацию на другой тип написания спецификации. Если другие типы написания спецификации и будут проще, то вот их автоматизация – навряд ли. Соответственно и сумма спецификации в другом типе и её автоматизации выйдет дороже, чем тесты.
Проблема изменения тестов при изменении внутренней реализации давно решена: тестируются не детали внутренней реализации, а единица поведения. Подробнее в книге Хорикова про модульное тестирование.
Пирамида тестирования утверждает, что каждая единица поведения должна быть проверена ровно 1 раз и на том самом нижнем уровне, на котором её можно проверить. Не все можно проверить на самом нижнем уровне – уровне модульных тестов без интеграций. Но если уж вы проверили что-то на более низком уровне, то на более высоком дублирующих тестов быть не должно, это же очевидно.
Как результат, ситуация "функционал одного и того же компонента покрывается многократно" невозможна. Более того, если тесты пишутся именно на поведение, а не на детали внутренней реализации, то проблем с рефакторингом не будет. Почему, очень хорошо объяснено в книге Хорикова про модульное тестирование.
Спасибо за статью. Лучшее описание типов, что я на данный момент встречал. Хотя я и не читал-то слишком больше, чем академических учебников, где все описывается сухо и на советских примерах (ой, кажется, у Вас тоже на них же :)
С нетерпением ждем пост про грамотную расстановку людей и соответствующие ссылки.
Это может быть человек с любым преобладающим типом. Даже "избегательный" – для него маленький стартап – это возможность делать еще меньше, другие работают, а он отдыхает. Внезапно.
Кросс-ссылка на схожий материал о пользе автотестов.
Тимофей, спасибо за хорошо проработанный материал.
Возможно будет интересно. Вам или Вашим читателям. Кросс-ссылочка. Мы тоже пытались показать, как автотесты экономят именно время разработчика. Не так красиво получилось, как у Вас, зато есть конкретные цифры, не сильно отличающиеся от Ваших.
И снова соглашусь с первым комментарием. Захардкоженная навигация только у Вас. У меня вот проблем нет. И вся статья дальше тоже теряет актуальность в связи с этим.
Почему нет проблемы? Все просто. Перенесите первый же Ваш код сниппет (с экшеном кнопки) в расширение UINavigationController. Все. Ни один Ваш унаследованный контроллер не знает ни об этом экшене, ни о других контроллерах, ни об их взаимосвязи...
Попробуйте, это крайне просто. Сильно проще, чем координатор. И эстетичнее. Как команды в системном меню...
Сам тоже сейчас редактирую статью про "правильную готовку" MVVM. Думал уже не публиковать, но, видимо, теперь придется :)
В статье Вики про MVVM указано, что вьюмодель содержит всего 3 ответственности:
абстракцию для разделения вьюхи и модели (слабая связность).
преобразование данных из вида модели к виду отображения (и возможно, но необязательно, обратно).
биндинг - вариация абстрагирования и/или преобразования данных.
Вот, в принципе и все, что имеет право быть во вьюомдели. Все остальное, что вы перечислили, это уже либо модель, либо представление. Без вариантов! Например, навигация – ответственность представления. Если Вы меняете систему представления (например, с iOS переезжаете на мак, часы или телевизор), то в Вашем приложении не нужно бы менять ни вьюмодель, ни модель... а если Ваша модель зависит от роутера или координатора, то вам этого не избежать... а вот если роутер или координатор дергаются напрямую из вью, то остальные два слоя вашего приложения изменений не требуют – достаточно поменять только представление...
Unidirectional Data Flow – это пункт 2 из обязательных ответственностей. Преобразование данных. В одну и в другую сторону. Они в 142% случаев всегда независимы друг от друга, да. Поэтому реализовать их в двух разных классах не представляет сложности никогда. Но при этом оба класса все же остаются внутри слоя вьюмодели. Это слой. Он не обязан состоять из одного класса... В вашем примере Вы просто лишь одну часть этой ответственности осуществляете через биндинг. Но можно и обе, проблем не будет, это все еще будет UDF, если вью будет писать в одно свойство, а модель в другое (в одном или разных объектах – уже неважно)...
Но в принципе биндинг для преобразования данных не обязателен, как и для абстракции. Но с его помощью можно реализовать и то, и другое. А можно и не реализовывать. Биндинг для MVVM не обязателен, но тогда оно выродится в MVP или MVC с пассивной моделью.
В остальном, Вы молодец. Копайте глубже и проблем у Вас не будет ни с какой архитектурой. Еще и на лету их сможете менять, если ответственности представления (типа навигации) не будете опускать ниже.
ПС: неплохо Вы статьи генерите – по одной в день – ИИ помогает?
Соглашусь с первым комментарием по поводу состояния.
"Состояние"("State") – это прям отдельный шаблон из книжки Банды Четырех, чтобы с ним работа была комфортна, он должен реализовываться через набор классов с одним и тем же интерфейсом. Иначе, рано или поздно столкнетесь с проблемами. Например, enum'ы не расширяемые. Добавить состояние без нарушения OCP у вас не выйдет. В мелких проектах это и не важно, но в крупных...
Ну, и Вы тут много говорите о том, что не нужно смешивать ответственности, а "Состояние" (с переходами между ними, которые довольно часто не тривиальные! и не все вообще разрешены!) – вполне себе цельная отдельная ответственность...
Попробуйте её инкапсулировать по шаблону Банды Четырех. Не с первого раза, но получится всю прелесть прочувствовать...
Даже художники и писатели "струячат" свои шедевры, как на конвейере, чтобы выжить. Стиг Ларсон, Ю Несбё и прочие...
Есть ли где-то не конвейер в этой жизни? :) наверное, только у бомжа в зимнем московском трамвае - вот где истинная свобода...
Или достаточно просто написать несколько интеграционных тестов. 🤷
Подскажите, пожалуйста, а зачем инъекции осуществлять через всю цепочку зависимостей? Очевидно же, что описанная проблема с "прокидыванием зависимостей" возникает исключительно из-за этого. Серьезно, ну зачем?
У Гугла, например, библиотеки инициируются как-то так:
При таком подходе вы убираете проброс из всех промежуточных зависимостей и инжектируете конкретно туда, куда нужно. Реализация - простейшая, тестируется - элементарно.
Сначала сами себе создаем лютейшую связность (high coupling) там, где её никогда не было, а потом героически своих же действий последствия преодолеваем.
Если смущает, что в реализации будет Singletone, то вспомните про шаблон "Strategy" + "Фабричный метод" той же Банды Четырех, которые делегирует создание зависимости кому-то стороннему. Прямая цель первого шаблона, кстати, и есть подмена алгоритма без увеличения связности, а второго – как раз подмена создания нужного объекта без увеличения связности. Как-будто судя по цели - то, что Вам нужно. Дешево и сердито. Нет? Не подходит "Фабричный метод"? – возьмите любой другой пораждающий шаблон. 🤷
У Мартина вообще 3 уровня, и четвертого, как-будто, и не нужно никогда (если вспомнить о назначении каждого слоя). Как-будто логика приложения – это и есть то, как бизнес-модель проецируется в представление и обратно. Как-будто логика приложения – это содержимое уровня контроллеров.
Вообще, конечно, узнать бы у автора, что подразумевается под логикой приложения. @Ognev_Anton Определения в статье нет, термин используется только на картинке. Но можно предположить, что под этим уровнем имеется ввиду вот это из статьи: "Настройка окружения, заголовки, работа с запросами и сессией, кодирование и декодирование пишутся один раз и являются общими для всего приложения."
В таком случае, это действительно уровень адаптера и преобразования данных – только с другой стороны кольца – не со стороны представления, а со стороны сервера. По Мартину входов в эти кольца может быть сколько угодно.
Но так исторически сложилось, что в иос (да и андроиде, и вообще в мобилах) – общение с сервером воспринимается, как самая главная часть бизнес логики, на которую и идет все равнение (т.е.от нее зависит все). Сервер для мобильной разработки – главный закон, решения сервера не оспариваются, а выполняются... У нас в приложении так же. Поэтому это более глубокий уровень, что, похоже, и отображено на диаграмме.
Но вот вопрос: какой из подходов лучше и почему (традиционный используется уже лет 15)? Стоит ли нам инвертировать зависимости между бизнес-логикой и адаптером к серверу (то, что в статье названо логикой приложения)? От чего это зависит? Как это замерить?
Что думаете? Сможете нам помочь в ответах на эти важные для нас вопросы?
Спасибо за подмеченный нюанс!
хорошо понятно.
Зря я "мегатест" упомянул в вопросе. Давайте забудем про него.
Конкретные примеры:
Вот у вас последний тест - там в 1 тесте сразу 2 скриншота генерируется.
Пример с "all" - там в 1 тесте вообще 4 скриншота проверяется.
Если падает 1 тест, в котором сразу с десяток (или 2 или 4, неважно, главное – что не одна) картинок генерируется, насколько просто искать потом место, которое нужно править?
______________
Кстати, "не больше 130 картинок на компонент", а тестов при этом сколько на компонент?
Скриншот-тесты - это не юниты, хоть они и близки к юнитам. Для юнитов необходимое условие – это изоляция. Если для скриншотов по аналогии использовать изоляцию, то в 1 тесте должна быть 1 картинка. Но мне, например, понятно, что скриншоты – все же выше юнитов, и поэтому, возможно, это правило не должно выполняться прям полностью. Вы его не выполняете. Но мне хочется понять, какие это издержки привносит...
Пока вы отметили, что приходится копаться в именах, но это происходит нечасто... Может быть что-то еще?
Вы в одном тесте пишите сразу кучу модификаторов состояний. В итоге количество скринов возрастает экспоненциально. В примере у вас лишь удвоение дважды, но как в итоге после полугода использования? Наверняка ж есть и компоненты у которых и самих изменяющихся свойств сильно больше, чем 2 и состояний у каждого из них – тоже может быть сильно больше чем 2. Писать явно проще, но добавление 1 свойства и всех его значений может сильно замедлить тесты. Можете подсказать по практике, сколько у вас больших компонентов, сколько в них вариантов, сколько в итоге времени гоняются тесты?
И еще вопрос: насколько удобно держать столько вариантов в 1 тесте? в итоге получается по сути 1 мегатест на 1 компонент. Если что-то падает, то насколько легко разобраться, куда идти и что править?
простите, но Вы забыли постусловие с переполнением и тест на него.
а еще тесты бы граничными условиями дополнить, чего не написано даже в ТЗ (а должно быть, как программа себя ведет в граничных случаях?).
sum(-MAX_INT, -MAX_INT)- это переполнение или успех?а в остальном - все отлично!
Вы правы, подходов много.
Но все же пирамида тестирования утверждает, что тест должен быть реализован на самом нижнем уровне, на котором это возможно. (Иначе пирамиды не выйдет, очевидно же?)
Пример. Валидация введенной суммы перевода.
Можно проверять прям от UI через сервер и до появления красной картинки обратно на UI. Получится сложный, медленный, дорогой, хрупкий e2e-тест, который тестирует поведение.
Но самый ли нижний это из возможных уровней, где проводится валидация?
Нет, конечно!
Мы можем убрать UI и проверять, допустим, что вход в функцию валидации какого-то значения, возвращает или не возвращает ошибку (которая потом бы в UI превратился в красную картинку). Так мы опустили проверки на уровень ниже. Проверяется ли при этом нужное поведение? Да, если к нему добавить 2 интеграционных теста, которые проверят, что значение из UI корректно в функцию записывается, а её возвращаемое значение корректно в UI устанавливается.
Что мы получили? Интеграция проверена 1 раз. Валидация введенного знания - 1 раз. Многократного покрытия нет.
Что касается TDD, то, наверное, Вы правы, что сначала будет написан один высокоуровневый e2e тест. Но ничто не мешает Вам на стадии рефакторинга рефакторить не только код, но и тест (или мешает? вопрос автору статьи, кстати @iv660 подразумевает ли рефакторинг и рефакторинг ранее написанного теста? ), разбивая 1 большой тест на несколько меньших, опуская все возможные проверки на более низкие уровни. Так вы избежите многократного тестирования и получите надежные, дешевые, быстрые тесты, которые не боятся рефакторинга, ибо каждый ваш тест будет соответствовать тому уровню, на котором реализовано соответствующее поведение.
У нас вроде работает \Разведенные в сторону руки/
2 момента:
Тесты нужны ДО создания программы и ВО ВРЕМЯ её создания тоже. Об этом вся статья про TDD, в комментах к которой мы общаемся (например, пп.1,6,7). Об этом же и Ваш пункт про СПЕЦИФИКАЦИЮ, который мы сейчас обсуждаем. СПЕЦИФИКАЦИЯ подразумевает, что она должна быть написана ДО. Значит, и тесты должны быть написаны ДО.
Также Вы выше пишите, что тесты нужны только на стадии ФИНАЛЬНОГО контроля. Но вот теория менеджмента подразумевает, что контроль бывает не только финальный. Он еще бывает входящий (ДО начала выполнения задачи, т.е.разработки программы), промежуточный и переодический (последние 2 - ВО ВРЕМЯ разработки программы).
Отсюда снова следует, что тесты должны быть созданы как минимум ДО начала работы программы, если мы их хотим использовать для входного контроля, или ВО ВРЕМЯ, если хотим использовать промежуточный и/или переодический.
Автору в п.4 и отдельным пунктом можно добавить про типы контроля и как TDD помогает про них не забывать...
Конечно нужно разделять!
Разделение мух от котлет выглядит так: фиксируем мы требования и поведение через написание теста, а контролируем потом через его исполнение. Вас не устраивает такое разделение?
Ниже в этой же ветке уже ответили, что это не всегда так. Я лишь добавлю, что перечисленные Вами "недостатки" давно придумано как нивелировать и устранять. Учитывая это я бы даже сказал, что запись спецификации в виде тестов ДОВОЛЬНО ЧАСТО получается даже дешевле и проще, чем обычным человеческим текстом.
Отсюда мы возвращаемся к тезису про более дешевую сумму записи спецификации и её автоматизации и получаем, что автотесты становятся практически самым дешевым и эффективным вариантом, как бы со стороны не казалось иначе. Такой вот парадокс, да.
Про это п.4. И мои комментарии выше. Я тоже LLM не использовал для генерации тестов, но прекрасно знаю некоторые своды правил, которые позволяют сделать так, что требования из ТЗ, записанные в виде кода теста, получаются проще, чем в тексте самого ТЗ. Возможно, Вы умеете писать ТЗ круче наших аналитиков (в этот вопрос я с вами в дискуссию вступать не стану).
Ну, я же не зря Вам целую книгу порекомендовал, наверное? Вот Вы спорите даже не прочитав, похоже. Собственно, как и всю статью... че уж я про книгу-то сетую...
Пожалуй, на этом я остановлюсь, на дальнейшие Ваши комментарии отвечать не стану. Спасибо Вам за содержательную дискуссию.
Ну, Вы сами написали, что TDD можно писать с любыми тестами. Но пирамида тестирования утверждает, что модульных (юнитов) должно быть больше всего. Поэтому замените в названии статьи модульные тесты на TDD - логическая цепочка не нарушится, смысл не поменяется.
Нет, не кажется. Это лишь частично спецификация. Второй частью – это автоматизация проверки соответствия результата этой спецификации. Т.е. заменять тесты только спецификацией – в корне неверно. Вы еще обязаны для полного замещения организовать автоматизацию на другой тип написания спецификации. Если другие типы написания спецификации и будут проще, то вот их автоматизация – навряд ли. Соответственно и сумма спецификации в другом типе и её автоматизации выйдет дороже, чем тесты.
Проблема изменения тестов при изменении внутренней реализации давно решена: тестируются не детали внутренней реализации, а единица поведения. Подробнее в книге Хорикова про модульное тестирование.
Пирамида тестирования утверждает, что каждая единица поведения должна быть проверена ровно 1 раз и на том самом нижнем уровне, на котором её можно проверить. Не все можно проверить на самом нижнем уровне – уровне модульных тестов без интеграций. Но если уж вы проверили что-то на более низком уровне, то на более высоком дублирующих тестов быть не должно, это же очевидно.
Как результат, ситуация "функционал одного и того же компонента покрывается многократно" невозможна. Более того, если тесты пишутся именно на поведение, а не на детали внутренней реализации, то проблем с рефакторингом не будет. Почему, очень хорошо объяснено в книге Хорикова про модульное тестирование.
Для конкретизации п.5 вот даже целую детальную статейку набросали, почему так выходит...
Спасибо за статью. Лучшее описание типов, что я на данный момент встречал. Хотя я и не читал-то слишком больше, чем академических учебников, где все описывается сухо и на советских примерах (ой, кажется, у Вас тоже на них же :)
С нетерпением ждем пост про грамотную расстановку людей и соответствующие ссылки.
Это может быть человек с любым преобладающим типом. Даже "избегательный" – для него маленький стартап – это возможность делать еще меньше, другие работают, а он отдыхает. Внезапно.