Как модульные тесты облегчают дизайн?

43

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

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

User039402
источник
7
Возможный дубликат Приводит ли TDD к хорошему дизайну?
комар
3
Ответ ниже - это действительно все, что вам нужно знать. Рядом с теми людьми, которые целый день пишут фабричные фабрики с инжекцией зависимостей в корень агрегата, тот парень, который спокойно пишет простой код для модульных тестов, который работает правильно, легко проверяется и уже задокументирован.
Роберт Харви
4
@gnat выполнение юнит-тестирования автоматически не подразумевает TDD, это другой вопрос
Joppe
11
«модульный тест (проверка значений в полях)» - вы, кажется, связываете модульные тесты с проверкой входных данных.
Jonrsharpe
1
@jonrsharpe Учитывая, что это код, разбирающий файл CSV, он может говорить о реальном модульном тесте, который проверяет, что определенная строка CSV дает ожидаемый результат.
JollyJoker

Ответы:

3

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

Написание test-first устраняет модульность и чистую структуру кода.

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

«При заданном условии x ожидаемое поведение y» часто становится заглушкой для поставки x(что является сценарием, в котором тест должен проверять поведение текущего компонента) и yстановится имитацией, вызов которой будет проверен в конец теста (если это не «должен возвращаться y», в этом случае тест просто будет явно проверять возвращаемое значение).

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

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

Написание тестов позже скажет вам, когда ваш код плохо структурирован.

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

Когда «смена тестов» становится обузой, потому что в одном модуле так много поведений, вы знаете, что вам нужно внести улучшения в свой код (или просто в свой подход к написанию тестов - но в моем опыте это обычно не так) ,

Когда ваши сценарии становятся слишком сложными ( «если xи yи zзатем ...») , потому что вам нужно абстрагировать больше, вы знаете , у вас есть улучшения , чтобы сделать в вашем коде.

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

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

Муравей П
источник
@SSECommunity: на сегодняшний день, имея только 2 отзыва, этот ответ очень легко пропустить. Я очень рекомендую «Разговор Майкла Фезерса», связанный с этим ответом.
displayName
103

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

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

Telastyn
источник
7
Отличный ответ. Мне всегда нравится думать о своих тестах как о первом клиенте кода; если писать тесты больно, то будет больно писать код, который использует API или что-то еще, что я разрабатываю.
Стивен Бирн
41
По моему опыту, большинство модульных тестов « не используют ваш код так, как другие программисты будут использовать ваш код». Они используют ваш код в качестве модульных тестов . Правда, они выявят много серьезных недостатков. Но API, предназначенный для модульного тестирования, может не быть API, наиболее подходящим для общего использования. Упрощенно написанные модульные тесты часто требуют, чтобы базовый код выставлял слишком много внутренних компонентов. Опять же, исходя из моего опыта - было бы интересно услышать, как вы справились с этим. (См. Мой ответ ниже)
user949300
7
@ user949300 - Я не большой сторонник теста первым. Мой ответ основан на идее кода (и, конечно, дизайна) в первую очередь. API не должны быть предназначены для модульного тестирования, они должны быть разработаны для вашего клиента. Юнит-тесты помогают приблизить вашего клиента, но они инструмент. Они здесь, чтобы служить вам, а не наоборот. И они, конечно, не остановят вас от создания дерьмового кода.
Теластин
3
Самая большая проблема с юнит-тестами в моем опыте заключается в том, что писать хорошие так же сложно, как писать хороший код. Если вы не можете отличить хороший код от плохого, написание модульных тестов не улучшит ваш код. При написании модульного теста вы должны уметь различать, что такое плавное, приятное использование и «неуклюжее» или сложное. Они могут заставить вас немного использовать ваш код, но не заставляют вас признать, что то, что вы делаете, плохо.
jpmc26
2
@ user949300 - классический пример, который я имел в виду, это репозиторий, которому требуется connString. Предположим, вы выставили это как общедоступное свойство для записи и должны установить его после того, как вы создали new () Repository. Идея состоит в том, что после 5-го или 6-го раза вы написали тест, который забывает сделать этот шаг - и тем самым вылетает - вы будете «естественно» склонены к тому, чтобы заставить connString быть инвариантом класса - передаваться в конструкторе - тем самым делая ваш API лучше и делает более вероятным, что рабочий код может быть написан, чтобы избежать этой ловушки. Это не гарантия, но это помогает, IMO.
Стивен Бирн
31

Мне потребовалось много времени, чтобы осознать, но реальная выгода (отредактируйте: для меня ваш возраст может отличаться) от разработки, основанной на тестировании ( с использованием модульных тестов), заключается в том, что вы должны разрабатывать API заранее !

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

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

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

Турбьерн Равн Андерсен
источник
Это, безусловно, преимущество, но я не думаю, что это «реальное» преимущество - реальное преимущество заключается в том, что написание тестов для вашего кода естественным образом выталкивает «условия» в зависимости и подавляет чрезмерное внедрение зависимостей (дальнейшее продвижение абстракции) ) до того, как это начнется.
Муравей P
Проблема заключается в том, что вы заранее пишете целый набор тестов, соответствующих этому API, тогда он работает не совсем так, как нужно, и вам приходится переписывать свой код и все тесты. Для общедоступных API, скорее всего, они не изменятся, и этот подход хорош. Однако API-интерфейсы для кода, который используется только для внутреннего использования, сильно меняются, когда вы выясняете, как реализовать функцию, для которой требуется совместная работа множества полу-частных API-интерфейсов
Хуан Мендес,
@AntP Да, это часть дизайна API.
Торбьерн Равн Андерсен
@JuanMendes Это не редкость, и эти тесты нужно будет менять, как и любой другой код, когда вы меняете требования. Хорошая IDE поможет вам реорганизовать классы как часть работы, выполняемой автоматически, когда вы меняете сигнатуры методов и т. Д.
Thorbjørn Ravn Andersen
@JuanMendes, если вы пишете хорошие тесты и небольшие единицы, влияние эффекта, который вы описываете, на практике невелико.
Муравей P
6

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

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

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

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

user949300
источник
Большое количество параметров метода - это запах кода и недостаток дизайна.
Суфий
5

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

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

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

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

Роберт Барон
источник
Это отличный ответ для меня. Не могли бы вы привести несколько примеров, например, по одному для каждого случая (когда вы получите представление о дизайне и т. Д.).
User039402
5

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

Это автоматически заставит вас отделить чтение строк от чтения отдельных значений.

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

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

Joppe
источник
4

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

Это впечатляющее руководство по написанию тестируемого кода , написанное Джонатаном Уолтером, Руссом Руффером и Мишко Хевери, содержит многочисленные примеры того, как недостатки в коде, которые препятствуют тестированию, также препятствуют простому повторному использованию и гибкости одного и того же кода. Таким образом, если ваш код тестируемый, его проще использовать. Большая часть «морали» - это смехотворно простые советы, которые значительно улучшают дизайн кода ( Dependency Injection FTW).

Например: очень трудно проверить, правильно ли работает метод computeStuff, когда кеш начинает выдавать вещи. Это связано с тем, что вам нужно вручную добавлять дерьмо в кеш, пока «bigCache» почти не заполнится.

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Тем не менее, когда мы используем внедрение зависимостей, гораздо проще проверить, правильно ли работает метод computeStuff, когда кеш начинает выдавать вещи. Все, что мы делаем, это создаем тест, в котором мы называем new HereIUseDI(buildSmallCache()); Notice, у нас есть более точный контроль над объектом, и он сразу же приносит дивиденды.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

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

Иван
источник
2
Честно говоря, я не уверен, как вы имеете в виду пример. Как метод computeStuff относится к кешу?
Джон V
1
@ user970696 - Да, я подразумеваю, что "computeStuff ()" использует кеш. Вопрос в следующем: «Работает ли computeStuff () правильно все время (что зависит от состояния кэша)? Следовательно, трудно подтвердить, что computeStuff () делает то, что вы хотите ДЛЯ ВСЕХ ВОЗМОЖНЫХ СОСТОЯНИЙ КЭША, если вы не можете напрямую установить / построить кеш, потому что вы жестко закодировали строку "cacheOfExорогоComputation = buildBigCache ();" (в отличие от передачи в кеш напрямую через конструктор)
Иван
0

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

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

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

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

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

topo Восстановить Монику
источник
-2

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

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

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

ом
источник
1
Это правда, но это скорее преимущество удобства сопровождения, чем прямое влияние на качество разработки кода.
Муравей P
@AntP, ОП спросил о рефакторинге и юнит-тестах.
ом
1
В вопросе упоминался рефакторинг, но реальный вопрос был о том, как модульные тесты могут улучшить / проверить дизайн кода, а не упростить сам процесс рефакторинга.
Ant P
-3

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

Дэвид Бейкер
источник
6
Это, безусловно, важный аспект тестов, но на самом деле он не отвечает на вопрос (не то, почему тесты хороши, а то, как они влияют на дизайн).
Халк