Привет, Хабр! Представляю вашему вниманию перевод статьи 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) Может показаться весьма спорным, но Дядя Боб предполагает, что мы уже больше не можем придумать теста или требования, которые сломали бы код. Код уже настолько общий, что учитывает множество неописанных деталей.

Комментарии (57)


  1. Samouvazhektra
    10.10.2017 21:31
    +4

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


    1. lxsmkv
      10.10.2017 23:15
      +5

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

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

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

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


      1. bvrmn Автор
        11.10.2017 11:00
        +1

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

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


        1. DistortNeo
          11.10.2017 11:02
          +2

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

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


          1. Bonart
            12.10.2017 17:14

            Смогут, ибо включат время в оценки задач.


            1. qw1
              12.10.2017 20:56

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


              1. ApeCoder
                13.10.2017 09:08

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


                1. qw1
                  13.10.2017 11:58

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


                  1. ApeCoder
                    13.10.2017 12:46

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


                    1. qw1
                      13.10.2017 18:05

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


                      1. ApeCoder
                        14.10.2017 10:40

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


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


              1. Bonart
                13.10.2017 12:11

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


                1. qw1
                  13.10.2017 18:07

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


                  1. Bonart
                    13.10.2017 19:44

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


      1. ApeCoder
        11.10.2017 13:00

        Обычно рекомендуют делать код лучше понемножку.
        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. bvrmn Автор
      11.10.2017 10:40
      +1

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


  1. nomit
    10.10.2017 22:45

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


    1. bvrmn Автор
      11.10.2017 10:46
      +1

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


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

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


  1. nzeshka
    10.10.2017 22:46
    -1

    ТеэDеD. И майевтика, и тесты.


  1. amakhrov
    11.10.2017 08:45
    +3

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


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

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


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

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


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


    1. bvrmn Автор
      11.10.2017 10:32
      +4

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

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

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

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


      1. StrangerInTheKy
        11.10.2017 14:29

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


        1. bvrmn Автор
          12.10.2017 16:45

          > а как сделать «так»?

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


      1. amakhrov
        11.10.2017 18:29

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

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


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


        1. DistortNeo
          12.10.2017 00:04

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

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

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

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

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

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

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

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


          1. amakhrov
            12.10.2017 01:56

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


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


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


            1. DistortNeo
              12.10.2017 09:37

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

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


              1. amakhrov
                12.10.2017 17:51
                +1

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


        1. bvrmn Автор
          12.10.2017 10:57

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

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


          1. ApeCoder
            12.10.2017 11:14

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


            1. bvrmn Автор
              12.10.2017 16:13

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

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


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


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

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


              1. ApeCoder
                12.10.2017 16:23
                +1

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

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


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

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


                1. bvrmn Автор
                  12.10.2017 16:54

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

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


                  1. ApeCoder
                    12.10.2017 17:03

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

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


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


                1. DistortNeo
                  12.10.2017 17:09

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


                  1. ApeCoder
                    12.10.2017 17:24

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


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


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


                    1. bvrmn Автор
                      12.10.2017 17:34

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

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


                      1. ApeCoder
                        12.10.2017 17:36

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


          1. amakhrov
            12.10.2017 17:56
            +1

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


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


      1. ApeCoder
        12.10.2017 16:51

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

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


        1. bvrmn Автор
          12.10.2017 16:58

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

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


          1. ApeCoder
            12.10.2017 17:05

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


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

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


    1. Bonart
      12.10.2017 17:21

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

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


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

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


      1. amakhrov
        12.10.2017 18:14
        +1

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

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


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

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


  1. Fedcomp
    11.10.2017 12:42
    +1

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

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

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


    1. bvrmn Автор
      11.10.2017 13:07
      +1

      это BDD

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


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

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


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


      1. Fedcomp
        13.10.2017 19:06

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

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


        1. bvrmn Автор
          13.10.2017 20:28

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

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


          1. Fedcomp
            14.10.2017 04:37

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


  1. ApeCoder
    11.10.2017 13:06

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


    1. bvrmn Автор
      11.10.2017 13:11
      +1

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

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


      1. ApeCoder
        11.10.2017 13:25

        Имхо.


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


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


        1. bvrmn Автор
          11.10.2017 13:55
          +1

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

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

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


          1. ApeCoder
            11.10.2017 14:09

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


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


        1. DistortNeo
          12.10.2017 00:16

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


          1. ApeCoder
            12.10.2017 09:51

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


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


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


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


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


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


  1. Danik-ik
    11.10.2017 19:05

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

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