Я пытаюсь привыкнуть регулярно писать модульные тесты с моим кодом, но я прочитал, что сначала важно написать тестируемый код . Этот вопрос касается твердых принципов написания тестируемого кода, но я хочу знать, полезны ли эти принципы проектирования (или, по крайней мере, не вредны), не планируя писать тесты вообще. Чтобы уточнить - я понимаю важность написания тестов; это не вопрос их полезности.
Чтобы проиллюстрировать мою путаницу, в статье, которая вдохновила этот вопрос, автор приводит пример функции, которая проверяет текущее время и возвращает некоторое значение в зависимости от времени. Автор указывает на это как на плохой код, потому что он выдает данные (время), которые он использует внутри, что затрудняет его тестирование. Мне, однако, кажется, что излишне убивать время в качестве аргумента. В какой-то момент значение должно быть инициализировано, и почему бы не приблизиться к потреблению? Плюс, цель метода в моем уме - вернуть некоторое значение, основанное на текущем времени , делая его параметром, который подразумевает, что эта цель может / должна быть изменена. Этот и другие вопросы заставляют меня задаться вопросом, является ли тестируемый код синонимом «лучшего» кода.
Является ли написание тестируемого кода хорошей практикой даже при отсутствии тестов?
Действительно ли тестируемый код более стабилен? был предложен в качестве дубликата. Однако этот вопрос касается «стабильности» кода, но я спрашиваю более широко о том, является ли код лучше по другим причинам, таким как читаемость, производительность, связывание и так далее.
источник
func(X)
возвращается"Morning"
, то замена всех вхожденийfunc(X)
с"Morning"
не изменит программу (то есть призвание.func
Ничего, кроме возвращения значения не делать). Идемпотентность подразумевает либо тоfunc(func(X)) == X
(что не является корректным по типу), либо то, что имеетfunc(X); func(X);
те же побочные эффекты, что иfunc(X)
(но здесь нет побочных эффектов)Ответы:
Что касается общего определения модульных тестов, я бы сказал, нет. Я видел простой код, сделанный запутанным из-за необходимости изменять его в соответствии с инфраструктурой тестирования (например, интерфейсы и IoC повсеместно усложняют задачу отслеживания уровней вызовов интерфейса и данных, которые должны быть очевидно переданы магией). Учитывая выбор между кодом, который легко понять, или кодом, который легко модульно тестировать, я каждый раз иду с поддерживаемым кодом.
Это значит не тестировать, а подбирать инструменты, которые вам подходят, а не наоборот. Есть и другие способы тестирования (но трудный для понимания код - это всегда плохой код). Например, вы можете создавать модульные тесты, которые являются менее детализированными (например, отношение Мартина Фаулера к тому , что блок обычно является классом, а не методом), или вместо этого вы можете использовать для своей программы автоматизированные интеграционные тесты. Это может быть не так красиво, как ваш тестовый фреймворк светится зелеными галочками, но мы следим за протестированным кодом, а не за геймификацией процесса, верно?
Вы можете сделать свой код простым в обслуживании и по-прежнему подходить для модульных тестов, определяя хорошие интерфейсы между ними, а затем писать тесты, которые осуществляют открытый интерфейс компонента; или вы могли бы получить более качественную тестовую среду (такую, которая заменяет функции во время выполнения, чтобы имитировать их, вместо того, чтобы требовать компиляции кода с использованием имитаций на месте). Усовершенствованная среда модульного тестирования позволяет заменить функциональность системы GetCurrentTime () на вашу собственную во время выполнения, поэтому вам не нужно вводить искусственные обертки для этого просто в соответствии с инструментом тестирования.
источник
Перво-наперво, отсутствие тестов - гораздо более серьезная проблема, чем тестируемый код или нет. Отсутствие модульных тестов означает, что вы не закончили с вашим кодом / функцией.
Я бы не сказал, что важно писать тестируемый код - важно писать гибкий код. Негибкий код сложно протестировать, поэтому подходы и то, что люди называют его, во многом совпадают.
Поэтому для меня всегда есть множество приоритетов при написании кода:
Модульные тесты помогают поддерживать работоспособность кода, но только до определенной степени. Если вы сделаете код менее читаемым или более хрупким, чтобы заставить модульные тесты работать, это станет контрпродуктивным. «Тестируемый код» - это, как правило, гибкий код, так что это хорошо, но не так важно, как функциональность или ремонтопригодность. Для чего-то похожего на текущее время, гибкость - это хорошо, но это наносит ущерб удобству сопровождения, делая код труднее использовать правильно и более сложно. Поскольку ремонтопригодность важнее, я обычно ошибаюсь в сторону более простого подхода, даже если он менее тестируемый.
источник
Да, это хорошая практика. Причина в том, что тестируемость не ради тестов. Ради ясности и понятности это приносит с собой.
Никто не заботится о самих тестах. Печально, что нам нужны большие наборы регрессионных тестов, потому что мы не настолько хороши, чтобы писать идеальный код без постоянной проверки своей работы. Если бы мы могли, концепция тестов была бы неизвестна, и все это не было бы проблемой. Я конечно хотел бы, чтобы я мог. Но опыт показал, что почти все мы не можем, поэтому тесты, охватывающие наш код, - это хорошо, даже если они отнимают время на написание бизнес-кода.
Как тесты улучшают наш бизнес-код независимо от самих тестов? Вынуждая нас сегментировать нашу функциональность на единицы, которые легко продемонстрировать, чтобы быть правильными. Эти юниты также легче получить правильно, чем те, которые мы иначе соблазнились бы написать.
Ваш пример времени - хороший момент. Пока у вас есть только функция, возвращающая текущее время, вы можете подумать, что нет смысла программировать ее. Насколько сложно это понять? Но ваша программа неизбежно будет использовать эту функцию в другом коде, и вы определенно хотите протестировать этот код в разных условиях, в том числе в разное время. Поэтому хорошей идеей будет иметь возможность манипулировать временем, которое возвращает ваша функция, - не потому, что вы не доверяете своему однострочному
currentMillis()
вызову, а потому, что вам необходимо проверять вызывающих абонентов этого вызова в контролируемых обстоятельствах. Итак, вы видите, что тестируемый код полезен, даже если сам по себе он не заслуживает особого внимания.источник
Nobody cares about the tests themselves
-- Я делаю. Я считаю тесты лучшей документацией того, что делает код, чем любые комментарии или файлы readme.Потому что вам может понадобиться повторно использовать этот код, значение которого отличается от значения, сгенерированного внутри. Возможность вставлять значение, которое вы собираетесь использовать в качестве параметра, гарантирует, что вы можете генерировать эти значения в любое время, а не просто «сейчас» (что означает «сейчас», когда вы вызываете код).
Эффективное тестирование кода означает создание кода, который можно (с самого начала) использовать в двух разных сценариях (производство и тестирование).
По сути, хотя вы можете утверждать, что нет никакого стимула сделать код тестируемым в отсутствие тестов, существует большое преимущество в написании кода, пригодного для повторного использования, и оба являются синонимами.
Вы также можете утверждать, что целью этого метода является возвращение некоторого значения, основанного на значении времени, и вам нужно, чтобы оно генерировало это на основе «сейчас». Один из них более гибкий, и если вы привыкнете выбирать этот вариант, со временем ваш коэффициент повторного использования кода возрастет.
источник
Говорить так может показаться глупым, но если вы хотите иметь возможность тестировать свой код, тогда да, лучше писать тестируемый код. Ты спрашиваешь:
Именно потому, что в примере, на который вы ссылаетесь, этот код становится непроверяемым. Если только вы не запускаете поднабор ваших тестов в разное время дня. Или вы сбрасываете системные часы. Или какой-то другой обходной путь. Все это хуже, чем просто сделать ваш код гибким.
Помимо того, что этот небольшой метод является негибким, он выполняет две функции: (1) получение системного времени и затем (2) возвращение некоторого значения на его основе.
Имеет смысл еще больше разделить обязанности, чтобы часть, находящаяся вне вашего контроля (
DateTime.Now
), имела наименьшее влияние на остальную часть вашего кода. Это упростит приведенный выше код с побочным эффектом систематической проверки.источник
Это, безусловно, имеет свою стоимость, но некоторые разработчики настолько привыкли платить, что забыли о стоимости. Для вашего примера теперь у вас есть два модуля вместо одного, вам требуется вызывающий код для инициализации и управления дополнительной зависимостью, и, хотя
GetTimeOfDay
он более тестируемый, вы снова в той же лодке, тестирующей вашу новуюIDateTimeProvider
. Просто если у вас есть хорошие тесты, выгоды обычно перевешивают затраты.Кроме того, в некоторой степени написание тестируемого кода побуждает вас создавать более понятный код. Новый код управления зависимостями раздражает, поэтому вы, возможно, захотите сгруппировать все зависящие от времени функции вместе, если это вообще возможно. Это может помочь смягчить и исправить ошибки, например, когда вы загружаете страницу прямо на временной границе, когда некоторые элементы отображаются с использованием времени до, а другие с использованием времени после. Это также может ускорить вашу программу, избегая повторных системных вызовов, чтобы узнать текущее время.
Конечно, эти архитектурные улучшения сильно зависят от того, кто замечает возможности и реализует их. Одна из самых больших опасностей фокусировки на юнитах - упускать из виду общую картину.
Многие фреймворки для модульных тестов позволяют вам обезопасить патч-объект во время выполнения, что позволяет вам использовать преимущества тестируемости без лишних хлопот. Я даже видел это сделано в C ++. Изучите эту способность в ситуациях, когда кажется, что цена тестируемости не стоит того.
источник
Возможно, что не каждая характеристика, способствующая тестируемости, является желательной вне контекста тестируемости - у меня возникают трудности с поиском обоснования, не связанного с тестированием, например, для параметра времени, который вы приводите, - но в широком смысле характеристики, которые способствуют тестируемости также внести свой вклад в хороший код независимо от тестируемости.
Вообще говоря, тестируемый код - это гибкий код. Это маленькие, дискретные, связные, куски, поэтому отдельные биты могут быть вызваны для повторного использования. Он хорошо организован и хорошо назван (чтобы иметь возможность тестировать некоторые функции, вы больше внимания уделяете именованию; если вы не пишете тесты, имя для одноразовой функции будет иметь меньшее значение). Он имеет тенденцию быть более параметрическим (как пример вашего времени), поэтому открыт для использования из других контекстов, чем исходная цель. Это СУХОЙ, поэтому менее загроможден и легче для понимания.
Да. Хорошей практикой является написание тестируемого кода, даже независимо от тестирования.
источник
Написание тестируемого кода важно, если вы хотите доказать, что ваш код действительно работает.
Я склонен согласиться с негативным мнением о том, чтобы деформировать ваш код в отвратительные искажения, просто чтобы приспособить его к конкретной тестовой среде.
С другой стороны, всем здесь, в тот или иной момент, приходилось иметь дело с магической функцией длиной в 1000 строк, с которой просто отвратительно иметь дело, практически к ней нельзя прикоснуться, не сломав одну или несколько неясных, не очевидные зависимости где-то еще (или где-то внутри себя, где зависимость практически невозможно визуализировать) и по определению практически невозможно проверить. На мой взгляд, идея (которая не лишена недостатков) о том, что рамки тестирования стали раздутыми, не должна восприниматься как бесплатная лицензия для написания некачественного, непроверяемого кода.
Например, идеалы разработки, основанные на тестировании, как правило, подталкивают вас к написанию процедур с одной ответственностью, и это определенно хорошая вещь. Лично я говорю: купите единый источник правды с единственной ответственностью, контролируемой областью действия (без чертовых глобальных переменных) и сведите к минимуму хрупкие зависимости, и ваш код будет тестируемым. Тестируется какой-то конкретной платформой тестирования? Кто знает. Но тогда, возможно, это среда тестирования, которая должна приспособиться к хорошему коду, а не наоборот.
Но, чтобы быть ясным, код, который настолько умный, или настолько длинный и / или взаимозависимый, что его нелегко понять другому человеку, не является хорошим кодом. И это также, по совпадению, не тот код, который можно легко протестировать.
Так что, приближаясь к моему резюме, тестируемый код лучше кода?
Я не знаю, может и нет. У людей здесь есть веские аргументы.
Но я верю, что лучший код - это также тестируемый код.
И если вы говорите о серьезном программном обеспечении для использования в серьезных начинаниях, то отправка непроверенного кода - не самая ответственная вещь, которую вы могли бы делать с деньгами вашего работодателя или ваших клиентов.
Также верно, что некоторый код требует более тщательного тестирования, чем другой, и немного глупо притворяться иначе. Как бы вы хотели быть космонавтом на космическом шаттле, если бы система меню, которая связывала вас с жизненно важными системами на шаттле, не была проверена? Или сотрудник на атомной станции, где программные системы, контролирующие температуру в реакторе, не тестировались? С другой стороны, требует ли часть кода, генерирующего простой отчет только для чтения, контейнеровоз, полный документации и тысячи юнит-тестов? Я надеюсь, что нет. Просто говорю...
источник
Вы правы, и с помощью насмешек вы можете сделать код тестируемым и не тратить время (намерение каламбура не определено). Пример кода:
Теперь предположим, что вы хотите проверить, что происходит в течение високосной секунды. Как вы говорите, чтобы протестировать этот излишний способ, вам придется изменить (рабочий) код:
Если бы Python поддерживал високосные секунды, тестовый код выглядел бы так:
Вы можете проверить это, но код является более сложным, чем необходимо, и тесты все еще не могут надежно использовать ветвь кода, которую будет использовать большая часть производственного кода (то есть, не передавая значение
now
). Вы работаете над этим, используя макет . Начиная с исходного производственного кода:Тестовый код:
Это дает несколько преимуществ:
time_of_day
независимо от его зависимостей.Стоит надеяться, что будущие фальшивые фреймворки сделают такие вещи проще. Например, поскольку вы должны ссылаться на смоделированную функцию как на строку, вы не можете легко заставить IDE изменять ее автоматически, когда время от времени
time_of_day
начинает использовать другой источник.источник
Качество хорошо написанного кода заключается в том, что его можно изменить . То есть, когда происходит изменение требований, изменение в коде должно быть пропорциональным. Это идеал (и не всегда достигнутый), но написание тестируемого кода помогает нам приблизиться к этой цели.
Почему это помогает нам стать ближе? На производстве наш код работает в производственной среде, включая интеграцию и взаимодействие со всем другим нашим кодом. В модульных тестах мы сметаем большую часть этой среды. Наш код теперь может изменяться, потому что тесты - это изменение . Мы используем модули по-разному, с разными входами (ложными, плохими, которые могут никогда не быть переданы, и т. Д.), Чем мы использовали бы в производстве.
Это готовит наш код к тому дню, когда в нашей системе происходят изменения. Допустим, для расчета времени необходимо другое время в зависимости от часового пояса. Теперь у нас есть возможность пройти в это время и не нужно вносить никаких изменений в код. Когда мы не хотим передавать время и хотим использовать текущее время, мы можем просто использовать аргумент по умолчанию. Наш код устойчив к изменениям, потому что он тестируемый.
источник
Исходя из моего опыта, одно из самых важных и далеко идущих решений, которые вы принимаете при создании программы, - это то, как вы разбиваете код на блоки (где «блоки» используются в самом широком смысле). Если вы используете OO-язык на основе классов, вам нужно разбить все внутренние механизмы, используемые для реализации программы, на некоторое количество классов. Затем вам нужно разбить код каждого класса на некоторое количество методов. В некоторых языках выбор состоит в том, как разбить ваш код на функции. Или, если вы делаете SOA, вам нужно решить, сколько сервисов вы будете строить и что будет входить в каждый сервис.
Разбивка, которую вы выбираете, оказывает огромное влияние на весь процесс. Хороший выбор облегчает написание кода и приводит к меньшему количеству ошибок (даже до того, как вы начнете тестировать и отлаживать). Они облегчают изменение и обслуживание. Интересно, что оказывается, что, как только вы обнаружите хорошую разбивку, ее обычно легче тестировать, чем плохую.
Почему это так? Я не думаю, что могу понять и объяснить все причины. Но одна из причин заключается в том, что хорошая разбивка всегда означает выбор умеренного «размера зерна» для единиц реализации. Вы не хотите втиснуть слишком много функциональности и слишком много логики в один класс / метод / функцию / модуль / и т.д. Это делает ваш код легче для чтения и записи, но также облегчает тестирование.
Но дело не только в этом. Хороший внутренний дизайн означает, что ожидаемое поведение (входы / выходы / и т. Д.) Каждой единицы реализации может быть определено четко и точно. Это важно для тестирования. Хороший дизайн обычно означает, что каждая единица реализации будет иметь умеренное количество зависимостей от других. Это облегчает чтение и понимание вашего кода, но также облегчает тестирование. Причины продолжаются; возможно, другие могут сформулировать больше причин, которые я не могу.
Что касается примера в вашем вопросе, я не думаю, что «хороший дизайн кода» эквивалентен утверждению, что все внешние зависимости (такие как зависимость от системных часов) всегда должны быть «внедрены». Это может быть хорошей идеей, но это отдельный вопрос из того, что я здесь описываю, и я не буду вдаваться в его плюсы и минусы.
Между прочим, даже если вы делаете прямые вызовы системных функций, которые возвращают текущее время, воздействуют на файловую систему и т. Д., Это не означает, что вы не можете тестировать свой код изолированно. Хитрость заключается в том, чтобы использовать специальную версию стандартных библиотек, которая позволяет подделывать возвращаемые значения системных функций. Я никогда не видел, чтобы другие упоминали эту технику, но это довольно просто сделать со многими языками и платформами разработки. (Надеемся, что ваша языковая среда исполнения с открытым исходным кодом и ее легко построить. Если выполнение вашего кода включает в себя этап связывания, надеюсь, также легко контролировать, с какими библиотеками он связан.)
Таким образом, тестируемый код не обязательно является «хорошим» кодом, но «хороший» код обычно тестируемый.
источник
Если вы придерживаетесь принципов SOLID, вы будете на хорошей стороне, особенно если дополните это KISS , DRY и YAGNI .
Одна упущенная для меня точка - это сложность метода. Это простой метод получения / установки? Тогда просто написание тестов для удовлетворения ваших требований к тестированию будет пустой тратой времени.
Если это более сложный метод, в котором вы манипулируете данными и хотите быть уверены, что они будут работать, даже если вам придется изменить внутреннюю логику, тогда это был бы отличный вызов для тестового метода. Много раз мне приходилось менять кусок кода через несколько дней / недель / месяцев, и я был действительно счастлив получить тестовый пример. При первой разработке метода я тестировал его с помощью метода тестирования, и я был уверен, что он будет работать. После изменения мой основной тестовый код все еще работал. Так что я был уверен, что мои изменения не сломали какой-то старый код в производстве.
Другой аспект написания тестов - показать другим разработчикам, как использовать ваш метод. Много раз разработчик будет искать пример того, как использовать метод и какое будет возвращаемое значение.
Просто мои два цента .
источник