Контравариантные тесты

Привет, Хабр! Представляю вашему вниманию перевод статьи Test Contra-variance


От переводчика: честно говоря, выбор слов ко-/контравариантность, по отношению к дизайну тестов, немного странен. Семантика конечно прослеживается, но весьма метафорична. Скорее всего, просто для красного словца и заголовка, привлекающего внимание, поэтому не сильно придирайтесь. В остальном — прекрасная заметка на тему TDD в формате диалога. Рассказано почему TDD это так больно, как сделать из юнит-тестов приятный инструмент и не относится к ним как к обязательно ломающемуся насилию над свободой самовыражения.


Ты пишешь юнит-тесты?


Конечно!

Сначала тесты, а потом код?


Да, я следую трем правилам TDD.

А есть ли разница в структуре модулей тестов и кода?


Я делаю один тестовый класс для каждого класса в коде.

То есть, если класс в основном коде называется User, то у тебя будет тестовый
класс с именем UserTest?


Да, почти всегда.

Получается что структура тестов ковариантна структуре кода?


Ну, полагаю что так.

Значит ты привязываешь структуру тестов к коду?


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

И когда рефакторишь структуру классов кода, не трогая поведения, то
тесты ломаются?


Да, это правда.

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


Это почему?

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


Хорошо, исходя из определения, тогда это действительно не рефакторинг.

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


Да-да, и что с того?

Это пример Проблемы Хрупких Тестов.


Проблема Хрупких Тестов?

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


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

К сожалению это обычная реакция.


И что делать?

Структура тестов должна быть контравариантна коду.


Контравариантна?

Да, структура тестов не должна отражать структуру кода. Из факта, что
какой-нибудь класс называется X, не должно следовать появление теста с
именем XTest.


Но постойте, это не по правилам!

Каким правилам?


Для каждого класса должен быть соответствующий тест.

Нет такого правила.


Как нет? Я точно о нем читал.

Не все что ты читаешь является правилом.


Ладно, если структура тестов должна быть контравариантной, то как ее такой сделать?

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


Это очевидно, software design 101 рассказывают о том же.

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


Мысль понятна, согласен.

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


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

Именно. Связанность тестов и кода должна быть минимальной.


Стоп! Но тесты и код должны быть связаны, так как описывают одно и тоже
поведение.

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


Можно пример?

Предположим я начинаю писать новый класс. Назовем его X. И я делаю новый тест
с именем XTest.


Но ты же только что сказал: "не надо так делать".

Не опережай события, мы только начали. По мере добавления новых тестов в XTest,
я добавляю новый код в X.


И рефакторишь код!

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


И ты рефакторишь тесты, правильно?

Точно! Я смотрю на связанность между XTest и X и работаю над ее минимизацией.
Это можно сделать через добавление параметров в конструктор X или повышение
уровня абстракции аргументов. Или даже введение полиморфического интерфейса
между XTest и X. (1)


И все это только для того чтобы написать тест?

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


Хорошо, но структура тестов все равно повторяет структуру кода. X и XTest
никуда не делись.

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


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

Правильно! Структурная симметрия нарушена, но я собираюсь сломать еще больше.


Это как?

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


Но нового теста ты не пишешь?

Да! Все больше и больше функций будет выделено, все больше классов будет
обнаружено. И через некоторое время у нас будет целое семейство классов,
сидящее за простым API X.


И все они будут покрыты XTest.

Верно! Структура будет практически полностью независима. А еще API X постепенно
станет настолько чистым и абстрактным, что будет минимально связан
с клиентами, включая XTest.


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

Подумай что происходит в процессе разработки X. Как это отражается на XTest?


Ну, тестов будет все больше и больше, а интерфейс с X будет все чище и
абстрактнее.

Правильно, теперь повтори первую часть еще раз.


Тестов будет все больше и больше?

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


Полные требования к поведению X API.

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


Конечно, понимаю.

Но что случается с классами, спрятанными за X API? Что делает каждый хороший
дизайнер чтобы справится с растущим списком требований?


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

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


И как это влияет на связанность поведения?

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


И это уменьшает связанность?

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


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


Ты хочешь сказать, что тесты неполны?

Конечно! Это просто непрактично описывать абсолютно все. Так что произойдет,
если мы будем постепенно увеличивать общность кода, пока любые возможные
тесты не начнут проходить?


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

Вот тебе и ого. Еще на засыпку — процесс обобщения это процесс развязывания,
мы развязываем обобщая!


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

Правильно, можешь пересказать суть?


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

Хорошо, а что насчет поведения?


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

Отлично, думаю ты все понял!


Вперед, к победе, с контравариантными тестами!



(1) Тут Дядю Боба немного занесло. Скорее всего он хотел рассказать про один из этапов написания тестов/кода — становление публичного интерфейса. В зависимости от опыта, разработчик может или сразу знать (спроектировать в голове) каким должен быть интерфейс, или это будет постепенное морфирование тестов и кода в нужную сторону. На этом этапе правки будут значительными с обеих сторон. Новички бросают TDD еще и потому, что пока не умеют делать хорошие интерфейсы и им приходится переписывать в два раза больше кода. Но это отличная практика! Двигает в сторону посидеть с блокнотом, порисовать и подумать.


(2) Ни в коем случае не руководство к действию! Это просто пример. Далеко не все классы можно разделить по этому признаку.


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

Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 64
  • +5

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

    • +5
      (сразу извиняюсь за оффтоп, но мысль приходится выкладывать полностью.)
      Это к сожалению проблема практически всей айтишной литературы. Особенно касающейся архитектуры. Провести дидактическую редукцию сложной концепции дело непростое, и людям просто лень. А может они и не умеют этого делать.
      Я очень критически отношусь к такой «лени» — «не можешь привести пример, значит это пустая болтовня». «Пример получится слишком большим?» — ничего мы подождем.

      Как я отличаю хорошую литературу от плохой? В хорошей не пишут «мы вернемся к этому чуть позже» — ведь обычно не возвращаются. В хорошей дают развернутые примеры, и не противоречат установленным догмам, типа не стоит называть тест XTest, но для примера, чтобы развить мысль, называют свой пример ХТest. Это никак не оправдано, даже если это запись разговора по памяти, то всегда есть такой этап как редактирование. И в нужном месте нужно изменить достоверность диалога в угоду дидактической удобоваримости.

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

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

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

        • +2
          По опыту: люди, которые работали на запоротом проекте, обтерпелись и начали вникать во все перипетии, практически никогда не смогут его хорошо переписать

          Смогут, просто им на это не дадут времени.

          • 0
            Смогут, ибо включат время в оценки задач.
            • 0
              Ну, выкатят они оценку: «3 месяца, чтобы разобраться, кратко записать, как это сейчас работает и выставить оценки на переписывание». Там им никто не даст делать задачу такую дорогую, с которой непонятно, будет ли польза в дальнейшем.
              • 0

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

                • 0
                  Это да, но
                  1. Никто не даст 3 месяца «разбираться».
                  2. Некоторые места надо сильно переписать (в т.ч. структуру БД, интерфейсы взаимодействия с другими системами и пользователем), т.е. это не тот рефакторинг, который разработчик может сделать без тысячи согласований.
                  • 0

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

                    • 0
                      По мелочи так можно править, но в ветке обсуждение как «хорошо переписать».
                      • 0

                        Это и есть переписать. Только не за один раз. Когда куча мелочей будет решена, последующие задачи будут легче.


                        И те вещи которые меняются, будут потихонечку переписаны.

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

            Обычно рекомендуют делать код лучше понемножку.
            http://jbazuzicode.blogspot.com/2017/08/refactor-lot-but-only-when-its.html


            Don't just refactor for fun. Refactor in service of delivering business value. If there's some terrible code that you never need to touch, then there's no reason to change it. Leave it terrible.

            So, when are the right times to refactor?
            • When you're changing code. Refactor to make it well-designed for its new purpose.
            • When you're reading code. Every time you gain some understanding, refactor to record that understanding. Lots of renames.
            • When you're afraid of code. If there's code you should be changing or reading, but you avoid because it's such a mess, then you should definitely refactor it.


            Note that this refactoring is a small improvement each time, not a dramatic major rewrite. >The goal is Better, not Good.
          • +1
            Статьи Дяди Боба в форме диалога это скорее притчи, которые наталкивают на правильные мысли. Они не про практичность. У меня каждый такой диалог закреплял интуитивный опыт, «знаю, а сказать не могу», в виде набора формализмов, которыми уже можно пользоваться и ссылаться при принятии решений.
          • 0
            Как по мне если делать тесты сложными (Применять наследования, добавлять различные параметры в конструктор и т.п.) то весь смысли в них теряется. Вместо простых изменений, не важно каких, будь-то изменения бизнес логики или структуры вам придется вспоминать или заново читать(код) архитектуру тестов. И это как по мне приведет только к удорожанию написания и поддержки тестов
            • +1

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


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

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

            • –1
              ТеэDеD. И майевтика, и тесты.
              • +3

                Уважаю Роберта Мартина.
                Но в этот раз как-то неубедительно.


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

                А что мы имеем по факту в итоге? Класс Х и тест-класс XTest.
                Тот факт, что у нас нет тестов на вспомогательные классы, которые используются в Х, означает просто, что в этих классах нет логики достаточно сложной для того, чтобы ее стоило тестировать отдельно. Как только они усложнятся — появятся отдельные тесты на них (просто потому, что тестировать их через общий X Api непрактично.


                В то время как тесты становятся все более определенными, код будет все более
                общим

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


                В итоге, из статьи лично мне неясно — а что делать-то надо?

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

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

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

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

                      Увы, к формальному ответу индустрия еще не пришла. Думать и пробовать, пытаться ужимать интерфейсы до минимума, меньше входных точек — меньше вещей, за которыми нужно следить.
                    • 0
                      Они усложняются как следствие дополнительных требований к X API, и тестировать их естественно через X API.

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


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

                      • 0
                        > Количество тестов растет лавинообразно, если все тестировать через внешний интерфейс, и не тестировать зависимости по отдельности.

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

                        > Не говоря уже о том, что сами тесты становятся сложными.

                        Про это уже было написано выше. Сложные тесты — следствие ошибок в проектировании системы, нарушение принципов SOLID.

                        > Тестировать через общий интерфейс все еще можно, но уже непрактично.

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

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

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

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


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


                          Как проверить, что валидация отрабатывает корректно на разных входных данных?
                          Написать тест на класс Валидатор? Или написать тест на класс ВебКонтроллер, и тестировать валидацию через публичный интерфейс — http запросы?

                          • 0
                            Выделяете валидатор в отдельный интерфейс IValidator и передаёте его в качестве параметра IWebControllerFactory.Create, делая, таким образом, валидацию и обработку запроса независимыми. Если же валидатор — исключительно внутренняя сущность вебконтроллера, например, из-за его тривиальности, то придётся тестировать его через внешний интерфейс.

                            Другой пример — реализация ассоциативного массива. У него простой и понятный внешний интерфейс, но реализация под капотом может быть разная. И тестировать правильность построения массивов с хэшами нет никакого смысла.
                            • +1

                              Ок, то есть в итоге у нас есть тест ValidatorTest и есть отдельный тест контроллера ControllerTest. Структура тестов повторяет структуру кода.

                        • +1
                          Количество тестов растет лавинообразно, если все тестировать через внешний интерфейс, и не тестировать зависимости по отдельности.

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

                          • 0

                            Что такое компонента? В статье призывается не создавать новых тестов когда мы выносим что-то в отдельный класс. Этот отдельный класс это компонента или нет? По какомй криетрию?

                            • 0
                              Что такое компонента?

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


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


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

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

                              • +1
                                Компонента предоставляет интерфейс, модель данных и инварианты над моделью.

                                Это похоже на описание хорошего класса :)


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

                                Почему одна компонента не может быть деталью реализации другой?

                                • 0
                                  Почему одна компонента не может быть деталью реализации другой?

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

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

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


                                    Иногда я не тестирую очень тесно связанные компоненты (например итератор отдельно от коллекции).

                                    • 0
                                      > Может, но если публичный API подкомпоненты используется только в родителе, то смысла тестировать ее нет.

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

                                      Давайте на примере: делаем хеш MD5, первая ссылка в гугле tls.mbed.org/md5-source-code

                                      Тестируем только публичный API, запускаем тест, попадаем на mbedtls_printf( «failed\n» ).

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

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


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


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

                                        • 0
                                          > Если у нас есть, допустим, компонента которая делает расчет и записывает его в БД

                                          Это две компоненты в чистом виде) Независимые требования.
                                          • 0

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

                                • +1

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


                                  Тем не менее, все равно выходит, что тесты в целом следуют за структурой классов.

                              • 0
                                Если бы не было тестов или они тестировали конкретные «юниты» — никогда бы не взялся за такой рефакторинг.

                                Были ли тесты хорошим кодом? Содержали ли они дублирование?

                                • 0
                                  Были ли тесты хорошим кодом? Содержали ли они дублирование?

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

                                  • 0

                                    Я не очень понял. Нельзя как-нибудь поподробнее объяснить?


                                    1-1 обертки над api в которых создавались нужные зависимости

                                    Мне кажется, 1-1 обертки это и есть дублирование, нет?

                              • 0
                                означает просто, что в этих классах нет логики достаточно сложной для того, чтобы ее стоило тестировать отдельно

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


                                В итоге, из статьи лично мне неясно — а что делать-то надо?

                                Пишите тесты для контрактов, а не реализаций.

                                • +1
                                  Получается у этого класса нет единственной ответственности.

                                  Неправда. Сложная логика, требующая тестирования <> множественная ответственность. Вот банально валидатор. Сначала форма была простая, валидацию тестировали через контроллер. Потом форма все усложнялась и усложнялся валидатор. И тестировать валидатор через контроллер стало непрактично — появились отдельные тесты на валидатор.


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

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

                                  • 0
                                    Неправда. Сложная логика, требующая тестирования <> множественная ответственность.

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

                                    • 0
                                      Правда. TDD требует покрывать отказными тестами любой код.

                                      А речь шла не про TDD, а про нарушение SRP. Наличие/отсутствие тестов ничего не говорит про соблюдение/нарушение SRP конкретным классом.


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

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

                              • +1
                                это BDD. Мы тестируем поведение конкретного класса через публичные методы и даем ему дернуть все внутренние.
                                Юнит тесты — все же должны быть изолированными тестами.

                                Это просто непрактично описывать абсолютно все

                                Но большую часть описать — вполне нормально.
                                • +1
                                  это BDD

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


                                  Юнит тесты — все же должны быть изолированными тестами.

                                  Кому должны?) В статье раскрыта суть проблемы такого подхода. Юнит тесты, которые тестируют отдельные приватные функции, только добавляют проблем.


                                  Покрытие компоненты/модуля через публичный API позволяет эффективно проводить рефакторинги в дальнейшем.

                                  • 0
                                    Покрытие компоненты/модуля через публичный API позволяет эффективно проводить рефакторинги в дальнейшем.

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

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

                                      • 0
                                        если мы к примеру добавим внутренний метод, нам придется в тесте тестирующим публичные методы создавать еще 3-4 тест-кейса. Вместо тестирования одного метода. Итого — желаемой гибкости не наблюдается.
                                        • 0
                                          Если внутренний метод никак не затрагивает внешний интерфейс, то зачем его тестировать?
                                          • 0

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

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

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

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

                                • 0

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

                                  • +1
                                    И структура тестов и структура кода должна быть отражением требований.

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

                                    • 0

                                      Имхо.


                                      Какая угодно реализация может быть. Хорошая реализация внутри должна быть отражением предметной области. (см. Ubiquitous Language)


                                      То есть микросервисы на марсе должны быть отражением какой-то части требований.

                                      • +1
                                        > Хорошая реализация внутри должна быть отражением предметной области.

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

                                        Например, возьмем rate limiting. У нас есть объект который нужно лимитировать, период времени, количество вызовов. Хорошей реализацией будет просто голый redis + интерфейс с элементарными вызовами. Знает ли redis про нашу предметную область? Да нет конечно, но при этом он хорошая реализация.
                                        • 0

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


                                          Ваши тесты могут покрыть только требование "rate limiting" чего бы это ни значило. Возможно у вас будет hexagonal artchitecture со своими focused integration tests по rate limiting и отдельно протестированным тестовым адаптером. Вам не надо будет повторять тесты redis в своих тестах.

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

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


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


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


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


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


                                            Иначе вам придется повторять одни и те же вещи что в требованиях, что в коде, что в тестах.

                                    • 0
                                      до момента пока не сможем написать проваливающийся тест

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

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