Как именно следует писать модульные тесты без насмешек?

80

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

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

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

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

Теперь к вопросу (ам).

  1. Как правильно писать модульные тесты?
  2. Где именно проходит грань между ними и интеграционными тестами?

Обновление 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это легкий класс, не имеющий доступа к «внешнему миру»), можно считать модульным тестом?

Александр Ломия
источник
10
Частично это просто опыт проектирования, который придет со временем. Вы узнаете, как структурировать ваши компоненты так, чтобы у них не было много трудных для моделирования зависимостей. Это означает, что тестируемость должна быть вторичной целью разработки любого программного обеспечения. Эту цель гораздо легче достичь, если тест написан до или вместе с кодом, например, с использованием TDD и / или BDD.
Am
33
Поместите тесты, которые работают быстро и надежно, в одну папку. Поместите медленные и потенциально хрупкие тесты в другое. Выполняйте тесты в первой папке как можно чаще (буквально каждый раз, когда вы делаете паузу в наборе текста и код компилируется, это идеально, но не все среды разработки поддерживают это). Проводите медленные тесты реже (например, когда у вас перерыв на кофе). Не беспокойтесь об имени устройства и интеграции. Звоните им быстро и медленно, если хотите. Это не важно
Дэвид Арно
6
"что практически каждый юнит тест должен издеваться" Да, так? «Быстрый поиск в Google о насмешках выявляет тонны статей, в которых утверждается, что« издевательство - это запах кода »« они ошибаются .
Майкл
13
@Michael Просто заявив : «Да, так что » и объявляя Противоположная точка зрения неправильно это не лучший способ приблизиться к спорную тему , как это. Возможно, напишите ответ и уточните, почему вы думаете, что издевательства должны быть повсюду, и, возможно, почему вы думаете, что «тонны статей» по своей сути неверны?
AJFaraday
6
Поскольку вы не дали цитату «издевательство - это запах кода», я могу только догадываться, что вы неправильно истолковали то, что прочитали. Насмешка не является запахом кода. Необходимость использовать рефлексию или другие махинации для инъекций - это кодовый запах. Сложность насмешек обратно пропорциональна качеству вашего дизайна API. Если вы можете написать простые простые модульные тесты, которые просто передают mock конструкторам, вы делаете это правильно.
TKK

Ответы:

59

цель модульных тестов - тестировать блоки кода изолированно.

Мартин Фаулер на модульном тесте

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

Что Кент Бек написал в « Разработка через тестирование, на примере»

Я называю их «модульными тестами», но они не очень хорошо соответствуют принятому определению модульных тестов.

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

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

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

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

Вы можете сравнить:

Может ли тест, в котором тестируется метод Person.calculate без проверки зависимости калькулятора (учитывая, что калькулятор является легковесным классом, не имеющим доступа к «внешнему миру»), считаться модульным тестом?

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

Когда я вношу изменения в код, меня не волнует изоляция тестов - я уже знаю, что «ошибка» где-то в моем текущем стеке непроверенных изменений. Если я часто запускаю тесты, я ограничиваю глубину этого стека, и обнаружение ошибки тривиально (в крайнем случае, тесты запускаются после каждого редактирования - максимальная глубина стека равна единице). Но запуск тестов не является целью - это прерывание - поэтому есть смысл в уменьшении воздействия прерывания. Один из способов уменьшить прерывание - обеспечить быстрое выполнение тестов ( Гэри Бернхардт предлагает 300 мс , но я так и не понял, как это сделать в моих обстоятельствах).

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

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

См. Также « Горячая лава » Гарри Персиваля.

VoiceOfUnreason
источник
5
Единственное, что делает насмешливое добавление, это доказывает, что калькулятор можно смоделировать, т. е. дизайн не связывает человека и калькулятор (хотя это можно проверить и другими способами)
jk.
40

Как именно следует писать модульные тесты без насмешек?

Минимизируя побочные эффекты в вашем коде.

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

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

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

Дэвид Арно
источник
8
Это правильный ответ как на практике, так и с точки зрения уклонения от бессмысленных семантических дебатов.
Джаред Смит
У вас есть пример нетривиальной базы кода с открытым исходным кодом, которая использует такой стиль и все еще получает хорошее тестовое покрытие?
Джори Себрехтс
4
@JoeriSebrechts каждый FP один? Пример
Джаред Смит
Не совсем то, что я ищу, поскольку это просто набор функций, которые не зависят друг от друга, а не система функций, которые вызывают друг друга. Как вы обходите необходимость создавать сложные аргументы для функции с целью ее тестирования, если эта функция является одной из функций верхнего уровня? Например, основной цикл игры.
Джори Себрехтс
1
@JoeriSebrechts хм, либо я неправильно понимаю, что вы хотите, либо вы недостаточно глубоко вникли в пример, который я привел. Функции ramda используют внутренние вызовы повсюду в их источнике (например R.equals). Поскольку это в основном чистые функции, они обычно не проверяются в тестах.
Джаред Смит
31

Эти вопросы довольно разные по своей сложности. Давайте сначала ответим на вопрос 2.

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

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

Многие практики боятся, что если в модульном тесте B будут повторно использоваться классы, которые уже были проверены модульным тестом A, то дефект в модуле A приведет к сбою теста во многих местах. Я считаю , что это не проблема: а тестовый набор должен преуспеть 100% для того , чтобы дать вам уверенность , что нужно, так что это не большая проблема , чтобы иметь слишком много неудач - в конце концов, вы делаете есть дефект. Единственная критическая проблема будет, если дефект вызвал слишком мало сбоев.

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

Килиан Фот
источник
4
The only critical problem would be if a defect triggered too few failures.это одно из слабых мест насмешек. Мы должны «запрограммировать» ожидаемое поведение, поэтому мы можем потерпеть неудачу, что приведет к завершению наших тестов как «ложных срабатываний». Но издевательство - очень полезная техника для достижения детерминизма (самое важное условие тестирования). Я использую их во всех своих проектах, когда это возможно. Они также показывают мне, когда интеграция слишком сложна или зависимость слишком тесна.
Laiv
1
Если тестируемый модуль использует другие модули, разве это не интеграционный тест? Поскольку, по сути, этот модуль будет тестировать взаимодействие между этими модулями, точно так же, как и интеграционный тест.
Александр Ломия
11
@AlexanderLomia: Что бы вы назвали юнитом? Вы бы также назвали «String» юнитом? Я бы хотел, но я бы не мечтал насмехаться над этим.
Барт ван Инген Шенау
5
« Юнит-тесты и интеграционные тесты четко разделены. Юнит-тест тестирует одну единицу (метод или класс) и использует другие единицы только столько, сколько необходимо для достижения этой цели ». Вот руб. Это ваше определение модульного теста. Мой совсем другой. Таким образом, различие между ними только «четко разделено» для любого данного определения, но разделение варьируется между определениями.
Дэвид Арно
4
@ Voo Поработав с такими базами кода, хотя найти исходную проблему может быть неприятно (особенно если архитектура закрашивала вещи, которые вы бы использовали для ее отладки), у меня все еще было больше проблем с насмешками, которые вызывали тесты, чтобы продолжать работать после того, как код, который они использовали для тестирования, сломался.
James_pic
6

Итак, чтобы ответить на ваши вопросы напрямую:

Как правильно писать модульные тесты?

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

Где именно проходит грань между ними и интеграционными тестами?

Интеграционный тест - это модульный тест, в котором ваши зависимости не проверяются.

Можно ли считать тестом, который тестирует метод Person.calculate без насмешек над Калькулятором, юнит-тест?

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

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

Но ваш настоящий вопрос, кажется, заключается в следующем:

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

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

public class MockCalc : ICalculator
{
     public Add(int a, int b) { return 4; }
}

Я бы не стал делать что-то вроде:

myMock = Mock<ICalculator>().Add((a,b) => {return a + b;})
myPerson.Calculate()
Assert.WasCalled(myMock.Add());

Я бы сказал, что это будет «тестирование моего макета» или «тестирование реализации». Я бы сказал: « Не пиши издевательства! * Вот так».

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

Ewan
источник
Спасибо за исчерпывающий ответ. Что касается того, что другие люди думают о моих тестах - на самом деле я хочу избегать написания полуинтеграционных, полуэлементных тестов, которые по ходу проекта становятся ненадежным путаницей.
Александр Ломия
нет проблем, я думаю, что проблема в том, что определения этих двух вещей не на 100% согласованы всеми.
Эван
Я бы переименовал ваш класс MockCalcв StubCalcи назвал бы его заглушкой, а не издевательством. martinfowler.com/articles/...
bdsl
@bdsl Этой статье 15 лет
Ewan
4
  1. Как правильно проводить юнит-тесты?

Мое эмпирическое правило заключается в том, что правильные юнит-тесты:

  • Кодируются против интерфейсов, а не реализаций . Это имеет много преимуществ. С одной стороны, это гарантирует, что ваши классы следуют принципу инверсии зависимости от SOLID . Кроме того, это то, что делают другие ваши классы ( верно? ), Поэтому ваши тесты должны делать то же самое. Кроме того, это позволяет вам тестировать несколько реализаций одного и того же интерфейса, многократно используя большую часть тестового кода (изменится только инициализация и некоторые утверждения).
  • Самодостаточны . Как вы сказали, изменения в любом внешнем коде не могут повлиять на результат теста. Таким образом, модульные тесты могут выполняться во время сборки. Это означает, что вам нужно издеваться, чтобы удалить любые побочные эффекты. Однако, если вы следуете принципу инверсии зависимости, это должно быть относительно просто. Хорошие тестовые фреймворки, такие как Spock, могут использоваться для динамического предоставления имитационных реализаций любого интерфейса для использования в ваших тестах с минимальным кодированием. Это означает, что каждому тестовому классу нужно только использовать код только из одного класса реализации плюс тестовая структура (и, возможно, классовые модели ["bean"]).
  • Не требует отдельного запущенного приложения . Если тест должен «говорить с чем-то», будь то база данных или веб-служба, это интеграционный тест, а не модульный тест. Я рисую линию на сетевых подключениях или файловой системе. Например, база данных SQLite, хранящаяся только в памяти, на мой взгляд, является честной игрой для модульного теста, если он вам действительно нужен.

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

  1. Где именно находится грань между ними [юнит-тестами] и интеграционными тестами?

Я нашел это различие наиболее полезным:

  • Модульные тесты моделируют «пользовательский код» , проверяя поведение классов реализации на соответствие требуемому поведению и семантике интерфейсов уровня кода .
  • Интеграционные тесты имитируют пользователя , проверяя поведение запущенного приложения на соответствие указанным сценариям использования и / или формальным API. Для веб-службы «пользователь» будет клиентским приложением.

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

  1. Правда ли, что практически каждый юнит-тест должен издеваться?

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

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

Только один модульный тест должен сломаться из-за ошибки в тестируемом модуле.

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

wberry
источник
3
  1. Они не должны нарушаться никакими несвязанными изменениями кода в другом месте кодовой базы.

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

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

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

Где именно проходит грань между ними и интеграционными тестами?

Я не думаю, что это важное различие. Что такое «единица» кода в любом случае?

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

Как именно следует писать модульные тесты без насмешек?

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

топо морто
источник
« Что такое« единица »кода? » - очень хороший вопрос, у которого могут быть неожиданные ответы, которые могут даже зависеть от того, кто отвечает. Как правило, большинство определений модульных тестов объясняют их как относящиеся к методу или классу, но это не очень полезная мера «единицы» во всех случаях. Если у меня есть Person:tellStory()метод, включающий детали человека в строку, затем возвращает его, тогда «история», вероятно, одна единица. Если я создаю приватный вспомогательный метод, который убирает часть кода, то я не верю, что я ввел новый модуль - мне не нужно тестировать это отдельно.
ВЛАЗ
2

Сначала несколько определений:

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

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

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

Запахи кода

Для этого мы обратимся к Википедии:

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

Это продолжается позже ...

«Запахи - это определенные структуры в коде, которые указывают на нарушение фундаментальных принципов проектирования и негативно влияют на качество проектирования». Сурьянараяна, Гириш (ноябрь 2014). Рефакторинг для разработки программного обеспечения. Морган Кауфманн. п. 258.

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

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

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

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

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

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

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

Эрик Эллиотт
источник
1

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

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

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

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

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

TiggerToo
источник