Привет, Хабр!

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

Что за зверь?

Композиционное тестирование —  вид тестирования, направленный на проверку соответствия системы установленным правилам компоновки и взаимодействия её компонентов. 

Теперь нагляднее:

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

Запуская систему, спроектированную с академическим подходом, мы получаем результаты тестов A, B, C, D, которые, скорее всего, были выполнены в изолированной среде и не влияли друг на друга. В то время как при запуске композиционного тестирования мы получаем результаты тестов A и B, причём тест B выполняется на основе данных из теста A. Тестовые случаи для такого подхода генерируются, исходя из разнообразных комбинаций данных, параметров системы и их возможных вариантов.

Почему был выбран этот подход?

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

Пример: В рамках многих проектов нам пришлось отказаться от проведения комплексных E2E (End-to-End) тестов. В некоторых случаях мы также прекратили писать тесты, которые проверяли нерелевантные аспекты, такие как информеры ошибок, и отказались от проверок валидации полей. Эти изменения позволили сделать процесс разработки автотестов более реалистичным и управляемым, избегая необходимости нанимать большое количество тестировщиков. В результате время, необходимое для тестовых запусков, значительно сократилось. Однако следует признать, что это повлияло на общее тестовое покрытие.

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

Сравнение

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

Пример: Требуется протестировать функциональность генерации трек-номера для посылки. Данные о посылке (её регистрация в системе) поступают в виде POST запроса с телом вида:

{
    "name": "Иван Иванов",
    "weight": 8508,
    "rate": "light",
    "dimentions": "2x51x9",
    "address": {
        "street": "ул. Центральная",
        "city": "Москва",
        "zipcode": "101100"
    },
    "phoneNumbers": [
        "+71234590099"
    ]
} 

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

def test_international_delivery():
   delivery_data = DeliveryData('international')
   response = requests.post(BASE_URL, json=delivery_data.to_json())
   assert response.status_code == 200
   assert response.json()["status"] == "success"
   assert check_delivery_code(response.json()["deliveryCode"])

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

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

Например, в контексте тестирования системы доставки, значение "Москва" в поле города активирует тесты, связанные с доставкой внутри одного города. В то же время другое значение, например "Казань", инициирует тесты междугородней доставки. Аналогично, международные адреса запускают тесты, связанные с международной доставкой.

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

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

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

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

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

Как перейти на композиционное тестирование?

Каждый проект уникален и не факт, что композиционное тестирование подойдет именно вам. 

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

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

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

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

Пример: Пользователь заполняет паспорт, и нам нужно проверить валидность этих данных, например, по дате выдачи. Мы подгружаем дату рождения из БД и проверяем (выдан не ранее 14 лет, не более 20 лет в пользовании, не старше 45 и т.д.). Для такой даты нам потребуется отдельный генератор.

Чтобы упростить работу с шаблонами, рекомендуем представлять их в сериализуемом виде. Это позволяет хранить шаблоны в централизованном хранилище и легко импортировать их в различные части тестового фреймворка или системы. Такой подход обеспечивает гибкость и масштабируемость процесса генерации тестовых данных.

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

Основные принципы трансформации фабрик:

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

  • Шаблоны генерации значений. Фабрики должны использовать шаблоны для генерации значений, которые могут быть настроены для создания разнообразных и реалистичных данных. Эти шаблоны должны быть легко настраиваемыми и расширяемыми.

  • Комбинирование данных. Одной из ключевых задач фабрик является способность комбинировать различные наборы данных для создания уникальных тестовых сценариев. Это позволяет тестировать систему в различных условиях и с разными комбинациями входных данных.

  • Интеграция с тестовыми фреймворками. Фабрики должны быть легко интегрируемы с популярными тестовыми фреймворками, чтобы обеспечить эффективность и удобство в использовании.

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

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

Объединение тестовых сценариев (параметризованные тесты). Основные принципы:

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

  • Параметризация тестов. Используя библиотеки, такие как pytest в Python, тесты параметризуются таким образом, чтобы они могли принимать различные входные данные или параметры. Это позволяет одному и тому же тестовому сценарию работать с разными наборами данных.

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

  • Автоматизированный запуск тестов. После подготовки данных тесты запускаются автоматически с различными параметрами. Это позволяет быстро и эффективно проверить систему в различных условиях.

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

Формирование сети Петри или древовидной структуры проекта:

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

  • Построение модели. На основе этого анализа строится модель в виде сети Петри или древовидной структуры. Эта модель представляет собой визуальное представление всех компонентов системы и их взаимосвязей.

  • Механизмы обхода. Разрабатываются механизмы обхода этой структуры, которые позволяют автоматизировано тестировать различные пути и взаимодействия в системе.

Переход на E2E Тесты

  • Интеграция системных тестов. В каждый E2E тест включаются соответствующие системные тесты, что позволяет проверять не только отдельные компоненты, но и их взаимодействие в рамках всей системы.

  • Поведенческое тестирование. Этот подход позволяет проводить поведенческое тестирование, оценивая систему в целом, а не только отдельные ее части.

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

А подходит ли нам?

Точно подходит, если речь идет о тестировании API или о продуктах с обширной комбинаторикой. Возможно, это также подойдет для систем с множеством форм или разветвленной бизнес-логикой. Однако, в других случаях, это может быть не лучшим выбором. Для архитектур продуктов, состоящих из множества атомарных сервисов и с минимальными интеграциями, более подходящим может оказаться академический подход или даже ограничение unit-тестами.

Спасибо за внимание! Мы будем очень благодарны, если вы поделитесь опытом о том, как устроена архитектура тестирования в вашем проекте. Какие подходы вы используете и как они работают? Довольны ли вы уровнем тестового покрытия вашего проекта?

Комментарии (0)