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

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



    Сегодня мы поговорим о том,

    • Как делать тестирование сложными зависимостями?
    • Как добиться большого тестового покрытия?
    • Как тесты влияют на дизайн?
    • Что делать, когда много логики в базе?
    • Как соблюсти компромисс между дизайном и «не дизайном».



    О спикере: Андрей Коломенский — Agile Coach в компании OnAgile, пишет код уже более 10 лет, работал как над сложными доменными моделями — такими, как платежные системы, так и над разработкой сложных legacy кодов, когда их приходилось спасать и восстанавливать продуктивность работы над ними.

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

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

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

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

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

    • Наш код был слишком жестким. Жесткость (rigidity) говорит о том, что система сопротивляется внесению изменений. Чтобы внести изменения в систему, мы должны затронуть очень много компонент.
    • Наша система была хрупкой (fragility). Это тенденция системы ломаться в самых различных местах при внесении, казалось бы, небольших изменений.
    • Наша система была непереносима — immobility. Это качество системы, которое говорит о том, что мы не можем повторно использовать код из одной системы в другой. Точнее, можем, но затраты на извлечение будут дороже, чем затраты на написание кода с самого начала.
    • Наша система содержала ненужные дупликации (needless repetition) и избыточную сложность ( needless complexity).
    • Ясность (opacity) выражения намерений у нас была достаточно низкой, хоть мы на ней и фокусировались. Частая ошибка программистов — когда мы запускаем новый продукт и долго не выходим в продакшн, мы делаем задел на будущее с целью экономии. Из-за этого мы не понимали, как именно должны быть устроены те части системы, которые были заглушками, что конкретно те элементы должны из себя представлять, и какими поведениями и зависимостями обладать. Дисфункции которые я перечислил выше мешали достижению ясности и в остальных частях системы.
    • Последний параметр viscosity я вынес отдельно. Это атрибут качества, который говорит о том, насколько система сопротивляется применению качественных архитектурных решений. Допустим, если тесты проходят час и ни о каком TDD речи быть не может, — это система с огромным показателем viscosity.

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

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

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

    Юнит-тесты — основной инструмент получения обратной связи от системы


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

    Давайте посмотрим на один тест.



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

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

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

    Что такое интегрированний тест?


    Я сейчас уйду от понятия Юнит-тест, потому это оно слишком расплывчато, каждый его понимает по-своему.

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

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

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

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

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

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



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

    Еще компоненты, которые не нужно читать — они просто для того, чтобы был понятен объем.








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



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

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

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

    Если бы мы писали так продакшн код, мы бы просто сошли с ума.

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

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



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

    Загнивание системы


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

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

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

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

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

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



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

    Альтернативный вариант.


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

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

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



    В результате с тестопригодной системой у нас есть больше времени для написания маленьких изолированных юнит-тестов, что приводит к качественному дизайну системы и уменьшает риск возникновения дефектов. Что насчет того что не производится «реальная работа»? Прежде чем я раскрою эту тему я хочу затронуть еще один момент. Моя точка зрения в том, что:

    Непрерывно поддерживать высокую продуктивность возможно только
    практикуя дисциплину Test Driven Development.


    Хотя противоположное мнение тоже довольно распространено (например, видео на это тему).

    Test Driven Development


    Test Driven Development — это дисциплина. Дисциплина подразумевает ограничение, которое мы на себя накладываем, применяя ее. Это не Red-Green-Refactoring, а ряд конкретных правил:

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

    Мы не просто пишем тест, а потом реализацию — не просто Red-Green-Refactoring, это дополнительные ограничения. Это — Test Driven Development — то, что позволяет поддерживать нашу систему в качественном состоянии и поддерживать максимально высокую продуктивность на протяжении длительного периода времени.

    Легаси код


    Согласно определению, данному Michaels C. Feathers, легаси-код — это код без юнит-тестов. Все просто. Я в своей практике замечаю прямую зависимость между отсутствием тестов и наличием огромнейшего числа проблем с дизайном системы, а также наличием интегрированных тестов и примерно такой же зависимостью с проблемами дизайна системы. Чем меньше маленьких изолированных тестов, тем больше проблем с дизайном системы.



    Когда тестов нет, это легаси.

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

    Легаси код — это код, который страшно изменять.

    Дейв Томас как-то сказал примерно следующее: «Я в некоторых случаях вообще не пишу тесты, я и так могу хорошо спроектировать качественную систему.»

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

    Роберт Мартин предлагает, чтобы в нашей профессии мы давали клятву, аналогичную клятве Гиппократа для врачей. Я привожу ее здесь для того, чтобы наглядно продемонстрировать, почему юнит-тесты, как минимум, важны для нашей индустрии, и, как максимум, Test Driven Development, как дисциплина, важна для нас, как программистов.

    Клятва программиста


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

    1. Я не буду создавать вредоносный код.

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

    2. Код, который я создаю, всегда будет моей лучшей работой.

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

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

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

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

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

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

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

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

    С юнит-тестами у меня такой проблемы нет. Практикуя Test Driven Development, проблем нет вообще — у меня код всегда корректен. Если там что-то ломается, это боль для меня как для профессионала, потому что я чувствую, что я где-то облажался, так как ситуация была под моим полным контролем. Лично для меня возникновение дефектов — это прямо серьезный вызов.

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

    Часто говорят, что Test Driven Development снижает продуктивность, или повышает ее, когда мы его долго практикуем. На самом деле он сохраняет продуктивность на постоянном уровне.

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

    Test Driven Development позволяет нам это делать.

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

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

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

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

    9. Я никогда не перестану изучать свое ремесло

    Маленькое упражнение


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

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

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

    Простейший пример




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

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

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



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

    Допустим, мы просим интерфейс, вернуть активных пользователей. Не достаточно просто проверить, что у нас в выходящем параметре массив пользователей. Нам нужны именно активные. Поэтому мы пишем тест, в котором мы спрашиваем: «Интерфейс, пожалуйста, дай нам активных пользователей!»

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



    Stub-ом возвращаем какое-то значение:

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

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



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

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

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

    Этого достаточно.

    The 4 Rules of Simple Design


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



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

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

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

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



    Я советую вам добавить этих ребят в друзья в соц. сетях:

    • Kent Beck — основатель движения экстремального программирования и дисциплины Test Driven Development.
    • J. B. Rainsberger — основатель компании JetBrains.
    • Robert Martin — уверен, вы про него знаете, если нет — дайте максимальный приоритет чтению его блога и книг.

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

    Что дальше?


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

    Скорее всего, вы столкнетесь с тем, что вы это сделать не можете, и если вы решите что-то с этим сделать, то советую вам посмотреть на эти ресурсы:

    https://cleancoders.com

    https://online-training.jbrains.ca/p/wbitdd-01

    Спасибо вам за приверженность в прочтении этой статьи.

    Вы можете написать Андрею в телеграмм https://t.me/akolomensky для того чтобы задать вопросы или спросить совета по инженерным, процессным или продуктовым кейсам, он обещал всем ответить, поэтому не стесняйтесь.

    Хотим заметить, разве не для того, чтобы соответствовать пункту клятвы «Я никогда не перестану изучать свое ремесло», и непрерывно совершенствоваться, мы с вами встречаемся на конференциях. Ведь интенсивный поток идей и случаев из практики, получаемый на конференциях, дает толчок к самосовершенствованию, как минимум на полгода. А чтобы, график развития не выглядел, как плохой график продуктивности, пора получить новый заряд — фестиваль РИТ++ будет 28 и 29 мая, а Highload++ Siberia 25 и 26 июня в Новосибирске. На последнюю до 30 апреля можно успеть подать заявки.

    Программу РИТ++ по направлениям, в том числе BackendConf, мы уже начали формировать и можно начинать прикидывать, что вам будет особенно полезно, и бронировать билеты. Например, в тему этой статьи заявка Юрия Бадальянца из 2ГИС на тему «Интеграционное тестирование микросервисов на Scala». Он тоже считает, что одного юнит-тестирования часто недостаточно, и необходимо применять и интеграционное тестирование.
    Конференции Олега Бунина (Онтико)
    331,00
    Конференции Олега Бунина
    Поделиться публикацией

    Комментарии 114

      +1

      Не смог понять, в чем цель поста… Ну да ладно, допустим, я тупой.


      Но вот конкретный вопрос (иду с конца).


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

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


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

        0
        4 Rules of Simple Design рассказывает о приоритизации. Без тестов безопасный рефакторинг невозможен, следовательно невозможно устранение дублирования и прояснение намерений.

        Вы правильно говорите, нужно двигаться по циклу Red — Green — Refactor.
          +1
          В TDD при написании тестов мы не можем оптимизировать код, потому что его ещё нет. Максимум, что у нас есть к моменту когда упадёт тест не по синтаксическим причинам — это интерфейсы (в широком смысле слова). Вот их мы и можем оптимизировать пока реального кода нет.
            +2
            Не смог понять, в чем цель поста…

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


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

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

            Называется «мозг». Мозг позволяет проверять систему на соответствие архитектуры тем или иным свойствам, а вот юнит-тесты — нет.
              +2
              Называется «мозг».

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


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

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

                0
                Юнит–тесты, написанные через TDD, позволяют моему мозгу принимать более качественные архитектурные решения, а также улучшать дизайн системы. Без них — анрил, особенно в командной разработке сложной системы.
                  +1
                  > Юнит–тесты, написанные через TDD, позволяют моему мозгу принимать более качественные архитектурные решения, а также улучшать дизайн системы.

                  А каким, собственно, образом? Смотрите, вы привели ряд параметров:
                  rigidity, fragility, immobility, needless repetition, needless complexity, opacity, viscosity
                  которые определяют качество вашей системы.

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

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

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

                    Проверьте, попробуйте написать юнит-тест на ваш текущий модуль, с полной изоляцией зависимостей. Любые сложности с которыми вы столкнетесь, будут связаны с одним из этих свойств. Обычно жесткость (сильная связанность) будет основной проблемой.
                      +1
                      > Юнит-тесты являются основой, на которой строится качественная архитектура.

                      Это утверждение, которое как раз и следует доказать. Каким именно образом простота юнит -тестирования приводит к улучшению (а не к ухудшению, например) архитектуры? Вы же сами привели 7 параметров качества архитектуры. И лишь один из этих параметров у вас с юнит-тестами улучшается. Как на счет остальных 6?

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

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

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

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

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

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

                      Видите, в чем проблема: тесты (в общем, и юнит-тесты даже в бОльшей степени) есть лишь обслуживающий элемент системы. И лучший обслуживающий элемент — это тот, который выполняет свои задачи максимально незаметно. Если вам тесты начинают диктовать архитектурные решения, то это сродни тому, что уборщица начнет диктовать распорядок дня работникам офиса, в котором она убирается. Что с такой работницей будет? Она пойдет на мороз. То же следует делать и с тестами. Если какой-то подход к тестированию плохо ложится на вашу систему — это проблема подхода. Не системы.
                        0
                        > Берешь и меняешь. Мы тут обсуждаем работу взрослых людей, профессионалов, или выезд детсада на пикник? Что это еще за «набраться храбрости»? «Не могу фичу внедрить, мне страшно»? Инфантилизм какой-то возведенный в абсолют, уж простите.

                        Обычно так: «Для того чтобы правильно имплементировать это требование нужно изменить структуру 2х модулей. Один модуль связан с адресацией, второй с контрактами. Есть интеграционные тесты, но они тестируют только ограниченное количество вариаций. Если я где-то накосячу, а QA не обнаружит дефект то у компани из-за меня будут проблемы, очень вероятно финансовые, а мне это не нужно. Сделаю-ка я чуть-чуть по-другому.»

                        Альтернативное мышление в виде «Я проведу рефакторинг, а QA пусть ищет дефекты» не лучше.

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

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

                        В западных странах в нормальную компанию на позицию Senior бекэнд разработки не возьмут без навыков написания юнит-тестов. В Silicon Valley это ~99% компаний практикующие гибкие подходы к разработке, большая часть также использует TDD.
                          +1
                          > Обычно так:

                          Замечательно. Человек осознает все риски и даже предлагает варианты решения проблемы. А какую альтернативу предлагаете вы? Самообмануться и убедить себя в том, что «если я где-то накосячу, а QA не обнаружит дефект, то у компани из-за меня не будет проблем, ведь волшебные юнит-тесты не дадут мне накосячить!11!»?

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

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

                          > В западных странах в нормальную компанию на позицию Senior бекэнд разработки не возьмут без навыков написания юнит-тестов.

                          Тут начать надо с того, что само понимание юнит-тестов у всех разное. Примерно в 50% случаев под юнит-тестами подразумеваются тесты вполне себе интеграционные.
                            +1
                            Да, именно. Я предлагаю жесткую дисциплину: ни одной строчки кода без быстрой, автоматизированной проверки его поведения. Я предлагаю писать код так чтобы QA обнаруживал 0 дефектов. TDD — единственная дисциплина которая может позволить реализовать это.

                            Это тяжело. Это очень тяжело. И это профессионально.

                            > Есть какие-то аргументы в пользу этой точки зрения?
                            Опыт индустрии за последние 25 лет. Попробуйте пройти bowling kata game, это будет как один из примеров как это работает.
                              0
                              > TDD — единственная дисциплина которая может позволить реализовать это.

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

                              > Опыт индустрии за последние 25 лет.

                              То есть, никаких логических соображений нет? Хорошо, давайте обратимся к опыту. Можно с ним как-то ознакомиться?
                                0
                                Логические соображения описаны мастерами в книгах и выступлениях, я привел их в статье, а также привел свои аргументы. Могу сконтачить с J.B.Rainsberger, Кент Беку можно написать в фейсбук.
                                  +1
                                  > Логические соображения описаны мастерами в книгах и выступлениях, я привел их в статье, а также привел свои аргументы.

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

                                  Я могу даже конкретный вопрос задать — каким образом вы обеспечите переработку графа зависимостей, сохранив семантику и разделение ответственностей модулей системы?
                                    +1
                                    Работа с Legacy это отдельный большой топик. Если тестов нет, то обычно применяется Golden Master и верхнеуровневые Acceptance тесты. Далее в зависимости от стратегии, самый простой вариант — писать новые классы с явным контролем зависимостей, изменяемые части системы постепенно выносить в отдельные классы и покрывать тестами.

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

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

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

                                    Тем самым мы:
                                    • Увеличили количество элементов в системе
                                    • Повысили количество абстракции
                                    • Лучше прояснили намерения
                                    • Вероятней всего обеспечили SRP и OCP
                                    • Сделали возможным написание изолированных тестов с разделением на тесты контракта и коллаборации


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

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

                                      Извините, но:
                                      1. не становится
                                      2. попытка подобрать более хорошие имена и названия привет, скорее, к обратному результату.

                                      Вы, видимо, не можете понять проблему, о которой речь. Я подробнее объясню. Вот вам дан граф зависимостей. Не обязательно это легаси система — может быть этот граф является просто условным иделаьным выражением требований к системе. Вы понимаете, что вот для такой системы вам будут нужны модули X, Y, и так далее, каждый из которых имеет определенную ответственность и, в силу этой ответственности, требует связи с некоторыми другими модулями.
                                      Потом оказывается, что данный граф плох для тестов (чисто математическое свойство, никак не связанное с устройством, назначением, функционированием вашей системы) и вам надо сделать из него другой граф, который этим свойством обладать не будет (чисто математическое преобразование).
                                      И вот тут-то и возникает проблема — после данного преобразования все ответственности окажутся размыты совершенно случайным образом, модули утратят всю семантику (вы даже не сможете дать им никаких названий, кроме условных foo42 или bar100500, так как ни один из модулей не будет делать ничего осмысленного) и в принципе это все превратится в невменяемый хаос. Естественно, что _чисто случайно_ данное преобразование может перевести ваш граф в некую так же осмысленную форму. Но, подчеркиваю — чисто случайно. Да, полученную систему будет легко тестировать. Но называть ворох бессмысленных модулей с именами вида foo42 «хорошей архитектурой» у меня язык не поворачивается, уж извините.
                                        +1
                                        Вы понимаете, что вот для такой системы вам будут нужны модули X, Y, и так далее, каждый из которых имеет определенную ответственность и, в силу этой ответственности, требует связи с некоторыми другими модулями.
                                        Потом оказывается, что данный граф плох для тестов (чисто математическое свойство, никак не связанное с устройством, назначением, функционированием вашей системы)

                                        С чего вы решили что оно никак не связанное?

                                          0
                                          > С чего вы решили что оно никак не связанное?

                                          А с чего ему быть связанным? Есть какие-то причины?
                                            0

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


                                            Вы же не пишете тесты с названиями типа


                                            "Если на вход подать 2 функция должна возарвтить 3"?

                                              0
                                              > Структура программы — это язык на котором вы их выражаете.

                                              Структура зависимостей программы — это просто направленный граф. Не понимаю, о каком языке вы говорите.
                                                0

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

                                                  0
                                                  Мы не говорили о программе, мы говорили о графе зависимостей:
                                                  > Потом оказывается, что данный граф плох для тестов (чисто математическое свойство, никак не связанное с устройством, назначением, функционированием вашей системы)
                                                  > С чего вы решили что оно никак не связанное?
                                                  > А с чего ему быть связанным? Есть какие-то причины?
                                                  > Разщумеется. Тесты это выражение требований. Структура программы — это язык на котором вы их выражаете. Если выражать требования на составленном вам языке неудобно, то язык неадекватен.

                                                  Я не понимаю, как последняя цитата отвечает на заданный вопрос в рамках контекста разговора.
                                                    0

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


                                                    Человек не может держать в оперативной памяти и оперировать больше определенного количетва объектов (кошелек Миллера). Поэтому он разбивает задачу на куски. Эти куски будут частью языка предметной области.


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


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


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


                                                    На хорошем языке должно быть удобно формулировать требования. см также Ubiquitous Language.

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

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

                                                      > На хорошем языке должно быть удобно формулировать требования. см также Ubiquitous Language.

                                                      Ну так, смотрите. У вас есть система, которая структурирована согласно требованиям предметной области. Вы пытаетесь написать для нее тесты. Получается плохо. Значит, в чем проблема? В способе формулирования тестов, очевидно. Ваши тесты — невыразительны. Способ, которым вы в этих тестах выражаете «требования к системе» очень плохо работает. Не позволяет вам формулировать данные требования в удобной форме. Значит, что? Значит, надо отказаться от этого способа написания тестов и использовать какой-то другой, такой, который позволит выражать требования к вашей системе проще и удобнее. Разве нет?
                                                        0
                                                        Ну так, смотрите. У вас есть система, которая структурирована согласно требованиям предметной области. Вы пытаетесь написать для нее тесты. Получается плохо. Значит, в чем проблема?

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

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

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

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


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

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

                                                              Хорошо, пусть структура неидеальна, но, допустим, близка к идеальной. Как мы выяснили из предыдущего примера (с идеальной структурой), фидбек тестов позволяет привести структуру к некоему виду, который строго хуже идеального. То есть (структура_с_тестами < идеальная_структура). Внимание, вопрос, где находится в этом упорядочении наша почти_идеальная_структура? Это (почти_идеальная_структура < структура_с_тестами < идеальная_структура)? Или, быть может, (структура_с_тестами < почти_идеальная_структура < идеальная_структура)?
                                                              А если мы введем еще совсем_не_идеальную_структуру, то будет она где? Насколько плохой должна быть структура изначально, чтобы быть гарантированно (ну или статистически достоверно) хуже, чем структура_с_тестами? Очевидно, что польза от ориентации по тестам будет только в этом случае. С-но, чтобы знать, применять тесты или нет, нам уже надо уметь оценивать качество структуры нашего кода. Но если мы умеем это делать — то тогда нам не требуется применять в качестве критерия тестов сами тесты, разве не так?

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

                                          Название должны ясно выражать намерения. Вы можете привести пример из класса, состоящего из 3-4-5 зависимостей, я сделаю из него класс с 1-2 зависимостями, причем они будут лучше выражать намерения чем те зависимости. Есть исключения, многопоточность, Observer-ы, но в целом задача решается.
                                            0
                                            > Если граф плох для быстрой, автоматизированной проверки, это является явным маркером сильного связывания.

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

                                            > Название должны ясно выражать намерения.

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

                                            > Вы можете привести пример из класса, состоящего из 3-4-5 зависимостей, я сделаю из него класс с 1-2 зависимостями

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

                                              Почему нельзя? Зачем тесту проверяющему правильность расчета (или интеграции расчетного модуля) проверять сразу и базу и логгирование.


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

                                                0
                                                > Почему нельзя?

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

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

                                                +1

                                                Можно разбить модуль, например так:


                                                • переносим получение из базы, расчёт и апдейт в отдельный модуль с тремя зависимостями, у оригинального становится четыре зависимости, да ещё он становится в идеале persistence ignorance.
                                                • переносим условие оповещения и оповещение в отдельный модуль с двумя зависимостями, оригинальный становится с тремя зависимостями.

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

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

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

                                                      0
                                                      Ваше описание мало похоже на описание бизнес-транзакции, поскольку отсутствуют ветки, описывающие "откат" транзакции в случае ошибок.

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


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

                                                        0

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


                                                        Уже давно при каком-то проектировании держу в уме "а как я это буду тестировать" :)

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

                                                          Можно-то и штаны через голову надеть, но… :)
                                                          Вводить событийную модель и вообще чего-то там серьезного вводить, лишь за тем, чтоб было _просто тесты писать_ — ну это лютый оверхед. Архитектура должна диктовать способ тестирования, а не способ тестирования — архитектуру.
                              +1
                              Видите, в чем проблема: тесты (в общем, и юнит-тесты даже в бОльшей степени) есть лишь обслуживающий элемент системы. И лучший обслуживающий элемент — это тот, который выполняет свои задачи максимально незаметно.

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

                                0
                                Это верно для интеграционных тестов, но не для модульных. Смотрите — клиент о вашей системе всегда знает только апи, и проблемы клиента — это проблемы плохого апи.
                                Модульные тесты же — это особый клиент, который знает не только об апи, но и о реализации, и в этом-то и состоит проблема данного особенного клиента. Другие клиенты такой проблемы иметь просто не могут от слова никак.
                                  0

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


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


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

                                    0
                                    > В целом, модульные тесты не должны знать деталей реализации

                                    Не знать детали реализации = не использовать моки и стабы. Это два эквивалентных требования. Конечно, если моков и стабов нет, то все ок. Но тогда:
                                    1. Либо ваш тест не модульный
                                    2. Либо в нем нет зависимостей в принципе

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

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

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

                                      Завивит от того, что вы называете моком. Если SUT требует ILogger то передать ему InMemoryLogger а потом узнать у InMemoryLogger что в нем лежит — не будет прибитостью к реализации

                                        0

                                        Это будет прибитостью к контракту ILogger. Причём в общем случае в изолированных юнит-тестах даже не нужно узнавать у InMemoryLogger что там лежит, достаточно NullLogger. InMemoryLogger приберечь для интеграционных тестов, проверяющих правильно ли SUT интегрируется с ILogger.

                                          0

                                          ILogger это часть интерфейса SUT, а не реализация. Так что прибитость к ILogger это ok.


                                          SUT не может интегрироваться с ILogger так как он интерфейс. А не другой юнит — это просто описание контракта и все.

                                            0

                                            У меня дополнение было, а не опровержение :)


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

                                              0

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


                                              Так как Y не является частью системы.


                                              А вот, например шор определяет так:


                                              James Shore


                                              Focused Integration Tests

                                              Unit tests aren't enough. At some point, your code has to talk to the outside world. You can use TDD for that code, too.

                                              A test that causes your code to talk to a database, communicate across the network, touch the file system, or otherwise leave the bounds of its own process is an integration test. The best integration tests are focused integration tests that test just one interaction with the outside world.
                                                0

                                                Не только то, что X соблюдает свой контракт, но и то, что при работе X использует Y его в соответствии с контрактом последнего.


                                                И как тогда назвать тесты, которые проверяют взаимодействие модулей друг с другом в рамках одного процесса?

                                                  0

                                                  В юнит тесте мы проверяем X на то, что если мы передали IY, то он делает то, что должен. IY это что-то, соблюдающее контракт IY, в частности Y.


                                                  Если мы кладем двигатель на стенд и подаем туда топливо из тестового стенда — это юнит тест двигателя.


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


                                                  И как тогда назвать тесты, которые проверяют взаимодействие модулей друг с другом в рамках одного процесса?

                                                  Фаулер считает что эти понятия размытые


                                                  Sociable and Solitary
                                                  Some argue that all collaborators (e.g. other classes that are called by your class under test) of your subject under test should be substituted with mocks or stubs to come up with perfect isolation and to avoid side-effects and a complicated test setup. Others argue that only collaborators that are slow or have bigger side effects (e.g. classes that access databases or make network calls) should be stubbed or mocked.

                                                  Occasionally people label these two sorts of tests as solitary unit tests for tests that stub all collaborators and sociable unit tests for tests that allow talking to real collaborators (Jay Fields' Working Effectively with Unit Tests coined these terms). If you have some spare time you can go down the rabbit hole and read more about the pros and cons of the different schools of thought.

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


                                                  Talking about different test classifications is always difficult. What I mean when I talk about unit tests can be slightly different from your understanding. With integration tests it's even worse. For some people integration testing is a very broad activity that tests through a lot of different parts of your entire system. For me it's a rather narrow thing, only testing the integration with one external part at a time. Some call them integration tests, some refer to them as component tests, some prefer the term service test. Even others will argue, that all of these three terms are totally different things. There's no right or wrong. The software development community simply hasn't managed to settle on well-defined terms around testing.

                                                  Don't get too hung up on sticking to ambiguous terms. It doesn't matter if you call it end-to-end or broad stack test or functional test. It doesn't matter if your integration tests mean something different to you than to the folks at another company. Yes, it would be really nice if our profession could settle on some well-defined terms and all stick to it. Unfortunately this hasn't happened yet. And since there are many nuances when it comes to writing tests it's really more of a spectrum than a bunch of discrete buckets anyways, which makes consistent naming even harder.
                                        0

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

                                          0
                                          > Чтобы использовать моки и стабы зависимостей тестируемого модуля не нужно знать детали его реализации в общем случае.

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

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

                                              0
                                              > Если использование конкретной зависимости является требованием

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

                                              > Если такого требования нет, то не надо его тестировать.

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

                                              > В тесте, который проверяет корректность расчета, на надо передавать механизм оповещения

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

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


                                                1. См выше sociable unit test
                                                2. Еще есть проблема с определением того, что такое "зависимость" — почему мы не мокаем System.String?

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


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


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


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

                                                  0
                                                  Еще есть проблема с определением того, что такое "зависимость"

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


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


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

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

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

                                                    Если интерфейс метода модуля Y диктует модуль X, то это зависимость Y от X, а не наоборот. Скажем, если вы пишите функцию маппинга одного значения на другое для применения в Array.map, то это ваша функция зависит от ожиданий Array.map, а не Array.map от неё. ;)


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

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


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

                                                      0
                                                      Если интерфейс метода модуля Y диктует модуль X, то это зависимость Y от X, а не наоборот.

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


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

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


                                                      ему всё равно клиент должен предоставить какую-то информацию о базе

                                                      Это нарушение SRP. С чего у вас клиент отвечает за конфигурацию зависимостей? Естественно, есть случаи, когда за нее должен отвечать клиент. Но это исключение. Давайте рассмотрим то, что происходит в большинстве случаев — клиент использует модуль, зависимости которого уже настроены некоей условной третьей стороной. Например, если клиент пользуется услугами фабрики для производства объекта-сумматора, то он может и не знать о форме, скажем так, зависимости. Он может передавать какой-нибудь там enum с квалификатором типа логирования и уже фабрика создать и подставит нужный логер. С точки зрения клиента никакого объекта логера и не существует, он не знает как там это реализовано будет в сумматоре. Может там просто портянка ифов, которая в заивимости от енама лог пишет, а не фабрика инжектит то, что требуется?

                                                        0
                                                        Например, если клиент пользуется услугами фабрики для производства объекта-сумматора, то он может и не знать о форме, скажем так, зависимости.

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


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

                                                          0
                                                          Кстати, тогда у вас есть модуль "сумматор-вместе-с-фабрикой" которым можно сносно пользоваться. А сумматор отдельно не реюзабелен.

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

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

                                                      Отлично. Должен ли я мокать System.String, чтобы вы назвали мой тест юнит тестом?


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

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


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

                                                        0
                                                        Отлично. Должен ли я мокать System.String, чтобы вы назвали мой тест юнит тестом?

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


                                                        Если что-то снаружи абсолютно невидимо, то оно мне не будет мешать тестировать.

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

                                                          0
                                                          С точки зрения сферического юнит-тестирования в вакууме — безусловно, да

                                                          Ok. Хорошо, мы наверное говорим о разных юнит тестах. Давайте юнит тест по вашему определению называть сферическим, а юнит тест по Фаулеру (см выше в т.ч. sociable) практическим.


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

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


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

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

                                                            0
                                                            Ok. Хорошо, мы наверное говорим о разных юнит тестах. Давайте юнит тест по вашему определению называть сферическим, а юнит тест по Фаулеру (см выше в т.ч. sociable) практическим.

                                                            Давайте. Можно просто определиться, что дальше под юнит-тестами мы подразумеваем sociable (раз уж ни вы, ни я solitary не пользуемся, хоть это и наиболее традиционный и распространенный вариант).


                                                            Фаулеровским юнит тестом оно будет.

                                                            Ну проблема в том, что подход Фаулера в 2018 не является традиционным. Сейчас, когда говорят "юнит-тест" (без уточнений), то подразумевают mockist style (по Фаулеру), а сам Фаулер приверженец classical style (ну, того, что он называет classical style). Если мы используем нестандартную терминологию, то надо заранее договариваться, да.

                                                              0
                                                              Ну проблема в том, что подход Фаулера в 2018 не является традиционным.

                                                              На основании чего вы сделали такой вывод?

                                                                0

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

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

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

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


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

                                  +1

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

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

                                    Это распространенное заблуждение. Верно будет, что труднее менять и понимать код, который плохо соответствует структуре предметной области. Если предметная область не предполагает сложных взаимосвязей — то и в вашей системе их не должно быть. Но если предполагает — они быть как раз должны, скорее всего.
                                    Понимаете, в чем дело? Структура взаимосвязей между модулями должна определяться нуждами предметной области, а не неким абстрактным математическим свойством. Если вы сформировали хорошую, годно мапающуюся на предметную область структуру, а потом видоизменяете ее, чтобы было «проще тестировать» — это неправильно. Правильно выгнать эту уборщицу на ветер и выбрать другую, которая будет удовлетворять вашим требованиям.
                                      0
                                      Что определяет качество выражения предметной области в коде?
                                        +1

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


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


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

                                        Вот тут хотелось бы пример

                                          0
                                          > Вы можете их слушать и не слушать.

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

                                          > То есть если вам трудно тестировать надо понять почему

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

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


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

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

                                              0
                                              > Я скорее всего пришлушаюсь к совету уборщицы

                                              Нет, вы не поняли. Она просто скажет вам, что отныне вы работаете не с 09 до 18, а с 08 до 11, потом с 16 до 19 и далее с 01 до 04. Потому что ей по неким ее причинам так удобнее убираться будет.

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

                                              Возможно. Но с тестами и советами уборщицы это никак не связано.
                                                0

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


                                                Вместо того, чтобы тупо делать мок надо сначала подумать.


                                                о с тестами и советами уборщицы это никак не связано.

                                                Примеры связаны с тестами — они их иллюстрируют. Проиллюстрируйте пожалуйста.


                                                P.S. Хинт: если поставить галочку MarkDown внизу сообщения, то хабр будет фофрмлять знак > в начале строки как цитату.

                                                  0
                                                  Примеры связаны с тестами — они их иллюстрируют. Проиллюстрируйте пожалуйста.

                                                  Ну вот смотрите, выше рассматривали пример с разбитием зависимостей. Я описал модуль с 6 зависимостями, который плох, VolCh предложил логичный вариант разбиения на три модуля, каждый из которых имел по 2-3 зависимости. Ему не пришлось писать тесты и получать от них какой-то "фидбек". Он просто понимает, что 6 зависимостей — это дохера, а 2-3 — не дохера. Не надо писать тестов, чтобы определить, что зависимостей много. Надо просто тупо взять и эти зависимости посчитать. Вот и весь сказ. Есть четкая, вычисляемая метрика. Вместо субъективного "удобно писать тесты". Кому-то, блин, будет удобно, а кому-то — нет.


                                                  P.S. Хинт: если поставить галочку MarkDown внизу сообщения, то хабр будет фофрмлять знак > в начале строки как цитату.

                                                  thx

                                                    0

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


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

                                                      0
                                                      Так можно не увидеть зависимость — тест покажет, что она реально нужна.

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

                                                        0

                                                        Какой программой вы пользуетесь, как часто ее запускаете и как анализируете ответы?

                                                          0
                                                          Какой программой вы пользуетесь, как часто ее запускаете и как анализируете ответы?

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


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

                                                            0

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


                                                            • Потому что в эту минуту я как раз изобретал новый способ, как перебраться через забор. Рассказать?
                                                            • Расскажите, пожалуйста, — учтиво сказала Алиса.
                                                            • Я расскажу тебе, как я до этого дошел, — начал Рыцарь. — Понимаешь, я сказал сам себе: "Все затруднение заключается в ногах. Голова-то до верха забора достает,- а вот ноги"… Так вот: сначала я кладу голову на верх забора — и голова моя, значит, на должной высоте; потом я становлюсь на голову и наверх поднимаются мои ноги, — значит, и они на должной высоте. Понимаешь? И тогда я уже по ту сторону забора.
                                                            • Пожалуй, вы очутитесь на той стороне, если вы это проделаете, — сказала Алиса, — но вы не думаете разве, что это будет довольно больно?
                                                            • Я еще не пробовал! — серьезно сказал Рыцарь, — так что наверное сказать не могу. Но я боюсь, действительно, немножко будет больно.


                                                            Эта мысль так, очевидно, огорчила его, что Алиса поспешила переменить предмет разговора.
                                                              0
                                                              Не встречалось ли в вашей практике такое, что теоретически казалось применимым а на практике возникали непреодолимые препяттвия?

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


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

                                                              Это рациональное зерно не относится к предмету разговора, т.к. мое предложение, фактически, математически верифицируемо. Не требуется проверять на практике, что 2+2=4. У языка программирования есть заданные синтаксис и семантика, эти синтаксис и семантика позволяют распарсить программу на данной языке и расчитать определенные метрики.

                                                                0
                                                                апример, попытка вместо прямого измерения метрики делать косвенные измерения при помощи тестов

                                                                Это ж не ваш опыт, как я понял, а теоретизирование.


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

                                                                Вам уже выше написали, что нужны не все связи.

                                                                  0
                                                                  Вам уже выше написали, что нужны не все связи.

                                                                  Не все, а какие? То есть если у меня 20 связей, но они относятся к тем, что "считать не нужно", то это ок все, нормальная структура?

                                                                    +1

                                                                    Да, конечно. Посчитайте совсем все связи в каком-то коде который считаете нормальным. Включая System.String, System.Integer, MySimpleIntToStringCache и прочее.

                                                                      0

                                                                      А при чем тут System.Strings, System.Integer и т.д.? Давайте уж учтем, что это вообще артефакты языка и в большом количестве языков они в принципе связью не являются.

                                                                        0
                                                                        А при чем тут System.Strings, System.Integer и т.д.?

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

                                                                          0
                                                                          Дайте определение связи.

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

                                                                            0

                                                                            То есть вы даже не можете определить, чего собственно пытаетесь достичь, при этом выше приводите "математический подсчет" в качестве инструмента?

                                                                              0
                                                                              > То есть вы даже не можете определить, чего собственно пытаетесь достичь

                                                                              Почему не могу? Могу, просто в каждом случае это будут разные вещи. Которые можно посчитать.
                                                                                0

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

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

                                                                      Скажем, на PHP можно не считать за архитектурную зависимость \Doctrine\Collection даже если ею всё приложение пронизано на всех уровнях. А вот \Doctrine\ORM уже полноценная архитектурная зависимость, которую желательно использовать как можжно более точечено, сведя зависимость всего приложения от неё к зависимости чётко перечисленных модулей.
                                                                        0
                                                                        В метриках, оценивающих архитектуру приложения по количеству связей между модулями, как правило не нужно считать вызовы стандартной библиотеки языка, как минимум той её части, которая не зависит от внешнего мира и прочего io. так же можно не считать даже связи с какими-то своими или стороними библиотеками, тоже мало зависящими от внешнего мира.

                                                                        Мы вроде и не собирались считать.

                                                                          0
                                                                          Есть связь или нет — вырожденный случай счёта.
                                            +1

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


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

                                              0
                                              > Сильносвязанный код трудно менять. Это не заблуждение, а факт. Код, моделирующий сложную предметную область, трудно менять просто потму что предметная область сложная. Связанность кода не может быть меньше связанности в предметной области.

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

                                              То есть:
                                              > Стремление к простым тестам мешало бы вводить подобные технические связи.

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

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


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


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

                                                  0

                                                  Тесты, их сложность служат индикатором количества "паразитических" зависимостей. Если открываем тест взаимодействия одной сложной штуки с другой сложной штукой и видим что там мокается (а то и подключается реальная) база, то это сигнал, что зависимость гораздо сильнее, чем описана экспертом. Если для тестирования подсчёта суммы в чеке нам нужно создавать заказ, а чек создаётся автоматически, то это тоже сигнальчик, если эксперт не заявлял, что чек должен создаваться только на основании заказа, а говорил, что на определенном этапе на базе заказа должен создаваться чек. Мы на пустом месте создали связь 1:1 чека и ордера вместо, хотя бы, 0..1:0..1

                                                    0
                                                    > Тесты, их сложность служат индикатором количества «паразитических» зависимостей.

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

                                                      Так стандартные метрики не различают зависимости реально нужные, истекающие из предметной области и(или) используемой инфраструктуры и "паразитные". А простота написания клиентского кода может быть обманчивой — просто создать объект и дернуть один из его 100500 методов? Просто. Но вот предсказать что будет если предварительно дернуть один из 100499, а потом этот может оказаться уже очень непросто.

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

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


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

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


                                                        Ну и вы пропустили один пункт — почему мы ориентируемся по сложности написания тестов (то есть по сложности написания моков и стабов, де-факто), а не по сложности написания клиентского кода?

                                                          +1

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

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

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


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

                                                            "Петька, приборы! 300! Что 300? А что приборы?" Расчёт зависимостей не покажет нам доменная это зависимость или техническая.


                                                            Ну и вы пропустили один пункт — почему мы ориентируемся по сложности написания тестов (то есть по сложности написания моков и стабов, де-факто), а не по сложности написания клиентского кода?

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

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

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


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

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

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

                                                                  Но это же плюс, а не минус, разве нет?


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

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


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


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

                                                                    0
                                                                    >Но это же плюс, а не минус, разве нет?

                                                                    Что из утверждений? )

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

                                                                    Тесты — один из таких модулей-клиентов.

                                                                    > То есть, ориентируясь на тесты, вы гарантированно сделаете хуже, тут вообще никаких вариантов нет.

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

                                                                      Плюс, когда тесты пишут не те люди, что пишут тестируемый код.


                                                                      Тесты — один из таких модулей-клиентов.

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


                                                                      Гарантированно хуже чем в какой альтернативной ситуации?

                                                                      Чем в ситуации, в которой вы при дизайне кода не будете ориентироваться на тесты

                                                                        0
                                                                        Есть плюсы и когда тесты пишет автор кода.

                                                                        А если я не буду ориентироваться на тесты, но буду ориентироваться на плохие практики, антипаттерны, обфускацию и т. п., то всё равно у меня будут код грантированно лучше, чем при ориентации на тесты?
                                                                          +1
                                                                          А потом бац! и ВНЕЗАПНО оказывается что в реальности клиенты будут работать совсем по другой схеме.

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

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

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

                                            Самое читаемое