Подходы в тестировании во многом устоялись. И все же остались вопросы, на которые комьюнити продолжает искать правильные ответы. Где проходит граница ответственности между тестировщиком и разработчиком? Нужно ли QA проверять автотесты, которые пишет разработчик? И на чью сторону перелетает мяч на разных этапах тестирования?
Меня зовут Кирилл Поляков, я ведущий инженер по тестированию Lamoda Tech. В этом тексте я поделюсь своими ответами, основанными на 12-летней практике в тестировании. Разберем, как связаны чистая архитектура и пирамида тестирования, расскажу нюансы выстраивания стратегии на разных уровнях тестирования, дам рекомендации для QA-инженеров, которые помогут улучшить процесс проверки кода.
Чистая архитектура и пирамида тестирования — объединяй и властвуй
Начнем с разбора двух абстрактных моделей, которые были сформулированы отдельно и для разных целей, но при внимательном рассмотрении могут пересекаться друг с другом и помогать в распределении задач тестирования.
Первая модель — «чистая архитектура». Это концепция, предложенная Робертом Мартином, настоящим гуру разработки и автором книги «Чистый код». Концепция фокусируется на разделении приложения на слои с четкими зависимостями. В основе лежит идея, что бизнес-логика и правила приложения должны быть изолированы от внешних изменений, таких как базы данных, пользовательский интерфейс или сторонние сервисы. В такой архитектуре зависимости направлены вовнутрь, и ядро приложения (бизнес-логика) не зависит от внешних слоев (интерфейсов, баз данных, фреймворков).
Вот из чего складывается чистая архитектура:
-
В основе приложения находится ядро, в котором сосредоточены ключевые бизнес-процессы и сущности, отражающие суть доменной области.
-
Над ядром располагается слой бизнес-логики — этот уровень преобразовывает бизнес-требования в конкретные действия системы.
-
Верхний слой — интерфейсы, через которые приложение взаимодействует с внешним миром: пользовательскими интерфейсами, API или внешними сервисами.
Современные разработчики также часто выделяют конфигурацию в качестве отдельного внешнего слоя. Это позволяет гибко управлять параметрами приложения, задавать настройки окружения и обеспечивать интеграцию с внешними системами, не внося изменения в основной код.
Четкое разграничение приложения на слои помогает изолированно вносить изменения, что упрощает поддержку и развитие системы. Такой подход способствует устойчивости приложения к изменениям и улучшает гибкость разработки.
Однако когда речь заходит о тестировании, используется другая популярная концепция — пирамида тестирования.
Этот подход описал Майк Кон в книге «Scrum: гибкая разработка ПО». Согласно нему, пирамида тестирования ориентирована на то, чтобы эффективно распределить тесты по уровням системы и тем самым обеспечить максимальное покрытие при минимальных затратах времени и ресурсов.
Одна из основных задача QA-инженера — найти баланс между глубиной проверки и затратами на ее выполнение. Разделение тестов по уровням позволяет оптимизировать процесс:
-
На нижних уровнях пирамиды (слои белого цвета) сосредоточены более детальные проверки, где тесты выполняются быстрее и оказываются проще в поддержке.
-
На верхних уровнях (слои зеленого цвета) находятся сценарии, охватывающие ключевые взаимодействия.
Такой подход помогает эффективно использовать ресурсы, обеспечивая достаточное покрытие и при этом сохраняя разумные затраты времени и усилий на тестирование.
В чем сходство?
Кажется, что эти концепты решают разные задачи и фокусируются на разных аспектах. Но если присмотреться, то становится очевидно, что они прекрасно дополняют друг друга.
Тесты как будто «привязаны» к слоям системы через интерфейсы:
-
Ядро приложения (Entities, Use Cases) тесно связано с unit-тестами. Эти тесты проверяют базовую бизнес-логику и сущности, взаимодействуя с ними напрямую через методы и интерфейсы изолированно.
-
Интерфейсные адаптеры (Controllers, Gateways, Presenters) являются связующим звеном между слоем бизнес-логики и внешним миром (интерфейсами) и проверяются интеграционными тестами.
-
Внешние интерфейсы (UI, Web, DB и т.д.) являются фокусом end-to-end тестов, которые проверяют систему как единое целое, имитируя реальные сценарии использования. Эти тесты не обращаются напрямую к внутренним методам или объектам системы, а воспринимают ее как черный ящик.
Теперь обратимся к каждому уровню пирамиды тестирования и разберем на примерах, как тестируются слои системы, какие подходы и рекомендации стоит применять, и кто эффективнее спроектирует и реализует тесты (не всегда это только QA).
Unit-тесты
Если запереть в одной комнате трех разработчиков и попросить их дать определение unit-тестам, мы наверняка услышим три разных ответа. Я как QA предпочитаю опираться на классическое определение: unit-тесты — это тесты, которые проверяют работу отдельной функции, класса или небольшой группы классов, тесно связанных друг с другом.
Такое определение подчеркивает, что объект тестирования должен содержать всю необходимую функциональность. Если функции тесно связаны, логичнее проверять весь класс или даже группу классов. Главное не забывать, что сильная связанность указывает на проблемы в реализации и сигнализирует о необходимости рефакторинга.
Чтобы лучше понять, как это работает на практике, рассмотрим ситуацию, в которой класс отвечает за определенную бизнес-логику и одновременно взаимодействует с внешними системами. Что именно мы будем тестировать в таком случае?
Представим, что в приложении есть класс UserService, который обрабатывает логику, связанную с пользователями. Этот класс взаимодействует с другими компонентами системы: с UserRepository — для работы с базой данных; и с BillingService — для расчетов и управления платежами. В рамках unit-тестирования мы изолируем UserService, заменяя взаимодействия с другими компонентами на моки.
Важно отметить: для тестирования UserService мы обращаемся к его методам напрямую. Мы не выполняем HTTP-запросы и не проходим через все слои приложения, так как это выходит за рамки unit-тестов. Нас интересует исключительно логика, реализованная внутри UserService.
Такой подход делает тестирование быстрым, удобным и сосредоточенным. Он помогает точно выявлять проблемы, не затрагивая другие слои системы.
Кто должен писать unit-тесты?
Исходя из вышесказанного, можно ответить однозначно: unit-тесты должен писать разработчик. В идеальном мире он сначала пишет тесты, а потом код, следуя подходу test-driven development (TDD).
Однако на практике все происходит иначе.
Сначала пишется код, а затем к нему добавляются тесты. Это приводит к риску когнитивного искажения, известного как проклятье знания. Разработчик знает, как должна работать функциональность, ведь он ее реализовал. Он автоматически предполагает, что все работает верно, а потому ему сложно представить негативные сценарии, в которых что-то идет не так.
Еще одна распространенная проблема — стремление сделать процесс написания тестов проще. Разработчик может игнорировать вызовы в моках, использовать нереалистичные данные или даже добавлять логику прямо в тесты, чтобы покрыть несколько сценариев сразу. Такие подходы кажутся удобными, но в итоге превращают тесты в самостоятельный код, который перестает проверять основную функциональность и начинает «работать сам на себя».
Это приводит к эффекту рекурсии: чтобы удостовериться в корректности тестов, приходится писать дополнительные тесты. Чтобы избежать такой ловушки, важно заранее определять границы тестирования. Тесты должны быть максимально простыми, изолированными и понятными, чтобы фокусироваться только на ключевой логике и минимизировать сложность.
Кроме того, если сценарии тестирования остаются без валидации со стороны QA-инженеров, тесты могут лишиться четкой структуры и перестать соответствовать общепринятым стандартам. Это делает их менее понятными для других членов команды, увеличивает вероятность ошибок и усложняет поддержку.
Что может пойти не так? Поделюсь реальной историей. В одном из проектов команда разработчиков с нуля создавала сервис без участия QA-инженеров. Один из разработчиков решил взять задачу тестирования и самостоятельно разработал фреймворк. Инициатива выглядела многообещающей. Но когда он стал определять, какие тесты и где писать, начались сложности.
Без четкого понимания подходов к тестированию герой нашей истории начал писать то, что можно назвать интеграционными unit-тестами — они одновременно проверяли функциональность ядра приложения и его взаимодействие с внешними зависимостями. Вместо использования проверенных инструментов он наполнил фреймворк собственными решениями: сложно настраиваемыми моками, фикстурами и дата-провайдерами, основанными на модифицированных билдерах приложения.
Эти инструменты действительно упростили написание тестов для него самого, но оказались мало понятны остальным членам команды. Возникли трудности с тем, как поддерживать такие тесты, и что именно они должны покрывать.
Зафиксируем, к чему может привести разное видение подхода к тестированию:
Ситуация |
Последствия |
Разработчик самостоятельно пишет все уровни тестов |
Смешение уровней тестирования, плохая читаемость |
Разработчик самостоятельно реализовал фреймворк тестирования |
Обилие «велосипедов», сложность поддержки |
QA не знает о тестах разработчика или не валидирует их |
Ложноположительные результаты и дублирующие тесты |
Unit-тесты: рекомендации для QA
-
Определите общие термины и границы
Начните с того, чтобы договориться с командой о том, что именно считается unit-тестами, где проходят границы интеграционных тестов и с какого уровня начинается end-to-end тестирование. Это устранит путаницу в терминах и обеспечит единое понимание подходов к тестированию. -
Установите единые стандарты тестов
Договоритесь о код-стайле для тестов. Единообразие в написании тестов делает их более читаемыми и понятными. Это особенно важно для QA-инженеров, которые должны быстро разбираться в тестах разработчиков, чтобы вносить свои предложения и улучшения. -
Анализируйте покрытие тестов
Оценивайте сценарии, которые разрабатываются разработчиками. QA может выступить в роли аудитора, проверяя, что тесты охватывают как основные, так и дополнительные сценарии, включая негативные кейсы и редкие ситуации. -
Организуйте процесс ревью тестов
QA-инженеры также могут участвовать в ревью unit-тестов, чтобы вовремя выявлять недостающие кейсы, улучшать структуру тестов и находить ошибки в логике тестирования. Это способствует повышению общего качества тестового покрытия. -
Оценивайте читаемость и поддерживаемость тестов
Помимо корректности выполнения тестов, важно обращать внимание на их читаемость. Понятные и хорошо структурированные тесты легче поддерживать, адаптировать и масштабировать в будущем. -
Фокусируйтесь на целостности тестирования
Убедитесь, что unit-тесты выполняют свою основную задачу — изолированно проверять функциональность отдельных компонентов. Тесты не должны дублировать работу интеграционных или end-to-end тестов, но обязаны вносить свой вклад в общую надёжность системы.
Интеграционные тесты
Это тесты, которые работают с реальными зависимостями. Задача интеграционного теста — проверить не функциональность сервиса, а точку интеграции (контракт).
Возможно, вы, как и я когда-то, сталкивались с аргументом: «У нас и так покрытие unit-тестами 96%, зачем нам какие-то другие тесты»?. На первый взгляд кажется, что большое покрытие тестов гарантирует стабильность системы. Но это заблуждение.
Здесь стоит вспомнить принцип, сформулированный еще Аристотелем, — «Целое больше суммы частей». Он лежит в основе теории систем и напоминает нам, что даже идеально работающие в изоляции модули могут вести себя иначе при взаимодействии друг с другом. Именно для проверки этих взаимодействий и существуют интеграционные тесты. Их цель — убедиться, что компоненты корректно передают данные друг другу и работают как единое целое.
Следовательно, интеграционные тесты должны фокусироваться на критических точках взаимодействия между слоями приложения. И, как я уже говорил ранее, особое внимание стоит уделять интеграции между бизнес-логикой и внешними интерфейсами. Именно здесь происходит преобразование внутренних структур данных во внешние представления — и наоборот.
Вернемся к нашему примеру с UserService. На уровне интеграционных тестов возникает вопрос: как поступить с такими зависимостями, как база данных и внешние сервисы? Должны ли мы использовать реальный Billing Service? А нужно ли мокать соединение с базой данных?
Подходы к написанию интеграционных тестов
Разногласия в таких вопросах привели к появлению двух подходов к написанию тестов:
1. Общительные тесты охватывают более широкие аспекты поведения системы. Использование двойников (mock/stub) в таких тестах не поощряется. Они применяются только на границах вашей доменной области/системы или для устранения проблем тестов: таких, как дублирование кода или сложность восприятия.
2. Тесты-одиночки проверяют небольшие блоки системы и фокусируются на специфике вычислений или на взаимодействии между зависимостями. Такие тесты используют моки для изоляции зависимостей и направлены на проверку конкретного поведения системы. Они основываются на принципе цепочки связанных контрактов — моки определяют контракты, которые выполняются и проверяются через тесты каждого участника.
Я сторонник второго подхода. Для взаимодействия с таким внешним сервисом, как Billing Service, мы используем настраиваемый мок-сервер. Это не только упрощает настройку, но и позволяет гибко воспроизводить различные сценарии поведения зависимости. Если же это сторонний сервис (third-party service), такой подход переходит в разряд необходимости.
Что касается базы данных, здесь также возможно два подхода:
-
Мокать само соединение. Но это имеет смысл только для проверки сценариев, связанных с самим процессом подключения: например, таймауты, недоступность сервера или ошибки аутентификации. Для остальных случаев лучше использовать более реалистичный подход.
-
Для ускорения тестов можно воспользоваться in-memory базой или другим легковесным аналогом. Однако такой подход не всегда учитывает особенности конкретной базы данных и ее версии.
Именно поэтому использование реальной базы данных остается лучшим вариантом. Да, это замедляет тестирование, но взамен вы получаете уверенность в корректности обработки запросов. А чтобы компенсировать увеличение времени выполнения тестов, можно применить параллелизацию в комбинации с шаблонами.
Кто должен писать интеграционные тесты?
Лично я считаю, что участие и разработчика, и QA одинаково приемлемо, но у каждого есть свои нюансы.
-
Разработчик не владеет необходимым бизнес-контекстом. Он знает все про свою систему и техническую составляющую инфраструктуры, но мало погружен в контекст внешних бизнесовых зависимостей. Так получается из-за того, что этими зависимостями обычно занимаются либо другие разработчики, либо внешние подрядчики. Соответственно, разработчик может реализовать тест, но в какой-то момент столкнется с когнитивными искажениями, вызванными не до конца понятным бизнес-контекстом.
-
QA-инженеры обычно лучше ориентируются в бизнес-контексте, но у них может не хватать технической экспертизы для реализации тестов. Отсутствие опыта работы с кодом или фреймворками тестирования может стать серьезным барьером.
Интеграционные тесты: рекомендации для QA
-
Фокусируйтесь на точках интеграции. При проектировании тестов уделяйте особое внимание точкам, где данные преобразуются между слоями системы.
-
Обратитесь к разработчикам за поддержкой. Если у QA недостаточно технической экспертизы для написания тестов, важно наладить сотрудничество с разработчиками.
-
Проводите ревью тестов. Даже если интеграционные тесты пишет разработчик, QA должен участвовать в их ревью. Это поможет убедиться, что тесты не только правильно реализованы, но и проверяют нужные бизнес-сценарии.
-
Поддерживайте предсказуемость окружения. Важно заранее определить, какие зависимости должны быть замоканы, а какие будут использоваться в реальном виде.
Бонус
Мы в Lamoda Tech пришли к тому, что создали свой low-code framework на Go, в котором все интеграционные тесты выражены в YAML-документах — Gonkey. Эти документы описывают контракт взаимодействия, включая моки, фикстуры и сам сценарий взаимодействия. Так мы избавили QA от необходимости реализовывать все это самостоятельно. Чтобы вы тоже могли упростить свою работу, делюсь фреймворком.
End-to-end тесты
End-to-end (E2E) тестирование проверяет работу всей системы целиком: от начала и до конца. Это наиболее сложный уровень тестирования, так как требует вовлечения всех участвующих сервисов в окружении, максимально близком к реальному. Здесь проверяются не только взаимодействия между этими сервисами, но и полное соответствие системы пользовательским сценариям.
Снова рассмотрим наш пример с UserService. На уровне E2E тестирования мы не ограничиваемся проверкой отдельных компонентов, таких как HTTP Handler или User Service. Здесь нас интересует, как вся система и её зависимости (здесь это Billing Service и база данных) работают вместе, обеспечивая целостный бизнес-процесс.
На этом уровне у нас уже другие требования к среде запуска тестов: мы не можем заменить Billing Service моками, как это делается в интеграционных тестах. Использование реальных зависимостей в окружении, максимально приближенном к реальному, необходимо, чтобы тесты отражали поведение всей системы.
Таким образом, E2E тесты позволяют обнаружить проблемы, которые могут быть упущены на уровнях unit- или интеграционного тестирования:
-
Ошибки конфигурации сервисов. К ним относятся некорректные параметры подключения, неправильные настройки окружения или несовместимость версий.
-
Нарушение контрактов между сервисами. На уровне интеграционных тестов контракты обычно проверяются точечно, но их соблюдение в сложных цепочках взаимодействия может быть нарушено.
-
Нарушение бизнес-логики на стыках компонентов или систем. Это могут быть ошибки, связанные с последовательностью операций, некорректной обработкой исключений или несогласованностью данных между сервисами. Такие проблемы особенно критичны, так как влияют на целостность пользовательского опыта.
Кто должен писать end-to-end тесты?
End-to-end тестирование, на мой взгляд, должно быть зоной ответственности QA, а не разработчиков. Поручать эту задачу разработчикам нецелесообразно, так как их ответственность чаще всего ограничивается работой над одним сервисом. Разработчики прекрасно понимают детали реализации бизнес-логики и контрактные обязательства своего сервиса, но зачастую не имеют полного представления о работе других систем, с которыми взаимодействует их приложение. QA, напротив, фокусируются на конечном результате и пользовательских сценариях, что позволяет им объективно оценивать функционирование всей системы в целом.
На этом уровне тестирования особенно важен качественный тест-дизайн. В отличие от модульного или интеграционного тестирования, где акцент делается на технических аспектах, end-to-end тестирование требует работы с полноценными пользовательскими сценариями, охватывающими весь бизнес-процесс. Именно QA, обладая экспертизой в тест-дизайне и пониманием бизнес-контекста, могут эффективно проектировать и реализовывать такие проверки, выявляя ошибки, которые иначе могли бы остаться незамеченными.
Но возложить всю ответственность за end-to-end тестирование на одного QA было бы ошибкой, особенно в случае сложных и распределенных систем. Ни один специалист не может обладать абсолютным знанием всех аспектов проекта: бизнес-контекста, архитектуры системы, конфигурации сервисов, деталей инфраструктуры — все это выходит за рамки базовой экспертизы QA, но критически важно для успешного проведения сквозных тестов.
Здесь необходима командная работа:
-
архитектор поможет определить границы реализации конкретного бизнес-процесса;
-
системный аналитик обеспечит понимание того, как пользовательские сценарии трансформировались в системные и функциональные требования;
-
девопс настроит пайплайны сборки, деплоя и запуска тестов, а также предложит оптимальные решения для тестовой инфраструктуры;
-
разработчик сможет внести вклад в разработку фреймворка автоматизации тестирования, предоставляя инструменты и поддержку для реализации E2E-тестов.
Только совместными усилиями можно выстроить процесс, который обеспечит надежное и полное тестирование системы.
End-to-end тесты: рекомендации для QA
-
Фокусируйтесь на пользовательских сценариях. Начните с определения ключевых сценариев, которые необходимо проверить. Убедитесь, что каждый тест покрывает реальный бизнес-кейс, а не исключительно техническую реализацию.
-
Делайте упор на качество тест-дизайна. Всегда думайте, что тестировать, а не только как. Это поможет сосредоточиться на критически важных аспектах системы и избежать избыточного количества тестов.
-
Управляйте сложностью системы. Разбивайте сложные сценарии на этапы, проверяя отдельные части процесса последовательно. Это поможет быстрее находить ошибки и упрощать анализ результатов.
-
Не пытайтесь делать всё самостоятельно. Привлекайте экспертов: архитектора, аналитика, девопсов, разработчиков.
-
Используйте реальные зависимости. В отличие от интеграционных тестов, в E2E тестировании не следует использовать моки. Все компоненты и сервисы по возможности должны быть настоящими, чтобы тесты отражали реальное поведение системы.
Выводы
Подводя итог, можно увидеть тесную связь между слоями приложения в рамках чистой архитектуры и уровнями пирамиды тестирования. Ядро системы соответствует основанию пирамиды, где сосредоточены стабильные абстракции и правила. Эти уровни редко меняются и служат основой всей системы. Здесь ведущую роль играют разработчики, обладающие глубокой экспертизой в построении логики приложения, а задача QA заключается в консультировании по вопросам тест-дизайна.
На верхних уровнях пирамиды тестирования, ближе к интерфейсам и пользовательским сценариям, ситуация иная. Эти элементы изменчивы и могут адаптироваться под новые бизнес-требования. Разработчики, чья зона ответственности ограничивается внутренней логикой системы, не обладают полной картиной изменений на этом уровне. Зато QA, глубже погружённые в пользовательские сценарии и бизнес-контекст, способны максимально эффективно тестировать эти слои.
Таким образом, ответственность за тестирование распределяется по пирамиде: от доминирующей роли разработчиков на нижних уровнях к ведущей роли QA на верхних. Такое разделение позволяет эффективно сочетать техническую экспертизу разработчиков и бизнес-ориентированное видение QA, обеспечивая качество системы на всех уровнях.
Знаю, что тема дискуссионная, поэтому в комментариях буду рад обсудить ваш опыт и обменяться мнениями.
ссылка на оригинал статьи https://habr.com/ru/articles/866102/
Добавить комментарий