Как я понимаю, суть модульных тестов заключается в том, чтобы тестировать модули кода изолированно . Это значит, что:
- Они не должны нарушаться никакими несвязанными изменениями кода в другом месте кодовой базы.
- Только один модульный тест должен прерваться из-за ошибки в тестируемом модуле, в отличие от интеграционных тестов (которые могут разбиться в кучу).
Все это подразумевает, что каждая внешняя зависимость тестируемого модуля должна быть отключена. Я имею в виду все внешние зависимости , а не только «внешние слои», такие как сеть, файловая система, база данных и т. Д.
Это приводит к логическому выводу, что практически каждый юнит-тест должен содержать макет . С другой стороны, быстрый поиск в Google о мошенничестве выявляет тонны статей, в которых утверждается, что «мошенничество - это запах кода», и его в основном (хотя и не полностью) следует избегать.
Теперь к вопросу (ам).
- Как правильно писать модульные тесты?
- Где именно проходит грань между ними и интеграционными тестами?
Обновление 1
Пожалуйста, рассмотрите следующий псевдокод:
class Person {
constructor(calculator) {}
calculate(a, b) {
const sum = this.calculator.add(a, b);
// do some other stuff with the `sum`
}
}
Может ли тест, который тестирует Person.calculate
метод без проверки Calculator
зависимости (учитывая, что Calculator
это легкий класс, не имеющий доступа к «внешнему миру»), можно считать модульным тестом?
источник
Ответы:
Мартин Фаулер на модульном тесте
Что Кент Бек написал в « Разработка через тестирование, на примере»
Любая конкретная претензия о «точке модульных тестов» будет сильно зависеть от того, какое определение «модульных тестов» рассматривается.
Если ваша точка зрения такова, что ваша программа состоит из множества небольших блоков, которые зависят друг от друга, и если вы ограничиваете себя стилем, который тестирует каждый блок изолированно, то множество тестовых двойников является неизбежным выводом.
Противоречивый совет, который вы видите, исходит от людей, работающих с другим набором предположений.
Например, если вы пишете тесты для поддержки разработчиков в процессе рефакторинга, а разделение одного блока на два - это рефакторинг, который должен поддерживаться, тогда что-то нужно дать. Может быть, этот вид теста требует другого названия? Или, может быть, нам нужно другое понимание «единицы».
Вы можете сравнить:
Я думаю, что это неправильный вопрос; это снова спор о ярлыках , когда я верю, что нас действительно волнуют свойства .
Когда я вношу изменения в код, меня не волнует изоляция тестов - я уже знаю, что «ошибка» где-то в моем текущем стеке непроверенных изменений. Если я часто запускаю тесты, я ограничиваю глубину этого стека, и обнаружение ошибки тривиально (в крайнем случае, тесты запускаются после каждого редактирования - максимальная глубина стека равна единице). Но запуск тестов не является целью - это прерывание - поэтому есть смысл в уменьшении воздействия прерывания. Один из способов уменьшить прерывание - обеспечить быстрое выполнение тестов ( Гэри Бернхардт предлагает 300 мс , но я так и не понял, как это сделать в моих обстоятельствах).
Если вызов
Calculator::add
не значительно увеличивает время, необходимое для запуска теста (или любого другого важного свойства для этого варианта использования), тогда я бы не стал использовать двойной тест - он не дает преимуществ, которые перевешивают затраты ,Обратите внимание на два допущения: человек как часть оценки стоимости и короткий набор непроверенных изменений в оценке выгоды. В тех случаях, когда эти условия не выполняются, значение «изоляции» меняется совсем немного.
См. Также « Горячая лава » Гарри Персиваля.
источник
Минимизируя побочные эффекты в вашем коде.
Если
calculator
взять пример кода, например, если он говорит с веб-API, то вы либо создаете хрупкие тесты, основанные на возможности взаимодействия с этим веб-API, либо создаете имитацию. Однако, если это детерминированный набор функций вычисления без состояния, то вы не должны (и не должны) имитировать его. Если вы это сделаете, вы рискуете, что ваш макет ведет себя иначе, чем реальный код, что приводит к ошибкам в ваших тестах.Перепечатки нужны только для кода, который читает / записывает в файловую систему, базы данных, конечные точки URL и т. Д .; которые зависят от среды, в которой вы работаете; или которые имеют высокую степень состояния и недетерминированны по своей природе. Таким образом, если вы сводите эти части кода к минимуму и скрываете их за абстракциями, то их легко смоделировать, а остальная часть кода избавляет от необходимости имитировать.
Для кодовых точек, которые имеют побочные эффекты, стоит писать тесты, которые имитируют, и тесты, которые не имеют. Последние, тем не менее, нуждаются в заботе, поскольку они по своей природе будут хрупкими и, возможно, медленными Поэтому вы можете запускать их, скажем, на ночь на CI-сервере, а не каждый раз, когда вы сохраняете и создаете свой код. Первые тесты должны проводиться настолько часто, насколько это практически возможно. Относительно того, является ли каждый тест модульным, или интеграционный тест становится академическим и позволяет избежать «пламенных войн» за то, что является и не является модульным тестом.
источник
R.equals
). Поскольку это в основном чистые функции, они обычно не проверяются в тестах.Эти вопросы довольно разные по своей сложности. Давайте сначала ответим на вопрос 2.
Модульные тесты и интеграционные тесты четко разделены. Юнит тест тестирует одну единицу (метод или класс) и использует другие единицы столько, сколько необходимо для достижения этой цели. Насмешка может быть необходима, но это не главное испытание. Интеграционный тест проверяет взаимодействие между различными фактическими единицами. Это различие является единственной причиной, по которой нам нужно как модульное, так и интеграционное тестирование - если бы один выполнял работу другого достаточно хорошо, мы бы этого не сделали, но оказалось, что обычно более эффективно использовать два специализированных инструмента, а не один обобщенный инструмент ,
Теперь важный вопрос: как проводить юнит-тест? Как сказано выше, модульные тесты должны создавать вспомогательные структуры только по мере необходимости . Зачастую проще использовать фиктивную базу данных, чем реальную базу данных или даже любую реальную базу данных. Однако издевательство само по себе не имеет значения. Если часто случается, что на самом деле проще использовать фактические компоненты другого слоя в качестве входных данных для модульного теста среднего уровня. Если так, не стесняйтесь использовать их.
Многие практики боятся, что если в модульном тесте B будут повторно использоваться классы, которые уже были проверены модульным тестом A, то дефект в модуле A приведет к сбою теста во многих местах. Я считаю , что это не проблема: а тестовый набор должен преуспеть 100% для того , чтобы дать вам уверенность , что нужно, так что это не большая проблема , чтобы иметь слишком много неудач - в конце концов, вы делаете есть дефект. Единственная критическая проблема будет, если дефект вызвал слишком мало сбоев.
Поэтому не надо издеваться над религией. Это средство, а не цель, поэтому, если вам удастся избежать лишних усилий, вам следует это сделать.
источник
The only critical problem would be if a defect triggered too few failures.
это одно из слабых мест насмешек. Мы должны «запрограммировать» ожидаемое поведение, поэтому мы можем потерпеть неудачу, что приведет к завершению наших тестов как «ложных срабатываний». Но издевательство - очень полезная техника для достижения детерминизма (самое важное условие тестирования). Я использую их во всех своих проектах, когда это возможно. Они также показывают мне, когда интеграция слишком сложна или зависимость слишком тесна.Итак, чтобы ответить на ваши вопросы напрямую:
Как вы говорите, вы должны проверять зависимости и тестировать только рассматриваемый модуль.
Интеграционный тест - это модульный тест, в котором ваши зависимости не проверяются.
Нет. Вам нужно вставить зависимость калькулятора в этот код, и у вас есть выбор между проверенной версией или реальной. Если вы используете поддельный тест - это юнит-тест, если вы используете реальный тест - это интеграционный тест.
Тем не менее, предостережение. вас действительно волнует, что люди думают, что ваши тесты должны называться?
Но ваш настоящий вопрос, кажется, заключается в следующем:
Я думаю, что проблема в том, что многие люди используют насмешки, чтобы полностью воссоздать зависимости. Например, я мог бы посмеяться над калькулятором в вашем примере как
Я бы не стал делать что-то вроде:
Я бы сказал, что это будет «тестирование моего макета» или «тестирование реализации». Я бы сказал: « Не пиши издевательства! * Вот так».
Другие люди не согласились бы со мной, мы бы начали массовые пламенные войны в наших блогах о «Лучшем пути к издевательству», которые действительно не имели бы смысла, если бы вы не понимали всю историю различных подходов и действительно не представляли большой ценности тому, кто просто хочет написать хорошие тесты.
источник
MockCalc
вStubCalc
и назвал бы его заглушкой, а не издевательством. martinfowler.com/articles/...Мое эмпирическое правило заключается в том, что правильные юнит-тесты:
Если есть полезные классы из фреймворков, которые усложняют модульное тестирование, вы можете даже найти полезным создание очень простых «оболочек» интерфейсов и классов для облегчения моделирования этих зависимостей. Эти обертки не обязательно будут подвергаться юнит-тестам.
Я нашел это различие наиболее полезным:
Здесь есть серая зона. Например, если вы можете запустить приложение в контейнере Docker и запустить интеграционные тесты в качестве завершающего этапа сборки, а затем уничтожить контейнер, можно ли включать эти тесты в качестве «модульных тестов»? Если это ваши горячие дебаты, вы в довольно хорошем месте.
Нет. Некоторые отдельные тестовые случаи будут для условий ошибки, таких как передача
null
в качестве параметра и проверка того, что вы получаете исключение. Множество подобных тестов не требуют каких-либо издевательств. Кроме того, реализации, которые не имеют побочных эффектов, например, для обработки строк или математических функций, могут не требовать каких-либо проверок, потому что вы просто проверяете выходные данные. Но большинство классов, которые стоит иметь, я думаю, потребует хотя бы одного макета где-нибудь в тестовом коде. (Чем меньше, тем лучше.)Упомянутая вами проблема «запаха кода» возникает, когда у вас слишком сложный класс, требующий длинного списка ложных зависимостей для написания ваших тестов. Это подсказка, что вам нужно реорганизовать реализацию и разделить ее так, чтобы у каждого класса была меньшая площадь и более четкая ответственность, и, следовательно, ее было бы легче тестировать. Это улучшит качество в долгосрочной перспективе.
Я не думаю, что это разумное ожидание, потому что оно работает против повторного использования.
private
Например, у вас может быть метод, который вызывается несколькимиpublic
методами, опубликованными вашим интерфейсом. Ошибка, внесенная в этот один метод, может привести к множественным ошибкам теста. Это не значит, что вы должны копировать один и тот же код в каждыйpublic
метод.источник
Я не совсем уверен, насколько это правило полезно. Если изменение одного класса / метода / чего-либо другого может нарушить поведение другого в производственном коде, то на самом деле это сотрудники, а не несвязанные. Если ваши тесты не работают, а ваш производственный код - нет, то ваши тесты являются подозрительными.
Я бы тоже отнесся к этому правилу с подозрением. Если вы действительно достаточно хороши для того, чтобы структурировать свой код и написать свои тесты таким образом, чтобы одна ошибка вызывала ровно один сбой модульного теста, то вы говорите, что уже определили все потенциальные ошибки, даже когда кодовая база развивается для использования вариантов, которые вы не ожидал.
Я не думаю, что это важное различие. Что такое «единица» кода в любом случае?
Попытайтесь найти точки входа, в которых вы можете написать тесты, которые просто «имеют смысл» с точки зрения проблемной области / бизнес-правил, с которыми имеет дело этот уровень кода. Часто эти тесты носят несколько «функциональный» характер - вводят данные и проверяют, соответствуют ли результаты ожидаемым. Если тесты показывают желаемое поведение системы, то они часто остаются достаточно стабильными, даже если рабочий код развивается и подвергается рефакторингу.
Не читайте слишком много в слове «юнит» и склоняйтесь к использованию ваших реальных производственных классов в тестах, не беспокоясь, если вы задействуете более одного из них в тесте. Если один из них сложен в использовании (потому что он требует много инициализации или требует реального сервера базы данных / почтового сервера и т. Д.), Тогда позвольте своим мыслям переключиться на насмешку.
источник
Person:tellStory()
метод, включающий детали человека в строку, затем возвращает его, тогда «история», вероятно, одна единица. Если я создаю приватный вспомогательный метод, который убирает часть кода, то я не верю, что я ввел новый модуль - мне не нужно тестировать это отдельно.Сначала несколько определений:
Модульный тест проверяет модули в отрыве от других модулей, но что это означает, что конкретно не определено никаким авторитетным источником, поэтому давайте определим его немного лучше: если границы ввода / вывода пересекаются (независимо от того, является ли этот ввод / вывод сетевым, дисковым, экран или ввод пользовательского интерфейса), есть полуобъективное место, где мы можем нарисовать линию. Если код зависит от ввода / вывода, он пересекает границу блока, и поэтому ему необходимо будет смоделировать блок, ответственный за этот ввод / вывод.
Согласно этому определению, я не вижу веской причины для насмешек над такими вещами, как чистые функции, то есть модульное тестирование поддается чистым функциям или функциям без побочных эффектов.
Если вы хотите объединить юнит-тесты с эффектами, юниты, отвечающие за эффекты, должны быть смоделированы, но, возможно, вам стоит рассмотреть интеграционный тест. Итак, короткий ответ: «Если вам нужно издеваться, спросите себя, действительно ли вам нужен интеграционный тест». Но здесь есть лучший, более длинный ответ, и кроличья нора намного глубже. Придурки могут быть моим любимым запахом кода, потому что есть чему поучиться у них.
Запахи кода
Для этого мы обратимся к Википедии:
Это продолжается позже ...
Другими словами, не все запахи кода плохие. Вместо этого они являются общими признаками того, что что-то может не быть выражено в его оптимальной форме, и запах может указывать на возможность улучшить рассматриваемый код.
В случае насмешек, запах указывает на то, что юниты, которые, кажется, требуют насмешек, зависят от юнитов, которые будут насмехаться. Это может быть признаком того, что мы не разложили проблему на атомарно разрешимые части, и это может указывать на недостаток проекта в программном обеспечении.
Суть всех разработок программного обеспечения состоит в том, чтобы разбить большую проблему на более мелкие, независимые части (декомпозиция) и объединить решения, чтобы сформировать приложение, которое решает большую проблему (композицию).
Насмешка необходима, когда единицы, используемые для разбивки большой проблемы на более мелкие части, зависят друг от друга. Иными словами, насмешка необходима, когда наши предполагаемые атомные единицы состава на самом деле не атомарны, и наша стратегия разложения не смогла разложить большую проблему на более мелкие, независимые проблемы, которые необходимо решить.
Что делает пародирование запахом кода, так это то, что в нем нет ничего плохого по своей сути - иногда это очень полезно. Что делает код запаха, так это то, что он может указывать на проблемный источник связи в вашем приложении. Иногда удаление этого источника связи гораздо более продуктивно, чем написание издевательства.
Есть много видов связи, и некоторые из них лучше, чем другие. Понимание того, что макеты являются запахом кода, может научить вас выявлять и избегать худших проявлений на ранних этапах жизненного цикла разработки приложений, прежде чем запах превратится в нечто худшее.
источник
Насмешка должна использоваться только в качестве крайней меры, даже в модульных тестах.
Метод не является единицей, и даже класс не является единицей. Единица - это любое логическое разделение кода, которое имеет смысл, независимо от того, как вы его называете. Важным элементом наличия хорошо протестированного кода является возможность свободной рефакторинга, а часть способности свободно проводить рефакторинг означает, что вам не нужно менять свои тесты, чтобы сделать это. Чем больше вы издеваетесь, тем больше вам нужно менять тесты при рефакторинге. Если вы считаете метод единичным, то вам придется менять свои тесты каждый раз, когда вы проводите рефакторинг. И если вы рассматриваете класс как единицу, то вам придется менять свои тесты каждый раз, когда вы хотите разбить класс на несколько классов. Когда вам нужно реорганизовать свои тесты, чтобы реорганизовать ваш код, люди выбирают не проводить рефакторинг своего кода, что является едва ли не худшей вещью, которая может случиться с проектом. Крайне важно, чтобы вы могли разбить класс на несколько классов без необходимости рефакторинга ваших тестов, иначе вы получите в итоге негабаритные 500-строчные классы спагетти. Если вы рассматриваете методы или классы как свои модули с помощью модульного тестирования, вы, вероятно, не занимаетесь объектно-ориентированным программированием, а выполняет какое-то мутантное функциональное программирование с объектами.
Изоляция вашего кода для модульного теста не означает, что вы все издеваетесь над ним. Если бы это произошло, вам пришлось бы высмеивать уроки математики на вашем языке, и абсолютно никто не думал, что это хорошая идея. Внутренние зависимости не должны трактоваться иначе, чем внешние. Вы верите, что они хорошо протестированы и работают так, как должны. Единственное реальное отличие состоит в том, что если ваши внутренние зависимости нарушают ваши модули, вы можете остановить то, что вы делаете, чтобы исправить это, вместо того, чтобы отправлять сообщение о проблеме в GitHub и либо копаться в кодовой базе, которую вы не понимаете, чтобы исправить это. или надежду на лучшее.
Изоляция вашего кода просто означает, что вы относитесь к своим внутренним зависимостям как к черным ящикам и не проверяете происходящее внутри них. Если у вас есть модуль B, который принимает входные данные 1, 2 или 3, и у вас есть модуль A, который вызывает его, у вас нет тестов для модуля A, чтобы выполнить каждую из этих опций, вы просто выбираете один и используете его. Это означает, что ваши тесты для модуля A должны проверять различные способы обработки ответов от модуля B, а не то, что вы передаете в него.
Итак, если ваш контроллер передает сложный объект в зависимость, и эта зависимость делает несколько возможных вещей, возможно, сохраняет его в базе данных и может возвращать различные ошибки, но все, что на самом деле делает ваш контроллер, это просто проверяет, возвращает ли он ошибка или нет, и передайте эту информацию вместе, тогда все, что вы тестируете в своем контроллере, это один тест, если он возвращает ошибку и передает ее, и один тест, если он не возвращает ошибку. Вы не проверяете, было ли что-то сохранено в базе данных, или что это за ошибка, потому что это будет интеграционный тест. Вам не нужно издеваться над зависимостью, чтобы сделать это. Вы изолировали код.
источник