Comments 64
Понятно что перевод, и камень в огород автора, но без примеров как-то очень расплывчато
Это к сожалению проблема практически всей айтишной литературы. Особенно касающейся архитектуры. Провести дидактическую редукцию сложной концепции дело непростое, и людям просто лень. А может они и не умеют этого делать.
Я очень критически отношусь к такой «лени» — «не можешь привести пример, значит это пустая болтовня». «Пример получится слишком большим?» — ничего мы подождем.
Как я отличаю хорошую литературу от плохой? В хорошей не пишут «мы вернемся к этому чуть позже» — ведь обычно не возвращаются. В хорошей дают развернутые примеры, и не противоречат установленным догмам, типа не стоит называть тест XTest, но для примера, чтобы развить мысль, называют свой пример ХТest. Это никак не оправдано, даже если это запись разговора по памяти, то всегда есть такой этап как редактирование. И в нужном месте нужно изменить достоверность диалога в угоду дидактической удобоваримости.
Вот у нас на работе недавно встала тема, давайте обмениваться опытом между отделами. Но я также недавно понял, что передать опыт на словах невозможно, его можно только получить. Иначе это не опыт а знание. Знание есть в книгах, все знания мира, но люди все равно озадачены проблемами ответы на которые есть в книгах. Значит людям нужно не знание, а решение их проблемы. Получается надо обмениваться не опытом, а проблемами. И наверное пытаться вместе их решить.
Вот возьму я тест, и захочу переписать его на контрвариантный лад. С чего начинать? В голове на «чистом листе» все очень стройно, но большинство людей имеют уже «запоротый проект» который надо чинить и переделывать. Вот у нас недавно делали ремонт в магазине техники, не закрывая магазин ни на день — вот этому хочется научиться. Как перестраивать приложение не останавливая его разработку.
С другой стороны опытные разработчики знают как это можно сделать но работы для этого нужно очень много. Т.е такая перестройка просто нерентабельна. Чтобы перестроить магазин фирма ведь заказала дополнительный труд. Они не смогли бы сделать этого только силами своих сотрудников в рабочее время. Вот перед какой проблемой все в основном и стоят. Но если мне попадется проект с чистого листа, я обязательно попробую.
С чего начинать? В голове на «чистом листе» все очень стройно, но большинство людей имеют уже «запоротый проект» который надо чинить и переделывать.
По опыту: люди, которые работали на запоротом проекте, обтерпелись и начали вникать во все перипетии, практически никогда не смогут его хорошо переписать. Модель данных, которая учитывает все нюансы текущего поведения не даст взглянуть на проблемы под другим углом и предложить более простую альтернативу. В результате получится та же самая модель, только еще и обложенная соломой со всех сторон. Это реально тяжело и вообще никак не зависит от опыта и крутости разработчика.
По опыту: люди, которые работали на запоротом проекте, обтерпелись и начали вникать во все перипетии, практически никогда не смогут его хорошо переписать
Смогут, просто им на это не дадут времени.
Пока "разбираешься" в ходе решения текущей задачи записываешь то, в чем разобрался в виде более ясного кода.
1. Никто не даст 3 месяца «разбираться».
2. Некоторые места надо сильно переписать (в т.ч. структуру БД, интерфейсы взаимодействия с другими системами и пользователем), т.е. это не тот рефакторинг, который разработчик может сделать без тысячи согласований.
Разумеется, сам по рефакторинг никогда не продать — значит надо продавать вкладывая в продуктовые задачи, с хорошими резонами, подкрепленными статистикой.
Время на выполнение задач в кривой системе и так велико.
Если при оценках будет отдельно указано время, возникшее не от сложности задачи, а от кривизны системы (со ссылками на превышение планов по этой же причине), то бизнес сможет оценить постоянные потери.
Также в оценку логично включать время (уже не выделяя отдельно) на рефакторинг, нужный чтобы эту конкретную задачу сделать быстрее.
И наконец, для продажи рефакторинга, который позволит ускорить целый ряд задач, хорошо указать, сколько мы уже потеряли от кривизны в оценке сделанных задач, и сколько можем потерять в оценке еще не сделанных.
Разумеется, ссылаться надо не общую абстракнтую кривизну, а на конкретные дефекты, мешающие делать конкретные продуктовые задачи.
Обычно рекомендуют делать код лучше понемножку.
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.
Тесты могут стать сложными, только по двум причинам:
- В случае сложного публичного интерфейса
- Внутри есть обращение к внешним сервисам. Тогда наивный простой интерфейс приведет к хаосу из моков и пронизывающего все и вся DI. Как правило, такие обращения можно подтянуть поближе к интерфейсу и сделать все понятнее.
В любом случае, сложные тесты сигнализируют о сложном дизайне основного кода.
Уважаю Роберта Мартина.
Но в этот раз как-то неубедительно.
Хорошо, итак, структура тестов не должна отражать структуру кода, потому что
такая связанность делает систему хрупкой и затрудняет рефакторинг.
А что мы имеем по факту в итоге? Класс Х и тест-класс XTest.
Тот факт, что у нас нет тестов на вспомогательные классы, которые используются в Х, означает просто, что в этих классах нет логики достаточно сложной для того, чтобы ее стоило тестировать отдельно. Как только они усложнятся — появятся отдельные тесты на них (просто потому, что тестировать их через общий X Api непрактично.
В то время как тесты становятся все более определенными, код будет все более
общим
Так-то оно так, да. Только для этого ничего делать не нужно. В общем случае это выходит автоматически. Как эмпирическое наблюдение — это очень хорошо. Но никакого руководства к действию из этого тезиса не следует.
В итоге, из статьи лично мне неясно — а что делать-то надо?
Тот факт, что у нас нет тестов на вспомогательные классы, которые используются в Х, означает просто, что в этих классах нет логики достаточно сложной для того, чтобы ее стоило тестировать отдельно.В этом весь смысл, тестирование поведения через публичный интерфейс, автоматически должно трогать все внутренности.
Как только они усложнятся — появятся отдельные тесты на них (просто потому, что тестировать их через общий X Api непрактично.Они усложняются как следствие дополнительных требований к X API, и тестировать их естественно через X API. Об этом в статье и говорится.
В итоге, из статьи лично мне неясно — а что делать-то надо?Тестировать через публичный интерфейс и проектировать код так, чтобы это было удобно делать. Если возникают трудности, значит ты что-то делаешь не так, и про это можно спросить у более опытного коллеги.
Пример из жизни: недавно менял реализацию в библиотеке, патч на 1.5к строк, ни один тест из 152 штук не пострадал, так как это было полное переписывание только внутренних функций. Если бы не было тестов или они тестировали конкретные «юниты» — никогда бы не взялся за такой рефакторинг.
Если возникают трудности, значит ты что-то делаешь не такПонять, что что-то идет не так, обычно несложно. Вопрос (и предыдущего комментатора, и я к нему присоединяюсь) — а как сделать «так»?
Пожалуй, статья хороша тем, что позволяет уточнить, в каком именно месте что-то пошло не так, но на этом польза заканчивается и остается некоторое нехорошее послевкусие. То есть «кто виноват» выяснили, теперь пора переходить к «что делать».
Они усложняются как следствие дополнительных требований к X API, и тестировать их естественно через X API.
Количество тестов растет лавинообразно, если все тестировать через внешний интерфейс, и не тестировать зависимости по отдельности. Не говоря уже о том, что сами тесты становятся сложными. Тестировать через общий интерфейс все еще можно, но уже непрактично.
А еще вдруг выясняется, что один из внутренних классов недетерминированный (допустим, использует рандом) или разговаривает с внешним сервисом, который надо замокать. И внезапно для XTest становится важно знать, что именно стоит за X Api, чтобы иметь возможность замокать что надо — хотя это не часть интерфейса X Api.
Совсем не так. В TDD тесты первичны, а внутренности вторичны. Вы не пишете тесты для внутренностей, а пишете внутренности таким образом, чтобы они проходили тесты. Каким образом эти внутренности реализованы, не имеет абсолютно никакого значения, так как тесты проверяют поведение, а не реализацию.
> Не говоря уже о том, что сами тесты становятся сложными.
Про это уже было написано выше. Сложные тесты — следствие ошибок в проектировании системы, нарушение принципов SOLID.
> Тестировать через общий интерфейс все еще можно, но уже непрактично.
В TDD тесты — это, в первую очередь, документация. Тестирование внутренностей в данном случае — это информационный шум.
Я, впрочем, не являюсь приверженцем TDD в его чистом виде, т.к. принцип написания ровно одного теста за раз и минимального количества кода, проходящего тест, считаю контрпродуктивным — приводящим к постоянным рефакторингам и костылям.
Лично я сначала продумываю архитектуру (пишу интерфейсы, вспомогательные структуры), затем пишу набор тестов под каждый интерфейс, затем код, проходящий тесты.
Давайте попробуем с конкретным примером, чтобы было понятней.
Допустим, у нас есть блог, в котором можно отправить коммент на какому-то посту. Форма коммента состоит из нескольких полей, которые наше приложение должно проверить на корректность (назовем этот этап валидацией). Помимо этого, приложение проверяет, что сам пост с заданным айди существует. И проверяет, что залогиненный юзер имеет доступ на добавление коммента.
Как проверить, что валидация отрабатывает корректно на разных входных данных?
Написать тест на класс Валидатор? Или написать тест на класс ВебКонтроллер, и тестировать валидацию через публичный интерфейс — http запросы?
Другой пример — реализация ассоциативного массива. У него простой и понятный внешний интерфейс, но реализация под капотом может быть разная. И тестировать правильность построения массивов с хэшами нет никакого смысла.
Количество тестов растет лавинообразно, если все тестировать через внешний интерфейс, и не тестировать зависимости по отдельности.
Вы путаете внешний интерфейс одной компоненты, о которой идет речь в статье, с внешним интерфейсом всего сервиса. Для сервиса, естественно, количество тестов растет, так как нужно проверять все комбинации, и так действительно не надо делать.
Что такое компонента? В статье призывается не создавать новых тестов когда мы выносим что-то в отдельный класс. Этот отдельный класс это компонента или нет? По какомй криетрию?
Что такое компонента?
Это хороший вопрос, только проблема в том, что ответ на него пытались и пытаются дать в тысяче книг. Формальный ответ на него даст возможность эффективно обучать студентов и даже сделать ИИ, способного разрабатывать самостоятельно.
На пальцах: это максимально сцепленная часть системы, которая реализует независимую часть требований. Компонента предоставляет интерфейс, модель данных и инварианты над моделью. Например, регистрация пользователей и подписки это разные компоненты.
В статье призывается не создавать новых тестов когда мы выносим что-то в отдельный класс. Этот отдельный класс это компонента или нет?
В статье призывается не делать тестов на появляющиеся внутренние классы, спрятанные за публичным API. Нет это не компонента, это деталь реализации компоненты.
Компонента предоставляет интерфейс, модель данных и инварианты над моделью.
Это похоже на описание хорошего класса :)
В статье призывается не делать тестов на появляющиеся внутренние классы, спрятанные за публичным API. Нет это не компонента, это деталь реализации компоненты.
Почему одна компонента не может быть деталью реализации другой?
Почему одна компонента не может быть деталью реализации другой?
Может, но если публичный API подкомпоненты используется только в родителе, то смысла тестировать ее нет. Она является внутренней по отношению к предметной области и требованиям.
Она является внутренней по отношению к предметной области и требованиям.
Я стараюсь, чтобы такие компоненты либо были сформулированны в терминах предметной области либо обладали своей внутренней предметной областью.
Иногда я не тестирую очень тесно связанные компоненты (например итератор отдельно от коллекции).
В такой «подкомпоненте» может десятки и сотни тысяч строк лежать, без необходимости что-либо переиспользовать.
Давайте на примере: делаем хеш MD5, первая ссылка в гугле tls.mbed.org/md5-source-code
Тестируем только публичный API, запускаем тест, попадаем на mbedtls_printf( «failed\n» ).
Где ошибку искать? Ну или там плохой тест, какой бы вы написали для публичного API, который покажет где ошибку искать?
Я согласен про "не может существовать отдельно" но не согласен про "единственный экземпляр".
Например энумератор не стоит тестировать отдельно от коллекции, правда, захочется вывести в отдельный раздел "тесты энумератора" который логично назвать MyCollectionEnumeratorTest что сожет совпасть с именем класса энумератора, но этот класс там не будет использоваться напрямую.
Если у нас есть, допустим, компонента которая делает расчет и записывает его в БД то расчет вынести отдельно и тестировать его без БД. Хотя расчет можно нигде не использовать но его использование может быть мыслимо.
Это две компоненты в чистом виде) Независимые требования.
Ну так это. Unit test тестирует не класс, а unit.
В некоторых случаях unit — один класс, в некоторых — несколько связанных классов. Вопрос определения юнита — чисто эмпирический. Иногда даже банально обусловлен удобством тестирования. Если логика в классе начинает усложняться, напишем для него отдельные тесты, даже если раньше он тестировался как часть более крупного юнита.
Тем не менее, все равно выходит, что тесты в целом следуют за структурой классов.
Если бы не было тестов или они тестировали конкретные «юниты» — никогда бы не взялся за такой рефакторинг.
Были ли тесты хорошим кодом? Содержали ли они дублирование?
Были ли тесты хорошим кодом? Содержали ли они дублирование?
Тесты просто дергали примитивные 1-1 обертки над api в которых создавались нужные зависимости. Да это хороший код, в том смысле что он читаем и его легко поддерживать, дублирования нет.
означает просто, что в этих классах нет логики достаточно сложной для того, чтобы ее стоило тестировать отдельно
Получается у этого класса нет единственной ответственности, и согласно SRP этот класс не должен существовать.
Ибо если есть класс, то у него есть ответственность, которую можно выразить через формальный контракт, исполнение которого можно проверить тестами.
В итоге, из статьи лично мне неясно — а что делать-то надо?
Пишите тесты для контрактов, а не реализаций.
Получается у этого класса нет единственной ответственности.
Неправда. Сложная логика, требующая тестирования <> множественная ответственность. Вот банально валидатор. Сначала форма была простая, валидацию тестировали через контроллер. Потом форма все усложнялась и усложнялся валидатор. И тестировать валидатор через контроллер стало непрактично — появились отдельные тесты на валидатор.
Ибо если есть класс, то у него есть ответственность, которую можно выразить через формальный контракт, исполнение которого можно проверить тестами.
Это довольно очевидно. Но ведь посыл статьи как раз в том, что мы НЕ тестируем внутренний класс. Несмотря на то, этот класс имеет определенную четко выраженную ответственность.
Неправда. Сложная логика, требующая тестирования <> множественная ответственность.
Правда. TDD требует покрывать отказными тестами любой код.
Если вы выделили публичный класс, то вы обязаны покрыть его тестами.
Если не выделили, то он покрывается тестами класса-владельца, но это как раз тривиально, поскольку приватные классы и методы напрямую очень мало кто тестирует.
Сложность логики здесь не фигурирует, этого критерия в TDD нет.
Правда. TDD требует покрывать отказными тестами любой код.
А речь шла не про TDD, а про нарушение SRP. Наличие/отсутствие тестов ничего не говорит про соблюдение/нарушение SRP конкретным классом.
то он покрывается тестами класса-владельца, но это как раз тривиально
Вот я как раз и утверждаю, что это не тривиально, если логика в классе сложная.
Либо мы по-разному понимаем публичный/внутренний класс.
Я имею в виду класс, который используется только из данного (основного) класса и больше нигде не нужен (ни отдельно, ни как вспомогательная часть другой фичи). Независимо от его сложности.
Юнит тесты — все же должны быть изолированными тестами.
Это просто непрактично описывать абсолютно все
Но большую часть описать — вполне нормально.
это BDD
BDD крутится вокруг специального DSL для написания сценариев и отражающего предметную область. BDD ближе к интеграционному тестированию, потому что, как правило, в сценариях затрагивается сразу несколько компонент. Пользователь залогинился, добавил товар в корзину и зачекаутил корзину. BDD скорее инструмент для приемочного тестирования, а не инструмент разработчика.
Юнит тесты — все же должны быть изолированными тестами.
Кому должны?) В статье раскрыта суть проблемы такого подхода. Юнит тесты, которые тестируют отдельные приватные функции, только добавляют проблем.
Покрытие компоненты/модуля через публичный API позволяет эффективно проводить рефакторинги в дальнейшем.
Покрытие компоненты/модуля через публичный API позволяет эффективно проводить рефакторинги в дальнейшем.
Не позволяют, потому что пишется очень много кода и очень много тест кейсов практически одинаковых чтобы протестировать behavior, а не реализацию.
Не позволяют, потому что пишется очень много кода и очень много тест кейсов практически одинаковых
Ну как не позволяют? При рефакторинге интерфейс не меняется, значит мы можем творить с кодом что хотим и тесты не должны ломаться, так как поведение закреплено. Количество тестов никак на это не влияет.
Логика, которая в нем содержится, должна быть протестирована или нет?
Вы пишете контракт и тесты для внешнего интерфейса, а затем реализующий код. Если вы добавили лишней логики во внутренний код, вы нарушили правила TDD (писать минимальное количество кода, проходящее тесты). И тестировать такой код не нужно, т.к. он не будет влиять на внешний интерфейс.
Я не согласен. И структура тестов и структура кода должна быть отражением требований. Если тесты по организации не похожи на код, то код или тесты не отражает требований.
И структура тестов и структура кода должна быть отражением требований.
Структура тестов и структура публичного интерфейса должны быть отражением требований. Все что внутри это, всего лишь, реализация, которая может быть какой угодно, хоть через микросервисы на марс ходить.
Имхо.
Какая угодно реализация может быть. Хорошая реализация внутри должна быть отражением предметной области. (см. Ubiquitous Language)
То есть микросервисы на марсе должны быть отражением какой-то части требований.
Хорошая реализация просто реализует предметную область. Интерфейс проектируется исходя из требований предметной области, а реализация удовлетворяет инварианты, опираясь на доступные средства, структуры данных, алгоритмы, проверки, сторадж, внешние сервисы и т.д. Уровень реализации не оперирует предметной областью, он как раз и является рабочей абстракцией предметной области, зафиксированной в интерфейсе.
Например, возьмем rate limiting. У нас есть объект который нужно лимитировать, период времени, количество вызовов. Хорошей реализацией будет просто голый redis + интерфейс с элементарными вызовами. Знает ли redis про нашу предметную область? Да нет конечно, но при этом он хорошая реализация.
Я не знаю, что такое redis, но полагаю, ято это какой-то свой продукт, со своей технической предметной областью который будут покрывать свои тесты.
Ваши тесты могут покрыть только требование "rate limiting" чего бы это ни значило. Возможно у вас будет hexagonal artchitecture со своими focused integration tests по rate limiting и отдельно протестированным тестовым адаптером. Вам не надо будет повторять тесты redis в своих тестах.
Дело в том, что любая система состоит из подсистем. Если не покрывать тестами более нижние слои абстракции то нужно тестировать их логику везде.
То есть любой код который выполняет операцию конкатенации строк, должен проверять все их значимые варианты, а не надеяться на то, что они протестированны на более нижнем уровне.
С другой стороны ваша система в целом тоже является деталью реализации для кого-то (может как часть чьего-то бизнеса).
Когда вы рефакторите код и у вас выделился какой-то слой абстракции, если это делать осмысленно то получится типа "поддержка работы с URL" или "дополнительные методы работы со строками".
Это аналогично тому, что вы в требованиях выделили главу "В целом с URL надо обращаться вот так" или "Вот так должно происходить слияние строк через одну" и далее ссылаетесь на эту главу.
Иначе вам придется повторять одни и те же вещи что в требованиях, что в коде, что в тестах.
до момента пока не сможем написать проваливающийся тест
Очень двусмысленно. Я бы предложил "до момента, когда написать проваливающийся тест уже не сможем"
Контравариантные тесты