Как стать автором
Обновить

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

Время на прочтение17 мин
Количество просмотров8.1K
Всего голосов 41: ↑40 и ↓1+39
Комментарии114

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

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


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


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

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


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

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

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

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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


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

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

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

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

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


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


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


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


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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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

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

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

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

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

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


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

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


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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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


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

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


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

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


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


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


James Shore


Focused Integration Tests

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

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

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


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

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


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


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


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

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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


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


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

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

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


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


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

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

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

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


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

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


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

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

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


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

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


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

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

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

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


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

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

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

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

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


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

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


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

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

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


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

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

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

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


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

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


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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

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


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


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

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

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

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

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

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

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


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

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

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

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

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

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

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


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


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

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


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

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

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


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

thx

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


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

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

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

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

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

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


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

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


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


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

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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


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

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

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

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

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

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

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


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

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


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

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

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

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


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

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


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

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

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

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


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

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

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

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


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

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


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


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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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