Предназначены ли интеграционные тесты для повторения всех модульных тестов?

37

Допустим, у меня есть функция (написана на Ruby, но должна быть понятна всем):

def am_I_old_enough?(name = 'filip')
   person = Person::API.new(name)
   if person.male?
      return person.age > 21
   else
      return person.age > 18
   end
end

В модульном тестировании я бы создал четыре теста, чтобы охватить все сценарии. Каждый будет использовать макет Person::APIобъекта с заглушенными методами male?и age.

Теперь речь идет о написании интеграционных тестов. Я предполагаю, что Person :: API больше не должен быть осмеян. Поэтому я бы создал точно такие же четыре тестовых примера, но без насмешливого объекта Person :: API. Это верно?

Если да, то какой смысл писать модульные тесты вообще, если бы я мог просто написать интеграционные тесты, которые дают мне больше уверенности (так как я работаю с реальными объектами, а не с заглушками или издевательствами)?

Филипп Бартузи
источник
3
Что ж, одним из моментов является то, что путем имитации / модульного тестирования вы можете изолировать любые проблемы в своем коде. Если интеграционный тест не пройден, вы не знаете, чей код поврежден, ваш или API.
Крис Волерт
9
Всего четыре теста? У вас есть шесть предельных возрастов, которые вы должны тестировать: 17, 18, 19, 20, 21, 22 ...;)
Дэвид Арно
22
@FilipBartuzi, я предполагаю, что метод проверяет, старше ли, например, мужчина старше 21 года? Как в настоящее время написано, он этого не делает, это правда, только если они старше 22 лет. «Более 21» на английском языке означает «21+». Так что в вашем коде есть ошибка. Такие ошибки фиксируются путем тестирования граничных значений, то есть 20, 21, 22 для мужчины, 17,18,19 для женщины в этом случае. Таким образом, по крайней мере шесть тестов необходимы.
Дэвид Арно
6
Не говоря уже о случаях 0 и -1. Что значит для человека быть -1 года? Что должен делать ваш код, если ваш API возвращает что-то бессмысленное?
RubberDuck
9
Это было бы намного проще проверить, если вы передали объект person в качестве параметра.
Джеффо

Ответы:

72

Нет, интеграционные тесты не должны просто дублировать охват модульных тестов. Они могут дублировать какое-то освещение, но это не главное.

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

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

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

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

Килиан Фот
источник
6
«мы можем получить достаточно уверенную оценку того, что система делает то, что должна, не утонув в альтернативных сценариях для тестирования». Спасибо. Я люблю, когда кто-то подходит к автоматизированному тестированию в здравом уме.
jpmc26
1
Дж. Б. Рейнсбергер хорошо рассказывает о тестах и ​​комбинаторном взрыве, о котором вы пишете в последнем абзаце, который называется «Интегрированные тесты - это афера» . Дело не столько в интеграционных тестах, но все же довольно интересно.
Барт ван Ниероп
The customer doesn't care about a particular utility function you wrote, they care that their web app is properly secured against access by minors-> Это очень умное мышление, спасибо! Проблема в том, когда вы делаете проект для себя. Трудно разделить ваше мышление между программистом и менеджером продукта в одно и то же время
Филипп Бартузи
14

Короткий ответ - нет". Более интересная часть - почему / как эта ситуация может возникнуть.

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

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

Наиболее очевидная причина в том, что ваша функция выполняет две разные задачи:

  • Поиск на Personоснове их имени. Это требует интеграционного тестирования, чтобы убедиться, что он может найти Personобъекты, которые предположительно созданы / хранятся в другом месте.
  • Рассчитать, Personдостаточно ли стар, исходя из их пола. Это требует модульного тестирования, чтобы убедиться, что расчет выполняется должным образом.

Сгруппировав эти задачи в один блок кода, вы не сможете выполнить одно без другого. Когда вы хотите провести модульное тестирование вычислений, вы вынуждены искать Person(из реальной базы данных или из заглушки / макета). Если вы хотите проверить, что поиск интегрируется с остальной частью системы, вы также должны выполнить расчет возраста. Что мы должны делать с этим расчетом? Должны ли мы игнорировать это или проверить? Кажется, именно это затруднительное положение вы описываете в своем вопросе.

Если мы представим альтернативу, у нас может быть сам расчет:

def is_old_enough?(person)
   if person.male?
      return person.age > 21
   else 
      return person.age > 18
   end
end

Поскольку это чистый расчет, нам не нужно проводить интеграционные тесты на нем.

У нас также может возникнуть желание написать задачу поиска отдельно:

def person_from_name(name = 'filip')
   return Person::API.new(name)
end

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

При написании интеграционных тестов Person::API.new(или person_from_name) все, что вам нужно, это заботиться о том, получите ли вы ожидаемое Person; все возрастные расчеты выполняются в других местах, поэтому ваши интеграционные тесты могут их игнорировать.

Warbo
источник
11

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

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

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

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

Джон Рейнор
источник
не относится к динамическим языкам (т.е. без стадии сборки)
Филипп Бартузи