Тестируемый код лучше кода?

103

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

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

Является ли написание тестируемого кода хорошей практикой даже при отсутствии тестов?


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

WannabeCoder
источник
24
Существует специальное свойство функции, которое требует от вас времени, называемого идемпотентностью. Такая функция будет выдавать один и тот же результат каждый раз, когда она вызывается с заданным значением аргумента, что не только делает ее более тестируемой, но и более удобной для компоновки и упрощает рассуждение.
Роберт Харви
4
Можете ли вы определить «лучший код»? Вы имеете в виду «ремонтопригодный» ?, «проще-использовать-без-МОК-контейнер-магия»?
k3b
7
Я предполагаю, что у вас никогда не было неудачного теста, потому что он использовал реальное системное время, а затем смещение часового пояса изменилось.
Энди
5
Это лучше, чем непроверяемый код.
Тулаинс Кордова
14
@RobertHarvey я бы не назвал это идемпотентности, я бы сказал , что ссылочная прозрачность : если func(X)возвращается "Morning", то замена всех вхождений func(X)с "Morning"не изменит программу (то есть призвание. funcНичего, кроме возвращения значения не делать). Идемпотентность подразумевает либо то func(func(X)) == X(что не является корректным по типу), либо то, что имеет func(X); func(X);те же побочные эффекты, что и func(X)(но здесь нет побочных эффектов)
Warbo

Ответы:

116

Что касается общего определения модульных тестов, я бы сказал, нет. Я видел простой код, сделанный запутанным из-за необходимости изменять его в соответствии с инфраструктурой тестирования (например, интерфейсы и IoC повсеместно усложняют задачу отслеживания уровней вызовов интерфейса и данных, которые должны быть очевидно переданы магией). Учитывая выбор между кодом, который легко понять, или кодом, который легко модульно тестировать, я каждый раз иду с поддерживаемым кодом.

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

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

gbjbaanb
источник
3
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Мировой инженер
2
Я думаю, стоит отметить, что я знаю по крайней мере один язык, который позволяет вам делать то, что описывает ваш последний абзац: Python с Mock. Из-за того, как работает импорт модуля, почти все, кроме ключевых слов, может быть заменено фиктивным, даже стандартными методами API / классами / и т. Д. Так что это возможно, но может потребоваться, чтобы язык был спроектирован таким образом, чтобы поддерживать такую ​​гибкость.
jpmc26
6
Я думаю, что есть разница между «тестируемым кодом» и «кодом [витым] в соответствии с рамками тестирования». Я не уверен, куда я иду с этим комментарием, кроме того, что я согласен с тем, что «искаженный» код - это плохо, а «тестируемый» код с хорошими интерфейсами - это хорошо.
Брайан Оукли
2
Я высказал некоторые свои мысли в комментариях к статье (поскольку расширенные комментарии здесь запрещены), зацените! Поясняю: я автор упомянутой статьи :)
Сергей Колодий
Я должен согласиться с @BryanOakley. «Тестируемый код» предполагает, что ваши проблемы разделены: можно протестировать аспект (модуль) без вмешательства других аспектов. Я бы сказал, что это отличается от «корректировки соглашения о тестировании для поддержки вашего проекта». Это похоже на шаблоны проектирования: они не должны быть принудительными. Код, который правильно использует шаблоны проектирования, считается сильным кодом. То же самое относится и к принципам тестирования. Если из-за того, что ваш код «тестируемый» приводит к чрезмерному искажению кода вашего проекта, вы делаете что-то не так.
Винс Эми
68

Является ли написание тестируемого кода хорошей практикой даже при отсутствии тестов?

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

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

Поэтому для меня всегда есть множество приоритетов при написании кода:

  1. Заставьте это работать - если код не делает то, что ему нужно, он бесполезен.
  2. Сделайте его обслуживаемым - если код не обслуживаем, он быстро перестанет работать.
  3. Сделайте его гибким - если код не гибкий, он перестанет работать, когда бизнес неизбежно придет и спросит, может ли код выполнить XYZ.
  4. Сделайте это быстро - за пределами базового приемлемого уровня, производительность просто подливает.

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

Telastyn
источник
4
Мне нравятся отношения, которые вы указываете между тестируемым и гибким - это делает всю проблему более понятной для меня. Гибкость позволяет вашему коду адаптироваться, но обязательно делает его немного более абстрактным и менее интуитивным для понимания, но это стоит того, чтобы принести пользу.
WannabeCoder
3
Тем не менее, часто я вижу, что методы, которые должны быть закрытыми, должны быть принудительно общедоступными или на уровне пакетов, чтобы инфраструктура модульного тестирования могла получить к ним прямой доступ. Далеко не идеальный подход.
jwenting
4
@WannabeCoder Конечно, гибкость стоит только тогда, когда в итоге вы сэкономите время. Вот почему мы не пишем каждый отдельный метод для интерфейса - в большинстве случаев просто проще переписать код, чем использовать слишком большую гибкость с самого начала. YAGNI по-прежнему является чрезвычайно мощным принципом - просто убедитесь, что независимо от того, что вам «не нужно», добавление его задним числом не даст вам в среднем больше работы, чем выполнение его заранее. Это код, который не следует за YAGNI, у которого больше всего проблем с гибкостью в моем опыте.
Луаан
3
«Отсутствие модульных тестов означает, что вы не закончили с вашим кодом / функцией» - Неверно. «Определение выполненного» - это то, что решает команда. Может включать или не включать некоторую степень покрытия тестами. Но нигде нет строгого требования, согласно которому функция не может быть «выполнена», если для нее нет тестов. Команда может принять решение о проведении тестов или нет.
aroth
3
@Telastyn За более чем 10 лет разработки у меня никогда не было команды, которая бы занималась модульным тестированием, и только у двух, у которых даже был один (у обоих был плохой охват). В одном месте требовался текстовый документ о том, как проверить функцию, которую вы писали. Вот и все. Возможно, мне не повезло? Я не тестирую юнит-юнит (серьезно, я модифицирую сайт SQA.SE, я очень про-юнит-тест!), Но я не нашел, чтобы они были настолько широко распространены, как утверждают ваши заявления.
CorsiKa
50

Да, это хорошая практика. Причина в том, что тестируемость не ради тестов. Ради ясности и понятности это приносит с собой.

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

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

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

Килиан Фот
источник
Другой пример - если вы хотите вытащить некоторую часть кода одного проекта в другое место (по какой-либо причине). Чем больше различные части функциональности независимы друг от друга, тем легче извлечь именно ту функциональность, которая вам нужна, и ничего более.
valenterry
10
Nobody cares about the tests themselves-- Я делаю. Я считаю тесты лучшей документацией того, что делает код, чем любые комментарии или файлы readme.
Jcollum
Я уже некоторое время медленно читаю о методах тестирования (как-то, кто вообще пока не проводит модульное тестирование), и я должен сказать, последняя часть о проверке вызова в контролируемых условиях и более гибкий код, который поставляется с это, заставило все виды вещей встать на место. Спасибо.
plast1k
12

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

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

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

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

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

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

utnapistim
источник
10

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

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

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

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

public static string GetTimeOfDay()
{
    DateTime time = DateTime.Now;
    if (time.Hour >= 0 && time.Hour < 6)
    {
        return "Night";
    }
    if (time.Hour >= 6 && time.Hour < 12)
    {
        return "Morning";
    }
    if (time.Hour >= 12 && time.Hour < 18)
    {
        return "Afternoon";
    }
    return "Evening";
}

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

Эрик Кинг
источник
1
Таким образом, вам придется тестировать рано утром, чтобы убедиться, что вы получите результат «Ночь», когда вы этого хотите. Это сложно Теперь предположим, что вы хотите проверить правильность обработки дат 29 февраля 2016 года ... И некоторые программисты на iOS (и, возможно, другие) страдают от ошибки новичка, которая портит ситуацию незадолго до или после начала года, как вы проверить это. И по
своему
1
@ gnasher729 Точно моя точка зрения. «Сделать этот код тестируемым» - это простое изменение, которое может решить множество проблем (тестирования). Если вы не хотите автоматизировать тестирование, то я думаю, что код можно передавать как есть. Но было бы лучше, если бы это было "проверяемым".
Эрик Кинг,
9

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

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

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

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

Карл Билефельдт
источник
+1 - вам нужно улучшить дизайн и архитектуру, чтобы упростить написание юнит-тестов.
BЈовић
3
+ - важна архитектура вашего кода. Более легкое тестирование - просто счастливый побочный эффект.
gbjbaanb
8

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

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

Да. Хорошей практикой является написание тестируемого кода, даже независимо от тестирования.

Карл Манастер
источник
не согласен с тем, что это СУХОЙ - обертывание GetCurrentTime в метод MyGetCurrentTime очень сильно повторяет вызов ОС, не принося никакой пользы, кроме как для помощи в тестировании. Это просто самые простые примеры, в реальности они становятся намного хуже.
gbjbaanb
1
«повторение вызова ОС без каких-либо льгот» - пока вы не закончите работу на сервере с одним тактом, обращаясь к серверу aws в другом часовом поясе, и это нарушит ваш код, а затем вам придется пройти через весь ваш код и обновите его, чтобы использовать MyGetCurrentTime, который вместо этого возвращает UTC. ; перекос часов, переход на летнее время и другие причины, по которым может быть плохой идеей слепо доверять вызову ОС или, по крайней мере, иметь единственную точку, в которую можно добавить другую замену.
Эндрю Хилл
8

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

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

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

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

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

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

Я не знаю, может и нет. У людей здесь есть веские аргументы.

Но я верю, что лучший код - это также тестируемый код.

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

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

Craig
источник
1
«лучший код, как правило, также тестируемый код» Это ключ. Делать это тестируемым не делает его лучше. Улучшение этого качества часто делает его тестируемым, и тесты часто дают вам информацию, которую вы можете использовать для его улучшения, но само по себе наличие тестов не означает качество, и есть (редкие) исключения.
Анаксимандр
1
Именно так. Считайте противозачаточным. Если это непроверяемый код, он не тестируется. Если он не проверен, как вы узнаете, работает ли он или нет, кроме как в реальной ситуации?
pjc50
1
Все тестирование доказывает, что код проходит тесты. В противном случае код, тестируемый модулем, не содержит ошибок, и мы знаем, что это не так.
wobbily_col
@anaximander Точно. По крайней мере, существует вероятность того, что простое наличие тестов является противопоказанием, которое приводит к ухудшению качества кода, если все внимание сосредоточено только на проверке флажков. «По крайней мере, семь модульных тестов для каждой функции?» "Проверьте." Но я действительно верю, что если код - это качественный код, его будет проще тестировать.
Крейг,
1
... но создание бюрократии из испытаний может быть полной тратой и не дать полезной информации или заслуживающих доверия результатов. Несмотря на; Я действительно хотел бы, чтобы кто-нибудь проверил ошибку Heartbleed SSL , да? или ошибка Apple goto fail ?
Крейг
5

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

Вы правы, и с помощью насмешек вы можете сделать код тестируемым и не тратить время (намерение каламбура не определено). Пример кода:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

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

def time_of_day(now=None):
    now = now if now is not None else datetime.datetime.utcnow()
    return now.strftime('%H:%M:%S')

Если бы Python поддерживал високосные секунды, тестовый код выглядел бы так:

def test_handle_leap_second(self):
    actual = time_of_day(
        now=datetime.datetime(year=2015, month=6, day=30, hour=23, minute=59, second=60)
    expected = '23:59:60'
    self.assertEquals(actual, expected)

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

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Тестовый код:

@unittest.patch('datetime.datetime.utcnow')
def test_handle_leap_second(self, utcnow_mock):
    utcnow_mock.return_value = datetime.datetime(
        year=2015, month=6, day=30, hour=23, minute=59, second=60)
    actual = time_of_day()
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Это дает несколько преимуществ:

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

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

l0b0
источник
К вашему сведению: ваш аргумент по умолчанию неверен. Он будет определен только один раз, поэтому ваша функция всегда будет возвращать время, когда она была впервые оценена.
arsuss
4

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

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

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

cbojar
источник
4

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

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

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

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

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

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

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

Алекс Д
источник
1

Если вы придерживаетесь принципов SOLID, вы будете на хорошей стороне, особенно если дополните это KISS , DRY и YAGNI .

Одна упущенная для меня точка - это сложность метода. Это простой метод получения / установки? Тогда просто написание тестов для удовлетворения ваших требований к тестированию будет пустой тратой времени.

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

Другой аспект написания тестов - показать другим разработчикам, как использовать ваш метод. Много раз разработчик будет искать пример того, как использовать метод и какое будет возвращаемое значение.

Просто мои два цента .

BTD
источник