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


91

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

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

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

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

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

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


33
Позвольте мне повторить это: вы, коллеги, хотите модульные тесты для нового кода, но они отказываются писать код таким образом, чтобы он был модульным для тестирования, хотя нет никакого риска сломать что-либо существующее? Если это правда, вы должны принять ответ @ KilianFoth и попросить его выделить первое предложение в своем ответе жирным шрифтом! У ваших коллег, видимо, очень большое недопонимание относительно их работы.
Док Браун

20
@ Ли: Кто сказал, что разделение всегда хорошая идея? Вы когда-нибудь видели кодовую базу, в которой все передается как интерфейс, созданный из фабрики интерфейсов с использованием некоторого интерфейса конфигурации? У меня есть; это было написано на Java, и это был полный, неразрешимый, глючный беспорядок. Чрезвычайная развязка - это запутывание кода.
Кристиан Хакл

8
« Эффективная работа Майкла Фезерса с Legacy Code» прекрасно справляется с этой проблемой и должна дать вам хорошее представление о преимуществах тестирования даже в новой базе кода.
10

8
@ l0b0 Это библия для этого. На stackexchange это не было бы ответом на вопрос, но в RL я бы сказал OP, чтобы эта книга была прочитана (по крайней мере, частично). OP, получите Эффективную Работу с Устаревшим Кодексом и прочитайте его, хотя бы частично (или попросите своего босса получить его). Здесь рассматриваются такие вопросы. Особенно, если вы не делали тестирование, и теперь вы в него входите - у вас может быть 20-летний опыт работы, но теперь вы будете делать вещи, с которыми у вас нет опыта . Гораздо проще прочитать о них, чем тщательно изучить все методом проб и ошибок.
Р. Шмитц

4
Спасибо за рекомендацию книги Майкла Фезерса, я обязательно заберу копию.
Ли

Ответы:


204

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

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

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

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

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


39
Я бы сказал, что это зависит от типа изменений. Существует разница между упрощением тестирования кода и внедрением специфических для теста хуков, которые НИКОГДА не должны использоваться в производстве. Я лично опасаюсь последнего, потому что Мерфи ...
Матье М.

61
Модульные тесты часто нарушают инкапсуляцию и делают тестируемый код более сложным, чем требовалось бы (например, путем введения дополнительных типов интерфейса или добавления флагов). Как всегда в разработке программного обеспечения, каждая хорошая практика и каждое хорошее правило имеют свою долю ответственности. Слепое создание большого количества модульных тестов может отрицательно сказаться на ценности бизнеса, не говоря уже о том, что написание и сопровождение тестов уже требуют времени и усилий. По моему опыту, интеграционные тесты имеют гораздо большую рентабельность инвестиций и, как правило, улучшают программные архитектуры с меньшим количеством компромиссов.
Кристиан Хакл

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

21
@ChristianHackl, почему модульный тест нарушает инкапсуляцию? Я обнаружил, что для кода, над которым я работал, если есть потребность добавить дополнительную функциональность, чтобы включить тестирование, реальная проблема заключается в том, что функция, которую вы тестируете, нуждается в рефакторинге, так что все функции одинаковы уровень абстракции (это различия в уровне абстракции, которые обычно создают эту «потребность» в дополнительном коде), при этом код более низкого уровня перемещается в свои собственные (тестируемые) функции.
Балдрикк

29
@ChristianHackl Модульные тесты никогда не должны нарушать инкапсуляцию. Если вы пытаетесь получить доступ к закрытым, защищенным или локальным переменным из модульного теста, вы делаете это неправильно. Если вы тестируете функциональность foo, вы тестируете только то, работает ли она на самом деле, а не если локальная переменная x является квадратным корнем ввода y в третьей итерации второго цикла. Если какая-то функция является закрытой, пусть будет так, вы все равно будете ее тестировать транзитивно. если он действительно большой и приватный? Это недостаток дизайна, но, возможно, даже невозможный вне C и C ++ с разделением реализации заголовка.
опа

75

Это не так просто, как вы думаете. Давайте разберемся с этим.

  • Написание юнит-тестов - определенно хорошая вещь.

НО!

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

  • Ваш «очень тонкий» webapi не кажется лучшим примером для модульного тестирования.

  • Изменение кода и тестов одновременно - это плохо.

Я бы предложил следующий подход:

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

  2. Убедитесь, что новый код тестируемый и имеет модульные и интеграционные тесты.

  3. Убедитесь, что ваша цепочка CI выполняет тесты после сборок и развертываний.

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

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

РЕДАКТИРОВАТЬ : С тех пор как я написал этот ответ, ОП разъяснил вопрос, чтобы показать, что они говорят о новом коде, а не об изменениях существующего кода. Я, возможно, наивно подумал: «Является ли модульное тестирование хорошим?» Аргумент был улажен несколько лет назад.

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


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

4
@ Ли Модульный тест должен проверять функциональность , которая может соответствовать или не соответствовать классу. Функциональная единица должна быть проверена на его интерфейсе (который может быть API в этом случае). Тестирование может выявить запахи конструкции и необходимость применять некоторые другие / более нивелирование. Создайте свои системы из небольших составных частей, их будет легче рассуждать и тестировать.
Уэс Толеман

2
@KonradRudolph: Я думаю, пропустил момент, когда ОП добавил, что этот вопрос касается разработки нового кода, а не изменения существующего. Так что нечего ломать, что делает большую часть этого ответа неприменимой.
Док Браун

1
Я категорически не согласен с утверждением, что написание модульных тестов - это всегда хорошо. Модульные тесты хороши только в некоторых случаях. Глупо использовать модульные тесты для тестирования кода веб-интерфейса, они созданы для проверки бизнес-логики. Также полезно написать модульные тесты, чтобы заменить отсутствующие проверки компиляции (например, в Javascript). Большая часть кода только для внешнего интерфейса должна писать исключительно сквозные тесты, а не модульные тесты.
Султан

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

18

Разработка кода для тестирования по сути не является запахом кода; напротив, это признак хорошего дизайна. Существует несколько известных и широко используемых шаблонов проектирования, основанных на этом (например, Model-View-Presenter), которые предлагают простое (более легкое) тестирование как большое преимущество.

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

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


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

13

ИМХО очень просто понять, что для создания модульных тестов тестируемый код должен иметь хотя бы определенные свойства. Например, если код не состоит из отдельных модулей, которые можно тестировать изолированно, слово «модульное тестирование» даже не имеет смысла. Если код не имеет этих свойств, его нужно сначала изменить, это довольно очевидно.

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

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

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


Интересно, что вы упоминаете TDD. Мы пытаемся ввести BDD / TDD, который также встретил некоторое сопротивление, а именно, что на самом деле означает «минимальный код для прохождения».
Ли

2
@ Ли: внесение изменений в организацию всегда вызывает некоторое сопротивление, и всегда нужно некоторое время, чтобы адаптировать новые вещи, это не новая мудрость. Это проблема людей.
Док Браун

Абсолютно. Я просто хотел бы, чтобы нам дали больше времени!
Ли

Часто нужно показать людям, что таким образом вы сэкономите время (и, надеюсь, тоже быстро). Почему делать то, что не принесет вам пользы?
Торбьерн Равн Андерсен

@ ThorbjørnRavnAndersen: Команда может также показать ФП, что их подход сэкономит время. Кто знает? Но мне интересно, не сталкиваемся ли мы здесь с проблемами менее технического характера; ОП продолжает приезжать сюда, чтобы рассказать нам, что его команда делает неправильно (по его мнению), как будто он пытается найти союзников для своего дела. Возможно, было бы более полезно обсудить проект вместе с командой, а не с незнакомцами на Stack Exchange.
Кристиан Хакл

11

Я не согласен с (необоснованным) утверждением, которое вы делаете:

для модульного тестирования службы Web API нам нужно будет смоделировать эту фабрику

Это не обязательно правда. Есть много способов , чтобы писать тесты, и есть способы , чтобы написать юнит - тесты , которые не связаны с издеваются. Что еще более важно, существуют другие виды тестов, такие как функциональные или интеграционные тесты. Много раз можно найти «тестовый шов» на «интерфейсе», который не является языком программирования ООП interface.

Некоторые вопросы, которые помогут вам найти альтернативный тестовый шов, который может быть более естественным:

  • Хочу ли я когда-нибудь написать тонкий Web API поверх другого API?
  • Могу ли я уменьшить дублирование кода между Web API и базовым API? Может ли один быть создан с точки зрения другого?
  • Могу ли я рассматривать весь веб-API и лежащий в его основе API как единое целое с «черным ящиком» и целенаправленно делать заявления о том, как все это ведет себя?
  • Если бы в будущем Web-интерфейс пришлось заменить новой реализацией, как бы мы поступили так?
  • Если в будущем Web API будет заменен новой реализацией, смогут ли клиенты Web API это заметить? Если так, то как?

Другое необоснованное утверждение, которое вы делаете, касается DI:

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

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

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


Принято во внимание утверждение относительно интерфейсов, но даже если бы мы не использовали их, нам все равно пришлось бы вводить объекты каким-либо образом, это забота остальной части команды. то есть некоторые в команде были бы довольны ctr без параметров, конкретизирующим конкретную реализацию и оставляющим это при этом. Фактически, один участник высказал идею об использовании отражения для внедрения макетов, поэтому нам не нужно разрабатывать код для их принятия. Который является пахнущим кодовым запахом imo
Ли

9

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

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

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

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


У нас также есть проблема с TDD, а именно, что составляет «минимальный код для прохождения». Я продемонстрировал команде этот процесс, и они воспользовались тем, что не просто написали то, что мы уже разработали, - что я могу понять. «Минимум», похоже, не определен. Если мы пишем тест и у нас есть четкие планы и проекты, почему бы не написать это, чтобы пройти тест?
Ли

@ См. "Минимальный код для прохождения" ... ну, это может звучать немного глупо, но это буквально то, что он говорит. Например, если у вас есть тест UserCanChangeTheirPassword, то в тесте вы вызываете (еще не существующую) функцию для изменения пароля, а затем утверждаете, что пароль действительно изменен. Затем вы пишете функцию до тех пор, пока не сможете запустить тест, и она не выдает ни исключений, ни ошибочных утверждений. Если в этот момент у вас есть причина для добавления какого-либо кода, то эта причина входит в другой тест, например UserCantChangePasswordToEmptyString.
Р. Шмитц

@ Ли. В конечном счете, ваши тесты в конечном итоге станут документацией того, что делает ваш код, за исключением документации, которая проверяет, выполняется ли она сама, а не просто чернилами на бумаге. Также сравните с этим вопросом - метод, CalculateFactorialкоторый просто возвращает 120 и тест проходит успешно. То есть минимум. Это также явно не то, что было задумано, но это просто означает, что вам нужен еще один тест для выражения того, что было задумано.
Р. Шмитц

1
@ Ли Маленькие шаги. Минимум может быть больше, чем вы думаете, когда код поднимается выше тривиального. Кроме того, дизайн, который вы делаете при одновременной реализации всего этого, может снова оказаться менее оптимальным, потому что вы делаете предположения о том, как это должно быть сделано, еще не написав тестов, демонстрирующих это. Снова запомните, код должен сначала сбоить
Торбьерн Равн Андерсен

1
Кроме того, регрессионные тесты очень важны. Они в поле зрения команды?
Турбьёрн Равн Андерсен

8

Если вам нужно изменить код, это запах кода.

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

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

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

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


3
«Throwaway прототип» - каждый проект когда-либо начинает жизнь как один из тех ... лучше всего думать о вещах, как никогда не быть таковыми. печатать это как я .. угадайте что? ... рефакторинг одноразового прототипа, которого не оказалось;)
Алджи Тейлор

4
Если вы хотите быть уверены, что выброшенный прототип будет отброшен, напишите его на языке прототипов, который никогда не будет разрешен в производстве. Clojure и Python - хороший выбор.
Турбьёрн Равн Андерсен

2
@ ThorbjørnRavnAndersen Это заставило меня посмеяться. Это должно было быть копанием в этих языках? :)
Ли

@Lee. Нет, только примеры языков, которые могут быть неприемлемы для производства - как правило, потому что никто в организации не может поддерживать их, потому что они незнакомы с ними и их кривые обучения крутые. Если они приемлемы, выберите другой, который не подходит.
Турбьёрн Равн Андерсен

4

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

Ты говоришь

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

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

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


2

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

Для учета осмотра / тестирования в проекте.

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

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


1

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

Хорошо написанные модульные тесты показывают, как этот код предназначен для использования. Они являются формой исполняемой документации. Я вижу ужасно написанные, слишком длинные юнит-тесты, которые просто невозможно понять. Не пиши их! Если вам нужно написать длинные тесты для настройки ваших классов, ваши классы нуждаются в рефакторинге.

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


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

1

В двух словах:

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

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

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


1

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


1
«сделано правильно» это проблема. Я должен поддерживать два проекта, в которых DI был сделан неправильно (хотя и стремился сделать это «правильно»). Это делает код просто ужасным и намного хуже, чем унаследованные проекты без DI и модульного тестирования. Правильно понять DI нелегко.
января

@ Ян, это интересно. Как они сделали это неправильно?
Ли

1
@Lee Один проект - это сервис, который требует быстрого запуска, но ужасно медленный при запуске, потому что вся инициализация класса выполняется заранее с помощью инфраструктуры DI (Castle Windsor в C #). Другая проблема, которую я вижу в этих проектах, - это смешение DI с созданием объектов с «новым», обходя DI. Это делает тестирование снова трудным и привело к некоторым неприятным условиям гонки.
Jan

1

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

Давайте посмотрим на разницу между тестируемым:

public class MyController : Controller
{
    private readonly IMyDependency _thing;

    public MyController(IMyDependency thing)
    {
        _thing = thing;
    }
}

и не тестируемый контроллер:

public class MyController : Controller
{
}

Первый вариант имеет буквально 5 дополнительных строк кода, две из которых могут быть автоматически сгенерированы Visual Studio. После того, как вы настроили свою инфраструктуру внедрения зависимостей для замены конкретного типа IMyDependencyво время выполнения - что для любой приличной структуры DI - еще одна строка кода - все просто работает, за исключением того, что теперь вы можете смоделировать и, таким образом, протестировать свой контроллер на содержание вашего сердца ,

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

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

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


1
То, что вы так быстро отклоняете как «непонимание новых вещей», может оказаться хорошим пониманием старых вещей. Инъекция зависимости, конечно, не нова. Идея и, вероятно, самые ранние реализации, насчитывают десятилетия. И да, я полагаю, что ваш ответ является примером того, как код становится более сложным из-за модульного тестирования, и, возможно, примером модульных тестов, нарушающих инкапсуляцию (потому что, кто говорит, что класс имеет открытый конструктор в первую очередь?). Я часто удалял внедрение зависимостей из кодовых баз, которые я унаследовал от кого-то другого, из-за компромиссов.
Кристиан Хакл

Контроллеры всегда имеют открытый конструктор, неявный или нет, потому что MVC требует этого. «Сложно» - возможно, если вы не понимаете, как работают конструкторы. Инкапсуляция - да, в некоторых случаях, но дебаты DI против инкапсуляции - это постоянные, очень субъективные дебаты, которые здесь не помогут, и, в частности, для большинства приложений, DI будет служить вам лучше, чем IMO инкапсуляции.
Ян Кемп

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

0

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

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

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

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

TL; DR, модульное тестирование помогает улучшить качество кода и сделать будущие изменения в коде без риска. Это также улучшает читабельность кода. Используйте тесты вместо комментариев, чтобы высказать свою точку зрения.


0

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.