Что пишут в блогах

Подписаться

Что пишут в блогах (EN)

Разделы портала

Онлайн-тренинги

.
Руководство по имитаторам для тестировщиков
10.06.2026 00:00

Автор: Штефан Дирнштофер (Stefan Dirnstorfer)
Оригинал статьи
Перевод: Ольга Алифанова

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

В этой статье описывается создание тестовых сценариев, которые могут поставить систему под угрозу в продакшене. Приходилось ли вам сталкиваться с дефектом, который сложно воспроизвести в тестовой среде? Если да, то, возможно, в этой статье вы найдёте идеи, которые помогут вам в этом.

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

Когда тестирование сценария становится сложным

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

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

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

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

def testme():

tmp = my_random_uniform(1,2)                # Нечистый компонент

return "rare" if tmp > 1.99 else "frequent" # Чистый компонент

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

  • Тесты не проверяют точные результаты, а принимают широкие диапазоны значений или любое значение определённого типа.
  • Тесты не покрывают ошибочные случаи и стресс-сценарии.
  • Покрытие тестами низкое. Взаимодействия с внешними модулями не тестируются.
  • Тесты выполняются слишком долго и не могут запускаться параллельно.

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

Новый старт: как зафиксировать и сбросить всю память

Как только тестируемая система начинает иметь собственную память, ваши тесты перестают быть легко воспроизводимыми. Система становится нечистой. Каждый запрос обрабатывается с учётом состояния и возможных знаний о предыдущих взаимодействиях. Если вы хотите протестировать настоящий «первый запуск», необходимо откатить все компоненты, с которыми система взаимодействовала. Это может быть сколь угодно сложно.

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

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

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

  • Развёртывание: поскольку снапшоты затрагивают всю систему, убедитесь, что вы можете быстро поднимать новые экземпляры.
  • Чувствительные данные: снапшоты могут содержать персональную или связанную с безопасностью информацию. Обфускация позволяет скрыть чувствительные данные, сохранив при этом важные для тестов аспекты.
  • Временные метки: объект мог быть «новым» на момент создания снапшота, но при восстановлении «состарившиеся» данные могут существенно повлиять на тестируемую функциональность.
  • Размер: крупные объекты, такие как изображения или видео, могут замедлять восстановление. Рассмотрите возможность их уменьшения или замены.
  • Поддержка: кто обновляет или пересоздаёт снапшоты?

Альтернатива снапшотам – это пересоздание данных. Если данные хранятся в оперативной памяти, это единственный возможный вариант. В этом случае вы описываете — в идеале в коде автоматизации — как начальное состояние системы создаётся из пустого. При проектировании ПО стоит учитывать следующие моменты:

  • Мультитенантность: идеальный дизайн предусматривает отдельные пространства, позволяющие одновременно создавать несколько сценариев данных.
  • Автоматизация: для обеспечения консистентности и скорости создания данных используйте автоматизированные скрипты, которые при необходимости могут также тестировать пользовательские механизмы ввода данных.
  • Массовая загрузка: предусмотрите простые способы создания множества сущностей одновременно — через UI, API или отдельные инструменты для тестировщиков.
  • Object Mother: это техника, скрывающая сложность создания данных за объектом-генератором с простым и читаемым API.

Внешний мир: как сбросить несбрасываемое

Как только тестируемая система начинает взаимодействовать с внешними компонентами, сохранение чистоты становится серьёзной проблемой. У вас есть два варианта: взять все внешние системы под свой контроль или заменить их двойниками — имитаторами, тест-двойниками или «самозванцами».

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

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

Команда может откладывать внедрение имитаторов. Начальные затраты высоки, а крупные рефакторинги почти неизбежны. Тем не менее, есть веские причины использовать mock-объекты. Если к компоненту вашего приложения применим хотя бы один из следующих критериев, стоит задуматься о создании имитатора:

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

Тестовые окружения с использованием mock’ов можно различать по количеству замоканных компонентов.

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

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

  • Если компоненты вашего приложения взаимодействуют по стандартным веб-протоколам (например, в микросервисной архитектуре), у вас есть доступ к множеству инструментов — MockServer, Mockoon, Mocki и так далее. Всё, что общается по стандартным протоколам, легко замокать. Запросы можно записывать в живых окружениях, анализировать, адаптировать и воспроизводить в тестах.
  • Структурированные языки программирования позволяют использовать технику внедрения зависимостей (Dependency Injection) для подмены компонентов с минимальными усилиями. В Java для этого можно использовать JMock и Mockito, в других языках есть аналогичные фреймворки.
  • При достаточной поддержке со стороны кода любой компонент можно модифицировать так, чтобы он сам выступал в роли имитатора в тестах, вызывая поведение, которое необходимо проверить. Тестировщикам нужен чёткий интерфейс для управления таким поведением. Это может быть реализовано через специальные управляющие интерфейсы или даже через «злоупотребление» пользовательскими данными в качестве триггеров. Например, пользователь с фамилией «CrashAtStepX» может незаметно вызывать особое поведение в тестах (разумеется, такая функциональность должна быть отключена в продакшене).

Во всех случаях имитатор маскирует небольшую часть продакшен-кода, которая в этом окружении остаётся непротестированной.

Медленно и быстро: как запускать тесты с подменой времени

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

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

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

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

Запуск кода в замедленном режиме может вызывать зависания системы в современных виртуализированных и сетевых архитектурах. Хотя такие сбои редки и кратковременны, они часто приводят к серьёзным отказам, которые сложно воспроизвести. Вот несколько способов их спровоцировать:

  • Симулировать медленный интернет или 3G: для веб-приложений это можно сделать через инструменты разработчика браузера, но замедления также можно моделировать на уровне сети.
  • Рандомизировать тайминги всех компонентов, способных вызывать условия гонки. Это замедляет приложение, но выявляет один из самых неприятных типов ошибок. Такая рандомизация может быть реализована на уровне операционной системы, например с помощью кастомных планировщиков Linux.

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

Заключение

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

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

  • Обсуждайте требования к тестированию как можно раньше. Стратегии тестирования могут существенно повлиять на архитектуру ПО.
  • Убедитесь, что у компонентов есть чёткие зоны ответственности и что их упрощённое поведение в замоканных сценариях явно определено.
  • Оценивайте усилия. Одни стратегии реализуются проще, другие сложнее — выбирайте осознанно.
  • Следите за актуальностью mock-объектов и корректностью исходных допущений.

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

Дополнительная информация