(Почему) важно, чтобы модульный тест не проверял зависимости?

103

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

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

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

Какова другая сторона этого? Действительно ли важно, чтобы модульный тест также не проверял его зависимости? Если так, то почему?

Изменить: я могу понять значение насмешливых внешних зависимостей, таких как базы данных, сети, веб-сервисы и т. Д. (Спасибо Анне Лир за мотивацию, чтобы я прояснил это.) Я имел в виду внутренние зависимости , то есть другие классы, статические функции и т. Д. которые не имеют никаких прямых внешних зависимостей.

dsimcha
источник
14
"переобучение, не хороший дизайн". Вам нужно будет предоставить больше доказательств, чем это. Многие бы назвали это не «сверхинжинирингом», а «лучшей практикой».
S.Lott
9
@ С.Лотт: Конечно, это субъективно. Я просто не хотел, чтобы куча ответов уклонялась от этой проблемы и говорила, что издевательство - это хорошо, потому что превращение вашего кода в способный к мошенничеству способствует созданию хорошего дизайна. В целом, однако, я ненавижу иметь дело с кодом, который отделен таким образом, что не имеет очевидных преимуществ ни сейчас, ни в обозримом будущем. Если у вас нет нескольких реализаций сейчас и вы не ожидаете их в обозримом будущем, тогда, IMHO, вам нужно просто жестко их кодировать. Это проще и не утомляет клиента деталями зависимостей объекта.
dsimcha
5
В целом, однако, я ненавижу иметь дело с кодом, который связан способами, которые не имеют очевидного обоснования, кроме плохого дизайна. Это проще и не дает клиенту гибкости для тестирования вещей в изоляции.
S.Lott
3
@ S.Lott: Разъяснение: я имел в виду код, который делает все возможное, чтобы отделить вещи без какого-либо четкого варианта использования, особенно когда он делает код или его клиентский код значительно более многословным, вводит еще один класс / интерфейс и т. Д. Конечно, я не призываю к тому, чтобы код был более тесно связан, чем самый простой и краткий дизайн. Кроме того, когда вы создаете строки абстракции преждевременно, они обычно оказываются в неправильных местах.
dsimcha
Подумайте об обновлении вашего вопроса, чтобы уточнить, какие различия вы делаете. Интегрировать последовательность комментариев сложно. Пожалуйста, обновите вопрос, чтобы уточнить и сфокусировать его.
S.Lott

Ответы:

116

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

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

Джеффри Фауст
источник
4
Не забывайте о регрессионных тестах, чтобы охватить обнаруженные и исправленные ошибки, чтобы предотвратить их повторное внедрение в систему при дальнейшем обслуживании.
RBerteig
2
Я бы не стал настаивать на определении, даже если согласен с ним. Некоторый код может иметь приемлемые зависимости, такие как StringFormatter, и он все еще считается большинством модульных тестов.
Данидакар
2
Данип: четкие определения важны, и я бы на них настоял. Но также важно понимать, что эти определения являются целью. К ним стоит стремиться, но вам не всегда нужен яблочко. Модульные тесты часто будут зависеть от библиотек более низкого уровня.
Джеффри Фауст
2
Также важно понимать концепцию фасадных тестов, которая не проверяет, как компоненты сочетаются друг с другом (например, интеграционные тесты), но тестирует отдельный компонент отдельно. (т. е. граф объектов)
Рикардо Родригес
1
Речь идет не только о том, чтобы быть быстрее, а о том, чтобы быть чрезвычайно утомительным или неуправляемым. Представьте, что если вы этого не сделаете, ваше дерево исполнения насмешек может экспоненциально расти. Представьте, что вы смоделируете 10 зависимостей в вашем модуле, если вы продолжите моделирование, скажем, 1 из этих 10 зависимостей используется во многих местах, и у него есть 20 собственных зависимостей, поэтому вам нужно смоделировать + 20 других зависимостей, потенциально дублирующих во многих местах. в финальной точке API вы должны смоделировать - например, базу данных, чтобы вам не пришлось сбрасывать ее, и это быстрее, как вы сказали
FantomX1
39

Тестирование со всеми имеющимися зависимостями все еще важно, но это больше в области интеграционного тестирования, как сказал Джеффри Фауст.

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

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

Абстрагирование от зависимостей от других вещей (файловая система, база данных, веб-сервисы и т. Д.) Позволяет избежать необходимости конфигурирования и делает вас и других разработчиков менее восприимчивыми к ситуациям, когда возникает искушение сказать: «О, тесты не пройдены, потому что я не сетевой ресурс не настроен. Ладно. Я запусту их позже. "

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

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


Что касается внутренних зависимостей, все становится немного мутным. Мне нравится думать о том, что я хочу максимально защитить свой класс от изменений по неправильным причинам. Если у меня есть настройки, как-то так ...

public class MyClass 
{
    private SomeClass someClass;
    public MyClass()
    {
        someClass = new SomeClass();
    }

    // use someClass in some way
}

Мне вообще все равно, как SomeClassсоздается. Я просто хочу использовать это. Если SomeClass изменяется и теперь требует параметров для конструктора ... это не моя проблема. Мне не нужно менять MyClass, чтобы учесть это.

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

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

Переписав MyClass, чтобы он принимал экземпляр SomeClass в конструкторе, я могу создать поддельный экземпляр SomeClass, который возвращает желаемое значение (либо через фальшивый фреймворк, либо с помощью ручного макета). Мне обычно не нужно вводить интерфейс в этом случае. Делать это или нет - это во многом личный выбор, который может быть продиктован вашим языком выбора (например, интерфейсы более вероятны в C #, но вам определенно не нужен в Ruby).

Адам Лир
источник
+1 Отличный ответ, потому что внешние зависимости - это тот случай, о котором я не думал, когда писал первоначальный вопрос. Я уточнил свой вопрос в ответ. Смотрите последние изменения.
dsimcha
@dsimcha Я расширил свой ответ, чтобы подробнее рассказать об этом. Надеюсь, это поможет.
Адам Лир
Адам, можете ли вы предложить язык, в котором «Если SomeClass изменяется и теперь требует параметры для конструктора ... Мне не нужно было менять MyClass», верно? Извините, это просто выскочило на меня как необычное требование. Я вижу, что не нужно менять модульные тесты для MyClass, но не нужно менять MyClass ... вау.
1
@moz Я имел в виду, что при создании SomeClass MyClass не нужно менять, если SomeClass внедряется в него, а не создается внутри. При нормальных обстоятельствах MyClass не нужно заботиться о деталях настройки SomeClass. Если интерфейс SomeClass изменится, то да ... MyClass все равно придется изменить, если затронуты какие-либо из его методов.
Адам Лир
1
Если вы будете издеваться над SomeClass при тестировании MyClass, как вы обнаружите, если MyClass использует SomeClass неправильно или использует непроверенную причину SomeClass, которая может измениться?
именитый
22

Помимо проблемы с модулем и интеграционным тестом, учтите следующее.

Класс Widget имеет зависимости от классов Thingamajig и WhatsIt.

Модульный тест для Widget не пройден.

В каком классе лежит проблема?

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

ручей
источник
3
@Brook: Как насчет того, чтобы увидеть, каковы результаты для всех зависимостей виджета? Если они все проходят, это проблема с Widget, пока не доказано обратное.
dsimcha
4
@dsimcha, но теперь вы добавляете сложность обратно в игру, чтобы проверить промежуточные шаги. Почему бы не упростить и просто сначала выполнить простые юнит-тесты? Затем проведите интеграционный тест.
Asoundmove
1
@dsimcha, это звучит разумно, пока вы не попадаете в нетривиальный граф зависимостей. Скажем, у вас есть сложный объект, имеющий 3+ слоя глубиной с зависимостями, он превращается в O (N ^ 2) поиск проблемы, а не в O (1)
Brook
1
Кроме того, моя интерпретация SRP заключается в том, что класс должен нести единственную ответственность на уровне концептуальной / проблемной области. Неважно, если выполнение этой обязанности требует от нее делать много разных вещей, если рассматривать ее на более низком уровне абстракции. Если довести до крайности, SRP будет противоречить ОО, потому что большинство классов хранят данные и выполняют над ними операции (две вещи при просмотре на достаточно низком уровне).
dsimcha
4
@ Брук: Больше типов не увеличивает сложность как таковую. Больше типов хорошо, если эти типы имеют концептуальный смысл в проблемной области и делают код более простым, а не трудным для понимания. Проблема заключается в искусственном разделении вещей, которые концептуально связаны на уровне проблемной области (т. Е. У вас есть только одна реализация, вероятно, никогда не будет больше одной, и т. Д.), И разделение плохо сопоставляется с концепциями проблемной области. В этих случаях глупо, бюрократически и многословно создавать серьезные абстракции вокруг этой реализации.
dsimcha
14

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

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

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

Julio
источник
7
А когда ваши интеграционные тесты не пройдены, модульное тестирование может решить проблему.
Тим Уиллискрофт
9
Продолжим аналогию: когда вы обнаружите, что ваша еда на вкус ужасна, может потребоваться некоторое расследование, чтобы определить, что это потому, что ваш хлеб заплесневел. В результате вы добавляете модульный тест для проверки заплесневелого хлеба перед приготовлением, чтобы эта конкретная проблема не могла повториться. Тогда, если у вас будет другой сбой еды позже, вы устраните заплесневелый хлеб как причину.
Kyralessa
Люблю эту аналогию!
ревун
6

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

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

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

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

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

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

Newtopian
источник
4

Какова другая сторона этого? Действительно ли важно, чтобы модульный тест также не проверял его зависимости? Если так, то почему?

Блок. Значит единственное число

Тестирование двух вещей означает, что у вас есть две вещи и все функциональные зависимости.

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

Существует n (n-1) / 2 потенциальных зависимостей среди n тестируемых элементов.

Это большая причина.

Простота имеет значение.

С. Лотт
источник
2

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

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

Майкл Браун
источник
2

(Это небольшой ответ. Спасибо @TimWilliscroft за подсказку.)

Неисправности легче локализовать, если:

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

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

rwong
источник
Почему не проверяются зависимости? Они предположительно более низкого уровня и их проще тестировать. Более того, с помощью модульных тестов, охватывающих конкретный код, легче точно определить местоположение ошибки.
Дэвид Торнли
2

Много хороших ответов. Я бы также добавил пару других моментов:

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

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

Стив
источник
0

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

  • четкое документирование зависимостей каждого класса через его интерфейс (очень полезно для новичков в команде)
  • классы становятся намного понятнее и проще для maintan (как только вы поместите создание уродливого графа объектов в отдельный класс)
  • слабая связь (внесение изменений намного проще)
Оливер Вейлер
источник
2
Метод: Да, но вы должны добавить дополнительный код и дополнительные классы для этого. Код должен быть как можно более кратким и в то же время читаемым. ИМХО, добавление фабрик и тому подобное - это чрезмерная инженерия, если вы не можете доказать, что вам нужно или, скорее всего, потребуется гибкость, которую она обеспечивает.
dsimcha
0

Э-э ... в этих ответах есть хорошие моменты по модульному и интеграционному тестированию!

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

Итак, есть другие факторы, которые гораздо важнее (например, «что тестировать» ), прежде чем вы подумаете о «как проверить» из моего опыта ...

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

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

Давайте возьмем это до крайности, когда коллега из моей команды предложил, что мы должны попытаться выполнить модульное тестирование нашего чистого кода уровня модели Java EE с желаемым 100% покрытием для всех классов в пределах и насмешливых данных в базе данных. Или менеджер, который хотел бы, чтобы интеграционные тесты покрывались 100% всех возможных вариантов использования в реальном мире и рабочих процессов веб-интерфейса, потому что мы не хотим рисковать, если какой-либо вариант использования потерпит неудачу. Но у нас ограниченный бюджет около 1 миллиона евро, довольно жесткий план по кодированию всего. Клиентская среда, где потенциальные ошибки приложений не будут представлять большой опасности для людей или компаний. Наше приложение будет внутренне проверено с помощью (некоторых) важных модульных тестов, интеграционных тестов, ключевых пользовательских тестов с разработанными планами испытаний, этапом испытаний и т. Д. Мы не разрабатываем приложение для какого-либо ядерного завода или фармацевтического производства!

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

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

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

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

Андреас Дитрих
источник