Что такое ифт тестирование
Интеграционное тестирование
Интеграционное тестирование рекомендуется проводить перед началом системного тестирования. Данный вид тестирования следует проводить как можно раньше, поскольку дефекты интеграции, как правило, имеют архитектурный характер, их исправление на поздних стадиях разработки является рискованным и может обойтись значительно дороже. Для ускорения начала процесса тестирования мы рекомендуем воспользоваться услугой по разработке эмуляторов внешних систем.
В рамках интеграционного тестирования также может проводиться регрессионное тестирование с целью проверки сделанных в приложении или окружающей среде изменений и работоспособности унаследованной функциональности.
Протестируем системы любой сложности: поисковые, биллинговые, процессинговые, SAP и многие другие
Ключевые преимущества
Интеграционное тестирование позволяет имитировать действия пользователей и быстро получать подтверждение, что программный продукт успешно взаимодействует с другими системами. Такой подход гарантирует сразу несколько преимуществ:
Основные задачи
Главная задача это поиск ошибок, связанных с взаимодействием модулей системы или нескольких систем. В результате все смежные системы и модули одной системы должны работать согласованно.
Способы проведения данного типа тестирования подбираются в зависимости от интеграционных решений.
Этапы
Провести тестирование функционала CRM при взаимодействии со смежными системами.
Была протестирована интеграционная цепочка из трех ESB-сервисов по получению информации о пластиковых картах клиентов банка.
Повысить надежность системы, обеспечивающей выполнение банковских операций.
Проведение функционального, регрессионного и интеграционного тестирования функционала автоматизированной системы банка.
В задачи проекта входили: анализ требований, подготовка тест-кейсов, поддержка тестирования разработчиков, внутреннее системное тестирование (включая интеграционное), приемочное тестирование.
Функциональное тестирование системы осуществлялось в процессе ее внедрения. Была проведена проверка широкого спектра интерфейсов и back-end-разработок. Проектная команда «Апланы» осуществила проверку взаимодействия Oracle Siebel CRM с системами ЦФТ РБО, 1С, скоринга, а также с функционалом колл-центра..
Интеграционное тестирование
20.1. Задачи и цели интеграционного тестирования
20.2. Организация интеграционного тестирования
20.2.1. Структурная классификация методов интеграционного тестирования
Как правило, интеграционное тестирование проводится уже по завершении модульного тестирования для всех интегрируемых модулей. Однако это далеко не всегда так. Существует несколько методов проведения интеграционного тестирования:
Все эти методики основываются на знаниях об архитектуре системы, которая часто изображается в виде структурных диаграмм или диаграмм вызовов функций [10]. Каждый узел на такой диаграмме представляет собой программный модуль, а стрелки между ними представляют собой зависимость по вызовам между модулями. Основное различие методик интеграционного тестирования заключается в направлении движения по этим диаграммам и в широте охвата за одну итерацию.
Восходящее тестирование. При использовании этого метода подразумевается, что сначала тестируются все программные модули, входящие в состав системы и только затем они объединяются для интеграционного тестирования. При таком подходе значительно упрощается локализация ошибок: если модули протестированы по отдельности, то ошибка при их совместной работе есть проблема их интерфейса. При таком подходе область поиска проблем у тестировщика достаточно узка, и поэтому гораздо выше вероятность правильно идентифицировать дефект.
Монолитное тестирование имеет ряд серьезных недостатков.
Нисходящее тестирование предполагает, что процесс интеграционного тестирования движется следом за разработкой. Сначала тестируют только самый верхний управляющий уровень системы, без модулей более низкого уровня. Затем постепенно с более высокоуровневыми модулями интегрируются более низкоуровневые. В результате применения такого метода отпадает необходимость в драйверах (роль драйвера выполняет более высокоуровневый модуль системы), однако сохраняется нужда в заглушках (Рис 20.2).
У разных специалистов в области тестирования разные мнения по поводу того, какой из методов более удобен при реальном тестировании программных систем. Йордан доказывает, что нисходящее тестирование наиболее приемлемо в реальных ситуациях [27], а Майерс полагает, что каждый из подходов имеет свои достоинства и недостатки, но в целом восходящий метод лучше [28].
В литературе часто упоминается метод интеграционного тестирования объектно-ориентированных программных систем, который основан на выделении кластеров классов, имеющих вместе некоторую замкнутую и законченную функциональность [10]. По своей сути такой подход не является новым типом интеграционного тестирования, просто меняется минимальный элемент, получаемый в результате интеграции. При интеграции модулей на процедурных языках программирования можно интегрировать любое количество модулей при условии разработки заглушек. При интеграции классов в кластеры существует достаточно нестрогое ограничение на законченность функциональности кластера. Однако, даже в случае объектно-ориентированных систем возможно интегрировать любое количество классов при помощи классов-заглушек.
Вне зависимости от применяемого метода интеграционного тестирования, необходимо учитывать степень покрытия интеграционными тестами функциональности системы. В работе [17] был предложен способ оценки степени покрытия, основанный на управляющих вызовах между функциями и потоках данных. При такой оценке код всех модулей на структурной диаграмме системы должен быть выполнен (должны быть покрыты все узлы), все вызовы должны быть выполнены хотя бы единожды (должны быть покрыты все связи между узлами на структурной диаграмме), все последовательности вызовов должны быть выполнены хотя бы один раз (все пути на структурной диаграмме должны быть покрыты) [10].
Интеграционные тесты в микросервисах
Авторизуйтесь
Интеграционные тесты в микросервисах
Senior Developer в DataArt
Кто любит, когда в пятницу вечером из продакшена прилетает баг и надо срочно его фиксить? Или когда все юнит-тесты зеленые, а на тестовом энвайроменте сервис не запускается? Скорее всего — никто. Все мы заинтересованы в качестве продукта, над которым работаем. Не только потому что мы ответственные разработчики, но и потому что любим отдохнуть в выходные.
Но появление багов неизбежно. Чтобы обеспечить качество продукта, нам необходимо выявлять их как можно раньше — в идеале, до того как наше решение ушло в продакшн. Для этого есть разные виды автоматического тестирования, начиная с выявления ошибок компиляции, заканчивая UI-тестированием на препродакшене и хорошо настроенными CI-процессами.
Написание тестов — не такая простая задача, какой кажется на первый взгляд. Мы всегда должны выбирать правильные подходы и где-то чем-то жертвовать для максимальной выгоды. В статье я хотел бы сфокусироваться на интеграционном тестировании и обсудить несколько подходов к нему — не всегда понятно, сколько нужно таких тестов и какие кейсы они должны покрывать.
Для начала определимся, что такое интеграционное тестирование или, как его еще называют, тестирование сервиса. Рассмотрим на примере небольшого проекта, какие вообще бывают типы тестов. Проект состоит из двух микросервисов: 1) сервис A — stateful и хранит состояние в некой DB; 2) сервис B — stateless и может являться некоторым воркером. Еще у нас есть WebApp, через которое мы взаимодействуем с нашими сервисами.
Самый первый вид тестирования — Unit-тестирование. Не буду углубляться в подробности, т. к. он всем хорошо известен. Просто скажу, что Unit-тестирование или, как его еще называют, изолированное тестирование — тестирование на уровне класса или группы классов, с помощью которого можно проверить каждый метод или функцию. Такое тестирование дает уверенность, что отдельные части кода работают, но не говорит, работает ли код в целом.
Этот минус решает Integration-тестирование, или тестирование сервиса. Здесь мы проверяем весь сервис в изоляции. Т.е. мы мокаем все внешние зависимости на другие сервисы. В нашем примере получаются сквозные тесты микросервиса A от HTTP request до DB и обратно. При этом виде тестирования мы можем быть уверены, что правильно настроен DI, все компоненты нормально работают вместе, и поведение соответствует бизнес-сценариям.
Но мы живем в микросервисном мире, и проверка каждого сервиса по отдельности не дает уверенности, что вся система работает. Тут на помощь приходит последний вид тестирования — end-to-end (E2E), или UI-тестирование. Оно может быть автоматическим и ручным. Проверяется работа всех компонентов системы вместе на соответствие бизнес-требованиям. Если Unit- и Integration-тестирование — по большей части, проверка с технической точки зрения, то E2E — проверка ожиданий пользователя от работы системы.
Ни одно обсуждение тестирования нельзя считать полным без упоминания пирамиды тестирования, предложенной Майком Коном в книге «Succeeding with Agile». Согласно пирамиде, самые многочисленные тесты — Unit. Они маленькие, изолированные и могут проверить любую отдельную часть вашего сервиса, вплоть до строчки кода. Далее — Integration. Это более объемные тесты, которые проходят через весь pipeline сервиса. И на вершине — E2E тесты, которые дают нам большую уверенность в работе системы, но самые долгие в имплементации и самыми неинформативные, поэтому их должно быть меньше всего. Также пирамида говорит, что, чем ближе к основанию, тем больше скорость написания тестов. Чем дальше, тем дороже написание, поддержка, и, в случае дефекта, — поиск причины.
Это была классическая стратегия тестирования, давайте рассмотрим и другие.
Перевернутая пирамида, или рожок тестирования. В этой стратегии основной упор — на E2E-тесты, т. к. они дают наибольшую уверенность, что работа всей системы полностью соответствует ожиданиям конечного пользователя. Одновременно это ловушка. С одной стороны, мы уверены в качестве продукта, а с другой — тратим огромное время на получение фидбека, что система работает после внесения каких-либо изменений. Если каждый Unit-тест выполняется за миллисекунды, Integration — за секунды, E2E может занимать несколько десятков секунд или минуты. И даже если тест выявил дефект, мы точно не знаем, в каком сервисе и в каком месте кода произошел сбой. Мы должны будем потратить достаточно много времени на поиск причины бага.
Я работал над одним проектом, в котором основными тестами были E2E, и полный прогон занимал несколько часов, поэтому их запускали только по ночам. Т. е. фидбэк по новой фиче мы получали только на следующее утро, и в случае дефекта начался долгий поиск причин. При таком подходе очень много времени расходовалось на поиски. В ожидании прохождения тестов параллельно могла начаться работа над другой задачей. Тогда приходилось отложить текущую задачу и вернуться к предыдущей. Это всеотрицательно сказывалось на продуктивности. Как разработчик я хочу максимально быстро узнавать о наличии дефекта. В идеале — на своей локальной машине во время имплементации. Этот подход не дает такой возможности. Поэтому я не рекомендую его никому.
Следующая стратегия — сота тестирования (testing honeycomb). Здесь основной упор делается на integration-тесты. Она идеально подходит для микросервисов, в частности, ее используют в Spotify.
Kогда сервис небольшой (как говорят, размер микросервиса должен быть таким, чтобы agile-команда смогла его полностью переписать за 1 спринт) и в нем мало бизнес-логики, этот подход дает большие плюсы:
Время идет, и наш сервис развивается, в нем появляется больше кода, и становится сложнее бизнес-логика. При использовании стратегии призмы тестирования у нас могут возникнуть большие проблемы. Какие? Давайте разберем.
Все мы сталкивались с ситуацией, когда все тесты зеленые, но выявляется баг. Что мы можем сделать после этого? Найти причину, закрыть ее Integration-тестом, который воспроизводит проблему, пофиксить и выпустить патч. И в этом кроется основной недостаток Integration и любого другого вида сквозного тестирования. Мы упускаем из виду проблемы в архитектуре приложения. Как бы удивительно ни звучало, Unit-тесты нужны не только для проверки бизнес-логики и поиска багов, но и для выявления проблем в дизайне. Всем известно, что если вы не можете покрыть какую-то часть кода Unit-тестами, у вас проблемы в архитектуре. Unit-тесты позволяют заметить их на ранних стадиях. Например, если для написания одного Unit-теста вам необходимо замокать кучу зависимостей, есть проблема, необходимо провести рефакторинг.
Таким образом, Unit-тесты помогают не только находить логические ошибки, но и выявлять проблемы в дизайне. А что происходит, если вы их не пишете? Архитектура ухудшается и становится более запутанной. Повышается вероятность сделать ошибку при каких-либо изменениях. Это как гидра: пофиксили что-то здесь — сломалось что-то там. И круг замкнулся, у вас опять все тесты зеленые, но появляется ошибка, вы покрываете ее Integration-тестом, фиксите следствие, а не причину. И так опять, и опять, и опять. В итоге ваш проект состоит только из костылей, а вы — несчастны.
Другая проблема — время. Чем больше у вас Integration тестов, тем больше времени занимает их полный прогон. Для микросервисов со сложной бизнес-логикой и взаимодействием прогон всех тестов может занять ни один десяток минут. Это снова негативно сказыается на производительности и качестве кода. Вы будете очень редко запускать все тесты, а, скорее всего, их полный прогон будет только на CI после открытия PR.
Каков же рецепт? Универсального решения не существует. Найти стратегию на все случаи жизни невозможно, каждая имеет сильные и слабые стороны. И только одним видом тестирования не обойтись, необходимо брать лучшее от всех подходов и грамотно их сочетать.
Основная рекомендация — используйте Integration-тесты для проверки сервиса на соответствие бизнес-требованиям. Воспроизведите основные бизнес-кейсы для сервиса в Integration-тестах, но не пытайтесь проверять бизнес-логику через них. Для этого есть Unit-тесты. Тогда при рефакторинге и/или переезде на другую DB вам ничего не нужно будет делать с самими тестами. Вы всегда будете уверены, что работа сервиса соответствует требованиям.
Пока ваш сервис живет и функционирует, вы можете менять стратегию, если видите, что предыдущий подход больше не дает бенефитов. Допустим, у вас маленький сервис с небольшим количеством CRUD-операций, используйте подход «соты тестрования». С развитием сервиса, если заметите, что тесты начинают занимать все больше и больше времени, а внесение изменений становится все сложнее, переходите к стратегии пирамиды тестирования.При выявлении баги все-таки закрывайте ее Unit-тестами, а не интеграционниками.
Спасибо за внимание. Безбажных вам сервисов.
Виды тестирования и подходы к их применению
Блочное (модульное, unit testing) тестирование наиболее понятное для программиста. Фактически это тестирование методов какого-то класса программы в изоляции от остальной программы.
Не всякий класс легко покрыть unit тестами. При проектировании нужно учитывать возможность тестируемости и зависимости класса делать явными. Чтобы гарантировать тестируемость можно применять TDD методологию, которая предписывает сначала писать тест, а потом код реализации тестируемого метода. Тогда архитектура получается тестируемой. Распутывание зависимостей можно осуществить с помощью Dependency Injection. Тогда каждой зависимости явно сопоставляется интерфейс и явно определяется как инжектируется зависимость — в конструктор, в свойство или в метод.
Для осуществления unit тестирования существуют специальные фреймворки. Например, NUnit или тестовый фреймфорк из Visual Studio 2008. Для возможности тестирования классов в изоляции существуют специальные Mock фреймворки. Например, Rhino Mocks. Они позволяют по интерфейсам автоматически создавать заглушки для классов-зависимостей, задавая у них требуемое поведение.
По unit тестированию написано много статей. Мне очень нравится MSDN статья Write Maintainable Unit Tests That Will Save You Time And Tears, в которой хорошо и понятно рассказывается как создавать тесты, поддерживать которые со временем не становится обременительно.
Интеграционное тестирование
Интеграционное тестирование, на мой взгляд, наиболее сложное для понимания. Есть определение — это тестирование взаимодействия нескольких классов, выполняющих вместе какую-то работу. Однако как по такому определению тестировать не понятно. Можно, конечно, отталкиваться от других видов тестирования. Но это чревато.
Если к нему подходить как к unit-тестированию, у которого в тестах зависимости не заменяются mock-объектами, то получаем проблемы. Для хорошего покрытия нужно написать много тестов, так как количество возможных сочетаний взаимодействующих компонент — это полиномиальная зависимость. Кроме того, unit-тесты тестируют как именно осуществляется взаимодействие (см. тестирование методом белого ящика). Из-за этого после рефакторинга, когда какое-то взаимодействие оказалось выделенным в новый класс, тесты рушатся. Нужно применять менее инвазивный метод.
Подходить же к интеграционному тестированию как к более детализированному системному тоже не получается. В этом случае наоборот тестов будет мало для проверки всех используемых в программе взаимодействий. Системное тестирование слишком высокоуровневое.
Идея простая. У нас есть входные данные, и мы знаем как программа должна отработать на них. Запишем эти знания в текстовый файл. Это будет спецификация к тестовым данным, в которой записано, какие результаты ожидаются от программы. Тестирование же будет определять соответствие спецификации и того, что действительно находит программа.
Проиллюстрирую на примере. Программа конвертирует один формат документа в другой. Конвертирование хитрое и с кучей математических расчетов. Заказчик передал набор типичных документов, которые ему требуется конвертировать. Для каждого такого документа мы напишем спецификацию, где запишем всякие промежуточные результаты, до которых дойдет наша программа при конвертировании. 1) Допустим в присланных документах есть несколько разделов. Тогда в спецификации мы можем указать, что у разбираемого документа должны быть разделы с указанными именами: $SectionNames = Введение, Текст статьи, Заключение, Литература 2) Другой пример. При конвертировании нужно разбивать геометрические фигуры на примитивы. Разбиение считается удачным, если в сумме все примитивы полностью покрывают оригинальную фигуру. Из присланных документов выберем различные фигуры и для них напишем свои спецификации. Факт покрываемости фигуры примитивами можно отразить так: $IsCoverable = true |
Понятно, что для проверки подобных спецификаций потребуется движок, который бы считывал спецификации и проверял их соответствие поведению программы. Я такой движок написал и остался доволен данным подходом. Скоро выложу движок в Open Source. (UPD: Выложил)
Данный вид тестирования является интеграционным, так как при проверке вызывается код взаимодействия нескольких классов. Причем важен только результат взаимодействия, а не детали и порядок вызовов. Поэтому на тесты не влияет рефакторинг кода. Не происходит избыточного или недостаточного тестирования — тестируются только те взаимодействия, которые встречаются при обработке реальных данных. Сами тесты легко поддерживать, так как спецификация хорошо читается и ее просто изменять в соответствии с новыми требованиями.
Системное тестирование
Системное — это тестирование программы в целом. Для небольших проектов это, как правило, ручное тестирование — запустил, пощелкал, убедился, что (не) работает. Можно автоматизировать. К автоматизации есть два подхода.
Первый подход — это использовать вариацию MVC паттерна — Passive View (вот еще хорошая статья по вариациям MVC паттерна) и формализовать взаимодействие пользователя с GUI в коде. Тогда системное тестирование сводится к тестированию Presenter классов, а также логики переходов между View. Но тут есть нюанс. Если тестировать Presenter классы в контексте системного тестирования, то необходимо как можно меньше зависимостей подменять mock объектами. И тут появляется проблема инициализации и приведения программы в нужное для начала тестирования состояние. В упомянутой выше статье Scenario Driven Tests об этом говорится подробнее.
Для чего нужно интеграционное тестирование?
Эта статья является конспектом книги «Принципы юнит-тестирования». Материал статьи посвящен интеграционным тестам.
Юнит-тесты прекрасно справляются с проверкой бизнес-логики, но проверять эту логику «в вакууме» недостаточно. Необходимо проверять, как разные ее части интегрируются друг с другом и внешними системами: базой данных, шиной сообщений и т. д.
В этой статье рассматривается роль интеграционных тестов, когда их следует использовать и когда лучше положиться на классические юнит-тесты. Также затронем эффективное написание интеграционных тестов.
Что такое интеграционный тест?
Юнит-тест удовлетворяет следующим трем требованиям:
проверяет правильность работы одной единицы поведения;
и в изоляции от других тестов.
Тест, который не удовлетворяет хотя бы одному из этих трех требований, относится к категории интеграционных тестов. На практике интеграционные тесты почти всегда проверяют, как система работает в интеграции с внепроцессными зависимостями.
Если все внепроцессные зависимости заменить моками, никакие зависимости не будут совместно использоваться между тестами, благодаря чему эти тесты останутся быстрыми и сохранят свою изоляцию друг от друга и становятся юнит-тестами. Тем не менее во многих приложениях существует внепроцессная зависимость, которую невозможно заменить моком. Обычно это база данных — зависимость, не видимая другими приложениями.
Важно поддерживать баланс между юнит- и интеграционными тестами. Работа напрямую с внепроцессными зависимостями замедляет интеграционные тесты. Кроме того, их сопровождение также обходится дороже.
С другой стороны, интеграционные тесты проходят через больший объем кода, что делает их более эффективными в защите от багов по сравнению с юнит-тестами. Они также более отделены от рабочего кода, а, следовательно, обладают большей устойчивостью к его рефакторингу.
Соотношение между юнит- и интеграционными тестами зависит от особенностей проекта, но общее правило выглядит так: проверьте как можно больше пограничных случаев бизнес-сценария юнит-тестами; используйте интеграционные тесты для покрытия одного позитивного пути, а также всех граничных случаев, которые не покрываются юнит-тестами.
Для интеграционного теста выберите самый длинный позитивный путь, проверяющий взаимодействия со всеми внепроцессными зависимостями. Если не существует одного пути, проходящего через все такие взаимодействия, напишите дополнительные интеграционные тесты — столько, сколько потребуется для отражения взаимодействий с каждой внешней системой.
Какие из внепроцессных зависимостей должны проверяться напрямую
Все внепроцессные зависимости делятся на две категории.
Управляемые зависимости (внепроцессные зависимости, находящиеся под вашим полным контролем): эти зависимости доступны только через ваше приложение; взаимодействия с ними не видны внешнему миру. Типичный пример — база данных.
Неуправляемые зависимости (внепроцессные зависимости, которые не находятся под вашим полным контролем) — результат взаимодействия с такими зависимостями виден извне. В качестве примеров можно привести сервер SMTP и шину сообщений.
Взаимодействия с управляемыми зависимостями относятся к деталям имплементации. И наоборот, взаимодействия с неуправляемыми зависимостями являются частью наблюдаемого поведения вашей системы.
Рис. 1 – Взаимодействия с зависимостями
Это различие приводит к тому, что такие зависимости по-разному обрабатываются в интеграционных тестах. Взаимодействия с управляемыми зависимостями являются деталями имплементации. Использовать их следует в исходном виде в интеграционных тестах. Взаимодействия с неуправляемыми зависимостями являются частью наблюдаемого поведения системы. Такие зависимости должны заменяться моками.
Требование о сохранении схемы взаимодействий с неуправляемыми зависимостями обусловлено необходимостью поддержания обратной совместимости с такими зависимостями. Моки идеально подходят для этой задачи. Они позволяют обеспечить неизменность схемы взаимодействий при любых возможных рефакторингов. Поддерживать обратную совместимость во взаимодействиях с управляемыми зависимостями не обязательно, потому что никто, кроме вашего приложения, с ними не работает. Внешних клиентов не интересует, как устроена ваша база данных; важно только итоговое состояние вашей системы. Использование реальных экземпляров управляемых зависимостей в интеграционных тестах помогает проверить это итоговое состояние с точки зрения внешних клиентов.
Иногда встречаются внепроцессные зависимости, обладающие свойствами как управляемых, так и неуправляемых зависимостей. Хорошим примером служит база данных, доступная для других приложений.
База данных — не лучший механизм для интеграции между системами, потому что она связывает эти системы друг с другом и усложняет дальнейшую их разработку. Используйте это решение только в случае, если других вариантов нет. Правильнее осуществлять интеграцию через API (для синхронных взаимодействий) или шину сообщений (для асинхронных взаимодействий).
В этом случае следует рассматривать таблицы, видимые для других приложений, как неуправляемую зависимость. Такие таблицы фактически выполняют функции шины сообщений, а их строки играют роль сообщений. Используйте моки, чтобы гарантировать неизменность схемы взаимодействий с этими таблицами. В то же время следует рассматривать остальные части базы данных как управляемую зависимость и проверять ее итоговое состояние, а не взаимодействия с ней.
Рис. 2 – БД, доступная для внешних приложений
Основные приемы интеграционного тестирования
Существует несколько общих рекомендаций, которые помогут извлечь максимальную пользу из интеграционных тестов:
явное определение границ доменной модели (модели предметной области);
сокращение количества слоев в приложении;
устранение циклических зависимостей.
Явное определение границ модели предметной области. Доменная модель представляет собой совокупность знаний о предметной области задачи, для решения которой предназначен ваш проект. Данная практика помогает с тестированием. Юнит-тесты должны ориентироваться на доменную модель и алгоритмы, тогда как интеграционные тесты — на контроллеры. Таким образом, четкое разграничение между доменными классами и контроллерами также помогает отделить юнит-тесты от интеграционных.
Сокращение количества слоев. Многие разработчики стремятся к абстрагированию и обобщению кода путем введения дополнительных уровней абстракции.
Рис. 3 – Типичное корпоративное приложение с несколькими слоями
В некоторых приложениях находится столько уровней абстракции, что разработчик уже не может разобраться в коде и понять логику даже простейших операций.
«Все проблемы в программировании можно решить путем добавления нового уровня абстракции (кроме проблемы наличия слишком большого количества уровней абстракции)».
Дэвид Дж. Уилер
Лишние абстракции также затрудняют юнит- и интеграционное тестирование. Кодовые базы со слишком большим количеством слоев обычно не имеют четкой границы между контроллерами и моделью предметной области. Старайтесь ограничиться минимально возможным количеством уровней абстракции. В большинстве серверных систем можно обойтись всего тремя: слоем доменной модели, слоем сервисов приложения (контроллеров) и слоем инфраструктуры.
Исключение циклических зависимостей. Циклическая зависимость возникает в том случае, если два или более класса прямо или косвенно зависят друг от друга. Типичный пример циклической зависимости — обратный вызов:
Как и в случае с избыточными уровнями абстракции, циклические ссылки создают дополнительную когнитивную нагрузку при попытке прочитать и понять код. Циклические зависимости также усложняют тестирование. Вам часто приходится использовать интерфейсы и моки, для того чтобы разбить граф классов и изолировать одну единицу поведения.
Что же делать с циклическими зависимостями? Лучше всего совсем избавиться от них. Отрефакторить класс ReportGenerationService, чтобы он не зависел от CheckOutService и сделать так, чтобы ReportGenerationService возвращал результат работы в виде простого значения вместо вызова CheckOutService:
Использование нескольких секций действий в тестах
Как вы, возможно, помните из предыдущего конспекта «Анатомия юнит-тестов», наличие более одной секции подготовки, действий или проверки в тесте — плохой признак. Он указывает на то, что тест проверяет несколько единиц поведения, что, в свою очередь, ухудшает сопровождаемость теста. Например, если имеются два связанных сценария использования (допустим, регистрация и удаление пользователя).
подготовка — подготовка данных для регистрации пользователя;
действие — вызов UserController.RegisterUser();
проверка — запрос к базе данных для проверки успешного завершения регистрации;
действие — вызов UserController.DeleteUser();
проверка — запрос к базе данных для проверки успешного удаления.
Лучше всего разбить тест, выделив каждое действие в отдельный тест. На первый взгляд это может показаться лишней работой, но эта работа окупается в долгосрочной перспективе. Фокусировка каждого теста на одной единице поведения упрощает понимание и изменение этих тестов при необходимости.
Исключение из этой рекомендации составляют тесты, работающие с внепроцессными зависимостями, трудно приводимыми в нужное состояние. Например, регистрация пользователя приводит к созданию банковского счета во внешней банковской системе. Банк предоставил вашей организации тестовую среду, которую вы хотите использовать для сквозных тестов. К сожалению, тестовая среда работает слишком медленно; также возможно, что банк ограничивает количество обращений к этой тестовой среде. В таком сценарии удобнее объединить несколько действий в один тест, чтобы сократить количество взаимодействий с проблемной внепроцессной зависимостью.
Выводы
Интеграционные тесты проверяют, как ваша система работает в интеграции с внепроцессными зависимостями.
Интеграционные тесты покрывают контроллеры; юнит-тесты покрывают алгоритмы и доменную модель.
Интеграционные тесты обеспечивают лучшую защиту от багов и устойчивость к рефакторингу; юнит-тесты более просты в поддержке и дают более быструю обратную связь.
«Порог» для написания интеграционных тестов выше, чем для юнит-тестов: их эффективность по метрике защиты от багов и устойчивости к рефакторингу должна быть выше, чем у юнит-тестов, для того чтобы скомпенсировать дополнительную сложность в поддержке и медленную обратную связь. Пирамида тестирования отражает этот компромисс: большинство тестов должны составлять быстрые и простые в поддержке юнит-тесты при меньшем количестве медленных и более сложных в поддержке интеграционных тестов, проверяющих правильность системы в целом.
Управляемые зависимости представляют собой внепроцессные зависимости, доступ к которым осуществляется только через ваше приложение. Взаимодействия с управляемыми зависимостями не видимы извне. Типичный пример — база данных приложения.
Неуправляемые зависимости — внепроцессные зависимости, доступные для других приложений. Взаимодействия с неуправляемыми зависимостями видны снаружи. Типичные примеры — сервер SMTP и шина сообщений.
Взаимодействия с управляемыми зависимостями являются деталями имплементации; взаимодействия с неуправляемыми зависимостями являются частью наблюдаемого поведения вашей системы.
Иногда внепроцессная зависимость обладает свойствами как управляемых, так и неуправляемых зависимостей. Типичный пример — база данных, доступная для других приложений. Наблюдаемую часть такой базы следует интерпретировать как неуправляемую зависимость; заменяйте ее моками в тестах. Рассматривайте остальную часть зависимости как управляемую — проверяйте ее итоговое состояние, а не взаимодействия с ней.
Выделите явное место для модели предметной области в коде. Четкая граница между классами предметной области и контроллерами помогает отличать юниттесты от интеграционных.
Лишние уровни абстракции отрицательно влияют на вашу способность понимать код. Постарайтесь свести количество этих уровней к минимуму. В большинстве бэкенд-систем достаточно всего трех слоев: предметной области, сервисов приложения и инфраструктуры.
Циклические зависимости увеличивают когнитивную нагрузку при попытках разобраться в коде. Типичный пример — обратный вызов (когда вызываемая сторона уведомляет вызывающую о результате своей работы).
Множественные секции действий в тестах оправданны только в том случае, если тест работает с внепроцессными зависимостями, которые трудно привести в нужное состояние. Никогда не включайте несколько действий в юнит-тест, потому что юнит-тесты не работают с внепроцессными зависимостями. Многофазные тесты почти всегда принадлежат к категории сквозных.