Каковы преимущества и недостатки автоматизированных модульных тестов по сравнению с автоматизированными интеграционными тестами? - PullRequest
28 голосов
/ 21 апреля 2009

Недавно мы добавили автоматизированные тесты в наши существующие Java-приложения.

Что у нас есть

Большинство этих тестов являются интеграционными тестами, которые могут охватывать стек вызовов, таких как: -

  1. HTTP-сообщение в сервлете
  2. Сервлет проверяет запрос и вызывает бизнес-уровень
  3. Бизнес-уровень делает кучу вещей через hibernate и т. Д. И обновляет некоторые таблицы базы данных
  4. Сервлет генерирует некоторый XML, запускает его через XSLT для получения HTML-ответа.

Затем мы проверяем, что сервлет ответил правильным XML и что в базе данных есть правильные строки (наш экземпляр Oracle разработки). Эти строки затем удаляются.

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

Все эти тесты выполняются как часть наших ночных (или adhoc) сборок.

Вопрос

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

С какими проблемами мы можем столкнуться при таком подходе?

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

Ответы [ 12 ]

34 голосов
/ 21 апреля 2009

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


(подробнее ...)

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

long pow2(int p); // returns 2^p for 0 <= p <= 30

Ваш тест и ваша спецификация выглядят практически одинаково (это своего рода псевдо-xUnit для иллюстрации):

assertEqual(1073741824,pow2(30);
assertEqual(1, pow2(0));
assertException(domainError, pow2(-1));
assertException(domainError, pow2(31));

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

Если вы измените реализацию так, чтобы, скажем, она возвращала 16 бит (помните, что sizeof(long) гарантированно будет не меньше sizeof(short)), тогда эти тесты быстро пройдут неудачно. Тест на уровне интеграции, вероятно, должен провалиться, но не обязательно, и он так же вероятен, как и где-нибудь далеко за вычислением pow2(28).

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

28 голосов
/ 05 мая 2009

Вы спрашиваете плюсы и минусы двух разных вещей (каковы плюсы и минусы езды на лошади против мотоцикла?)

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


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

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

Нет смысла проводить ручные юнит-тесты.

Как насчет рефакторинга, который, кажется, является вашей главной заботой? Если вы проводите рефакторинг только реализации (содержимого) метода, но не его существования или «внешнего поведения», модульный тест все еще действителен и невероятно полезен (вы не можете представить, насколько он полезен, пока не попробуете).

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


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

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


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

В вашем случае, я бы предпочел использовать подход "золотая середина":

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

На самом деле ваш Интеграционный тест только убедится, что ваш "беспорядок" не работает (потому что в начале он не будет работать, верно?), Но не даст вам никакой подсказки по

  • почему не работает
  • если ваша отладка "путаницы" действительно что-то исправляет
  • если ваша отладка "беспорядка" ломает что-то еще

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

20 голосов
/ 21 апреля 2009

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

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

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

16 голосов
/ 04 мая 2009

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

Юнит-тесты:

  • (+) Продолжает тестирование близко к соответствующему коду
  • (+) Относительно легко проверить все пути кода
  • (+) Легко увидеть, если кто-то случайно изменил поведение метода
  • (-) Гораздо сложнее писать для компонентов пользовательского интерфейса, чем для не-GUI

Интеграционные тесты:

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

В идеале оба необходимы.

Примеры:

  • Модульный тест: убедитесь, что входной индекс> = 0 и <длина массива. Что происходит, когда за пределами? Должен ли метод генерировать исключение или возвращать ноль? </p>

  • Интеграционный тест: что видит пользователь, когда вводится отрицательное значение запаса?

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

Лучшая часть о модульном тестировании, которую мы обнаружили, заключается в том, что он заставляет разработчиков идти от кода-> test-> думать, чтобы думать-> test-> code. Если разработчик должен сначала написать тест, [s] он склонен больше думать о том, что может пойти не так, как надо.

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

5 голосов
/ 02 мая 2009

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

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

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

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

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

Так прагматично, это действительно сводится к тому, где вам больше всего нужна скорость в вашем собственном орг / процессе.

3 голосов
/ 04 мая 2009

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

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

Интеграционные тесты, тестирование поведения и инфраструктуры. Модульные тесты обычно только тестируют поведение.

Итак, модульные тесты хороши для тестирования некоторых вещей, интеграционные тесты для других вещей.

Итак, почему юнит тест?

Например, очень сложно проверить граничные условия при интеграционном тестировании. Пример: внутренняя функция ожидает положительное целое число или 0, внешний интерфейс не позволяет вводить отрицательное целое число, как вы гарантируете, что внутренняя функция работает правильно, когда вы передаете ей отрицательное целое число? Может быть, правильное поведение - бросить исключение. Это очень сложно сделать с помощью интеграционного теста.

Итак, для этого вам нужен модульный тест (функции).

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

звонок с HTTP-клиента проверка сервлета вызов из сервлета на бизнес-уровень проверка бизнес-уровня чтение базы данных (спящий режим) преобразование данных бизнес-уровнем запись в базу данных (спящий режим) преобразование данных -> XML XSLT-преобразование -> HTML передача HTML -> клиент

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

Вам нужны как модульные, так и интеграционные тесты.

2 голосов
/ 05 мая 2009

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

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

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

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

2 голосов
/ 04 мая 2009

Джоэл Спольски написал очень интересную статью о юнит-тестировании (это был диалог между Джоэлом и каким-то другим парнем).

Основная идея заключалась в том, что модульные тесты - это очень хорошая вещь, но только если вы используете их в «ограниченном» количестве. Джоэл не рекомендует достигать состояния, когда 100% вашего кода находится в тестовых случаях.

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

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

Как я использую модульные тесты: мне не нравится TDD, поэтому я сначала пишу код, а затем тестирую его (с помощью консоли или браузера), просто чтобы убедиться, что этот код выполняет ненужную работу. И только после этого я добавляю «хитрые» тесты - 50% из них проваливаются после первого тестирования.

Это работает и не занимает много времени.

2 голосов
/ 29 апреля 2009

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

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

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

1 голос
/ 05 мая 2009

У нас есть 4 различных типа тестов в нашем проекте:

  1. Юнит-тесты с насмешками, где необходимо
  2. Тесты БД, которые действуют аналогично модульным тестам, но затем касаются БД и очищаются
  3. Наша логика раскрывается через REST, поэтому у нас есть тесты, которые выполняют HTTP
  4. Тесты Webapp с использованием WatiN, которые на самом деле используют экземпляр IE и проходят через основные функции

Мне нравятся юнит-тесты. Они работают очень быстро (в 100-1000 раз быстрее, чем тесты № 4). Они безопасны от типов, поэтому рефакторинг довольно прост (с хорошей IDE).

Основная проблема в том, сколько работы требуется для их правильного выполнения. Вы должны издеваться над всем: доступ к БД, доступ к сети, другие компоненты. Вы должны украсить немодные классы, получая миллионы в основном бесполезных классов. Вы должны использовать DI, чтобы ваши компоненты не были тесно связаны и, следовательно, не тестировались (обратите внимание, что использование DI на самом деле не является недостатком:)

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

# 3 и особенно # 4 более проблематичны. Они требуют некоторого подмножества производственной среды на сервере сборки. Вы должны создать, развернуть и запустить приложение. Вы должны иметь чистую базу данных каждый раз. Но, в конце концов, это окупается. Тесты Watin требуют постоянной работы, но вы также получаете постоянное тестирование. Мы запускаем тесты для каждого коммита, и очень легко увидеть, когда мы что-то сломаем.

Итак, вернемся к вашему вопросу. Модульные тесты бывают быстрыми (что очень важно, время сборки должно быть меньше, скажем, 10 минут) и их легко реорганизовать. Гораздо проще, чем переписать всю вещь, если ваш дизайн меняется. Если вы используете хороший редактор с хорошей командой find usages (например, IDEA или VS.NET + Resharper), вы всегда можете найти, где тестируется ваш код.

С помощью тестов REST / HTTP вы получите хорошее подтверждение того, что ваша система действительно работает. Но тесты запускаются медленно, поэтому сложно провести полную проверку на этом уровне. Я предполагаю, что ваши методы принимают несколько параметров или, возможно, ввод XML. Чтобы проверить каждый узел в XML или каждый параметр, потребуется десятки или сотни вызовов. Вы можете сделать это с помощью модульных тестов, но вы не можете сделать это с вызовами REST, когда каждый может занять большую долю секунды.

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

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