Некоторые люди утверждают, что интеграционные тесты - это все виды плохого и неправильного - все должно быть проверено модулем, что означает, что вы должны смоделировать зависимости; вариант, который по разным причинам мне не всегда нравится.
Я считаю, что в некоторых случаях юнит-тест просто ничего не доказывает.
Давайте возьмем следующую (тривиальную, наивную) реализацию репозитория (в PHP) в качестве примера:
class ProductRepository
{
private $db;
public function __construct(ConnectionInterface $db) {
$this->db = $db;
}
public function findByKeyword($keyword) {
// this might have a query builder, keyword processing, etc. - this is
// a totally naive example just to illustrate the DB dependency, mkay?
return $this->db->fetch("SELECT * FROM products p"
. " WHERE p.name LIKE :keyword", ['keyword' => $keyword]);
}
}
Допустим, я хочу доказать в тесте, что этот репозиторий действительно может найти продукты, соответствующие различным заданным ключевым словам.
Если не считать интеграционное тестирование с реальным объектом соединения, как я могу знать, что это на самом деле генерирует реальные запросы - и что эти запросы на самом деле делают то, что я думаю, они делают?
Если мне нужно смоделировать объект соединения в модульном тесте, я могу доказать только такие вещи, как «он генерирует ожидаемый запрос» - но это не значит, что он действительно будет работать … то есть, возможно, он генерирует запрос Я ожидал, но, возможно, этот запрос не делает то, что я думаю, что делает.
Другими словами, я чувствую, что тест, который делает утверждения о сгенерированном запросе, по существу не имеет значения, потому что он проверяет, как findByKeyword()
метод был реализован , но это не доказывает, что он действительно работает .
Эта проблема не ограничивается репозиториями или интеграцией баз данных - кажется, что она применима во многих случаях, когда утверждение о применении макета (test-double) только доказывает, как все реализовано, а не собирается ли оно на самом деле работа.
Как вы справляетесь с такими ситуациями?
Являются ли интеграционные тесты действительно «плохими» в таком случае?
Я понимаю, что лучше проверять одну вещь, и я также понимаю, почему интеграционное тестирование приводит к множеству путей кода, которые невозможно проверить, но в случае службы (например, хранилища), единственной целью которой является взаимодействовать с другим компонентом, как вы можете что-то протестировать без тестирования интеграции?
источник
Ответы:
Ваш коллега прав, что все, что может быть подвергнуто модульному тестированию, должно быть модульным, и вы правы, что модульные тесты займут вас лишь так далеко, а не дальше, особенно при написании простых оболочек для сложных внешних служб.
Распространенным способом мышления о тестировании является тестовая пирамида . Эта концепция часто связана с Agile, и многие писали об этом, в том числе Мартин Фаулер (который приписывает ее Майку Кону в « Преуспевании с Agile» ), Алистер Скотт и блог тестирования Google .
Идея состоит в том, что быстродействующие, гибкие модульные тесты являются основой процесса тестирования - должны быть более сфокусированные модульные тесты, чем системные / интеграционные тесты, и больше системных / интеграционных тестов, чем сквозные тесты. По мере приближения к вершине тесты, как правило, занимают больше времени / ресурсов для выполнения, имеют тенденцию быть более хрупкими и ненадежными и менее конкретны в определении того, какая система или файл повреждены ; естественно, предпочтительнее не быть «тяжелым».
С этой точки зрения интеграционные тесты неплохие , но их сильная зависимость может указывать на то, что вы не разработали свои отдельные компоненты так, чтобы их было легко тестировать. Помните, что цель здесь состоит в том, чтобы проверить, что ваше устройство выполняет свои спецификации, в то же время задействуя минимум других хрупких систем : вы можете попробовать базу данных в памяти (которую я считаю тестом, дружественным к юнит-тесту, вдвойне, наряду с мошенничеством). ), например, для тестирования сложных краев, а затем напишите пару интеграционных тестов с реальным ядром базы данных, чтобы убедиться, что основные случаи работают при сборке системы.
В качестве примечания вы упомянули, что написанные вами макеты просто проверяют, как что-то реализовано, а не работает ли оно . Это что-то вроде антипаттерна: тест, который является идеальным зеркалом своей реализации, на самом деле вообще ничего не тестирует. Вместо этого проверьте, что каждый класс или метод ведет себя в соответствии со своей спецификацией , на любом уровне абстракции или реализма, который требуется.
источник
Это все равно, что сказать, что антибиотики вредны - все надо лечить витаминами.
Модульные тесты не могут поймать все - они только проверяют работу компонента в контролируемой среде . Интеграционные тесты проверяют, что все работает вместе , что труднее сделать, но в конечном итоге более значимо.
Хороший, всеобъемлющий процесс тестирования использует оба типа тестов: модульные тесты для проверки бизнес-правил и других вещей, которые можно протестировать независимо, и интеграционные тесты, чтобы убедиться, что все работает вместе.
Вы можете проверить его на уровне базы данных . Запустите запрос с различными параметрами и посмотрите, получите ли вы ожидаемые результаты. Конечно, это означает копирование / вставку любых изменений обратно в «истинный» код. но это позволит вам проверить независимо запрос любых других зависимостей.
источник
Модульные тесты не улавливают все дефекты. Но они дешевле в настройке и (повторном) запуске по сравнению с другими видами тестов. Модульные тесты оправданы сочетанием умеренной стоимости и низкой стоимости.
Вот таблица, показывающая уровень обнаружения дефектов для различных видов тестирования.
источник: стр.470 в Коде, Полном 2 Макконнеллом
источник
Нет, они не плохие. Надеюсь, у вас должны быть модульные и интеграционные тесты. Они используются и работают на разных этапах цикла разработки.
Модульные тесты
Модульные тесты должны выполняться на сервере сборки и локально после компиляции кода. Если какой-либо модульный тест не пройден, сборка должна завершиться неудачей или не зафиксировать обновление кода, пока тесты не будут исправлены. Причина, по которой мы хотим изолировать модульные тесты, заключается в том, что мы хотим, чтобы сервер сборки мог выполнять все тесты без всех зависимостей. Тогда мы могли бы запустить сборку без всех необходимых сложных зависимостей и провести множество очень быстрых тестов.
Итак, для базы данных нужно иметь что-то вроде:
Теперь реальная реализация IRepository пойдет в базу данных, чтобы получить продукты, но для модульного тестирования можно смоделировать IRepository с поддельным, чтобы выполнить все тесты по мере необходимости, без базы данных actaul, поскольку мы можем моделировать все виды списков продуктов. быть возвращенным из фиктивного экземпляра и протестировать любую бизнес-логику с имитированными данными.
Интеграционные тесты
Интеграционные тесты - это, как правило, тесты пересечения границы. Мы хотим запустить эти тесты на сервере развертывания (в реальной среде), в изолированной программной среде или даже локально (указывает на изолированную программную среду). Они не запускаются на сервере сборки. После того как программное обеспечение было развернуто в среде, оно обычно запускается как действие после развертывания. Они могут быть автоматизированы с помощью утилит командной строки. Например, мы можем запустить nUnit из командной строки, если классифицируем все тесты интеграции, которые мы хотим вызвать. Они на самом деле вызывают реальный репозиторий с реальным вызовом базы данных. Эти типы тестов помогают с:
Эти тесты иногда сложнее выполнить, так как нам может потребоваться настроить и / или удалить их. Попробуйте добавить продукт. Мы, вероятно, хотим добавить продукт, запросить его, чтобы увидеть, был ли он добавлен, а затем, когда мы закончим, удалите его. Мы не хотим добавлять 100 или 1000 «интегрированных» продуктов, поэтому требуется дополнительная настройка.
Интеграционные тесты могут оказаться весьма полезными для проверки среды и проверки работоспособности.
Один должен иметь оба.
источник
Тесты интеграции баз данных не плохие. Более того, они необходимы.
Возможно, ваше приложение разбито на слои, и это хорошо. Вы можете проверить каждый слой изолированно, издеваясь над соседними слоями, и это тоже хорошо. Но независимо от того, сколько слоев абстракции вы создаете, в какой-то момент должен быть слой, который выполняет грязную работу - фактически общается с базой данных. Если вы не проверяете это, вы не проверяете вообще. Если вы тестируете слой n с помощью имитации слоя n-1, вы оцениваете предположение, что уровень n работает при условии, что уровень n-1 работает. Чтобы это работало, вы должны как-то доказать, что слой 0 работает.
Хотя теоретически вы можете тестировать базу данных, анализируя и интерпретируя сгенерированный SQL, гораздо проще и надежнее создавать тестовую базу данных на лету и общаться с ней.
Заключение
Какова уверенность, полученная в результате модульного тестирования вашего абстрактного репозитория , слоев Ethereal Object-Relational-Mapper , Generic Active Record , теоретической персистентности , когда в конце концов ваш сгенерированный SQL содержит синтаксическую ошибку?
источник
Вам нужны оба.
В вашем примере, если вы проверяли, что база данных находится в определенном состоянии, при
findByKeyword
запуске метода вы возвращаете данные, которые ожидаете, что это прекрасный интеграционный тест.В любом другом коде, использующем этот
findByKeyword
метод, вы хотите контролировать то, что подается в тест, чтобы вы могли возвращать нули или правильные слова для вашего теста или что-то еще, после чего вы высмеиваете зависимость от базы данных, чтобы точно знать, что будет делать ваш тест получать (и вы теряете накладные расходы на подключение к базе данных и проверку правильности данных в ней)источник
Автор статьи в блоге, на которую вы ссылаетесь, в основном касается потенциальной сложности, которая может возникнуть в результате интегрированных тестов (хотя она написана очень самоуверенно и категорично). Однако интегрированные тесты не обязательно плохие, а некоторые на самом деле более полезны, чем чистые модульные тесты. Это действительно зависит от контекста вашего приложения и того, что вы пытаетесь протестировать.
Многие приложения сегодня просто не будут работать вообще, если их сервер базы данных выйдет из строя. По крайней мере, подумайте об этом в контексте функции, которую вы пытаетесь протестировать.
С одной стороны, если то, что вы пытаетесь проверить, не зависит или может вообще не зависеть от базы данных, то напишите свой тест таким образом, чтобы он даже не пытался использовать база данных (просто предоставьте фиктивные данные по мере необходимости). Например, если вы пытаетесь проверить некоторую логику аутентификации при обслуживании веб-страницы (например), вероятно, было бы неплохо отсоединить ее от БД вообще (при условии, что вы не полагаетесь на БД для аутентификации, или что Вы можете издеваться над этим достаточно легко).
С другой стороны, если это функция, которая напрямую зависит от вашей базы данных и которая вообще не будет работать в реальной среде, если база данных будет недоступна, тогда высмеивают то, что делает БД в вашем клиентском коде БД (т.е. слой, использующий это DB) не обязательно имеет смысл.
Например, если вы знаете, что ваше приложение будет полагаться на базу данных (и, возможно, на конкретную систему баз данных), насмешка над поведением базы данных ради этого часто будет пустой тратой времени. Механизмы баз данных (особенно СУБД) представляют собой сложные системы. Несколько строк SQL на самом деле могут выполнять большую работу, которую было бы сложно смоделировать (на самом деле, если ваш SQL-запрос имеет длину в несколько строк, скорее всего, вам потребуется гораздо больше строк Java / PHP / C # / Python код для получения того же результата внутри): дублирование логики, которую вы уже реализовали в БД, не имеет смысла, и проверка этого тестового кода сама по себе станет проблемой.
Я бы не стал относиться к этому как к проблеме модульного тестирования по сравнению с интегрированным тестом , а скорее смотрю на объем тестируемого объекта. Общие проблемы модульного и интеграционного тестирования сохраняются: вам нужен достаточно реалистичный набор тестовых данных и тестовых случаев, но также достаточно маленький для быстрого выполнения тестов.
Время для сброса базы данных и повторного заполнения тестовыми данными является аспектом, который следует учитывать; вы обычно сравниваете это со временем, которое требуется для написания этого фиктивного кода (который вы также должны будете поддерживать в конечном итоге).
Другим важным моментом является степень зависимости вашего приложения от базы данных.
источник
Вы правы, считая такой модульный тест неполным. Неполнота заключается в том, что интерфейс базы данных подвергается проверке. Ожидания или утверждения такого наивного издевательства являются неполными.
Чтобы завершить его, вам потребуется сэкономить достаточно времени и ресурсов для написания или интеграции механизма правил SQL, который гарантировал бы, что оператор SQL, выдаваемый испытуемым, приведет к ожидаемым операциям.
Однако часто забываемая и несколько дорогая альтернатива / компаньон для насмешек - это «виртуализация» .
Можете ли вы раскрутить временный, находящийся в памяти, но «настоящий» экземпляр БД для тестирования одной функции? да ? там у вас есть лучший тест, который проверяет фактические данные, сохраненные и извлеченные.
Теперь можно сказать, что вы превратили юнит-тест в интеграционный тест. Существуют различные мнения о том, где провести черту для классификации между юнит-тестами и интеграционными тестами. ИМХО, «юнит» - это произвольное определение и должно соответствовать вашим потребностям.
источник
Unit Tests
иIntegration Tests
являются orthgonal друг к другу. Они предлагают другой взгляд на приложение, которое вы создаете. Обычно вы хотите оба . Но момент времени отличается, когда вы хотите, какие виды тестов.Наиболее часто вы хотите
Unit Tests
. Модульные тесты фокусируются на небольшой части тестируемого кода - то, что именно называется,unit
остается читателю. Но та цель проста: получение быстрых обратной связи , когда и где ваш код сломался . Тем не менее, должно быть ясно, что вызовы фактической БД являются ненулевыми .С другой стороны, есть вещи, которые можно тестировать только в тяжелых условиях без базы данных. Возможно, в вашем коде есть условие состязания, и при обращении к БД возникает нарушение,
unique constraint
которое может быть выдано только в том случае, если вы действительно используете свою систему. Но такие тесты дороги, вы не можете (и не хотите) запускать их так часто, какunit tests
.источник
В мире .Net у меня есть привычка создавать тестовый проект и создавать тесты как метод кодирования / отладки / тестирования в оба конца без пользовательского интерфейса. Для меня это эффективный способ развития. Мне было не так интересно запускать все тесты для каждой сборки (потому что это замедляет мой рабочий процесс разработки), но я понимаю полезность этого для большой команды. Тем не менее, вы могли бы создать правило, согласно которому перед фиксацией кода все тесты должны быть запущены и пройдены (если выполнение тестов занимает больше времени, поскольку база данных действительно поражена).
Макетирование слоя доступа к данным (DAO) и отсутствие реального попадания в базу данных не только не позволяет мне кодировать то, что мне нравится и к чему я привык, но и пропускает большую часть фактической базы кода. Если вы на самом деле не тестируете слой доступа к данным и базу данных, а просто притворяетесь, а затем тратите много времени на макетирование, я не понимаю полезности этого подхода для реального тестирования моего кода. Я тестирую маленький кусок вместо большего с одним тестом. Я понимаю, что мой подход может быть больше похож на интеграционный тест, но кажется, что модульный тест с макетом - это лишняя трата времени, если вы на самом деле просто пишете интеграционный тест один раз и сначала. Это также хороший способ разработки и отладки.
Фактически, какое-то время я уже знал о TDD и Behavior Driven Design (BDD) и думал о способах его использования, но сложно добавить модульные тесты задним числом. Возможно, я ошибаюсь, но написание теста, охватывающего больше кода от начала до конца с включенной базой данных, кажется гораздо более полным и более приоритетным тестом для написания, который охватывает больше кода и является более эффективным способом написания тестов.
На самом деле, я думаю, что что-то вроде Behavior Driven Design (BDD), которое пытается тестировать от начала до конца на доменно-специфическом языке (DSL), должно быть подходящим способом. У нас есть SpecFlow в мире .Net, но он начался как открытый исходный код с Cucumber.
https://cucumber.io/
Я просто не впечатлен истинной полезностью написанного мною теста, который выводит из строя слой доступа к данным и не затрагивает базу данных. Возвращенный объект не попал в базу данных и не был заполнен данными. Это был совершенно пустой объект, который я должен был макетировать неестественным способом. Я просто думаю, что это пустая трата времени.
Согласно Stack Overflow, насмешка используется, когда реальные объекты нецелесообразно включать в модульный тест.
https://stackoverflow.com/questions/2665812/what-is-mocking
«Моккинг в основном используется в модульном тестировании. У тестируемого объекта могут быть зависимости от других (сложных) объектов. Чтобы изолировать поведение объекта, который вы хотите проверить, вы заменяете другие объекты имитациями, которые имитируют поведение реальных объектов. Это полезно, если реальные объекты нецелесообразно включать в модульный тест ».
Мой аргумент заключается в том, что если я что-то кодирую от начала до конца (от веб-интерфейса пользователя до бизнес-уровня, от уровня доступа к данным до базы данных, в оба конца), прежде чем я проверю что-либо в качестве разработчика, я собираюсь протестировать этот поток в оба конца. Если я отключаю UI, отлаживаю и тестирую этот поток, начиная с теста, я тестирую все, кроме UI, и возвращаю именно то, что ожидает UI. Все, что у меня осталось, это отправить интерфейс, что он хочет.
У меня есть более полный тест, который является частью моего естественного рабочего процесса разработки. Для меня это должен быть тест с наивысшим приоритетом, который охватывает максимально возможное тестирование фактической пользовательской спецификации. Если я никогда не буду создавать какие-либо другие более детальные тесты, по крайней мере, у меня будет еще один полный тест, который подтвердит мою желаемую функциональность.
Соучредитель Stack Exchange не убежден в преимуществах 100% покрытия модульных тестов. Я тоже нет. Я бы взял более полный «интеграционный тест», который поразил бы базу данных, а не поддерживал кучу имитаций базы данных в любой день.
https://www.joelonsoftware.com/2009/01/31/from-podcast-38/
источник
Внешние зависимости должны быть смоделированы, потому что вы не можете управлять ими (они могут пройти во время фазы тестирования интеграции, но потерпеть неудачу в производстве). Диски могут выходить из строя, соединения с базой данных могут прерываться по ряду причин, могут быть проблемы с сетью и т. Д. Наличие интеграционных тестов не дает никакой дополнительной уверенности, потому что это все проблемы, которые могут произойти во время выполнения.
С настоящими юнит-тестами вы тестируете в пределах песочницы, и это должно быть ясно. Если разработчик написал запрос SQL, который потерпел неудачу в QA / PROD, это означает, что он даже не тестировал его один раз до того времени.
источник