Современная разработка строится вокруг скорости и гибкости, и поэтому эффективное тестирование становится критически важным. Исследования DORA показывают: лучшие команды добиваются высокой производительности и надежности одновременно. Их показатели впечатляют: цикл поставки изменений в 127 раз быстрее, релизов в год — в 182 раза больше, отказов при изменениях — в 8 раз меньше, а восстановление после инцидентов — в 2 293 раза быстрее. Секрет в том, что они используют подхода Shift-Left (дословно «сдвиг влево»).

Shift-Left — это практика, при которой задачи вроде тестирования и проверки безопасности переносятся на более ранние этапы разработки. Такой подход позволяет командам выявлять и устранять проблемы до того, как они попадут в продакшн. 

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

Пример из практики: баг с учётом регистра при регистрации пользователя

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

Если проверка регистра сделана криво и вся ответственность за это свалена на базу данных, то баг всплывёт только на уровне E2E-тестов или ручных проверок. Например, когда один и тот же пользователь сможет завести аккаунт с адресами, различающимися только регистром букв. А на этом этапе исправлять проблему уже и долго, и дорого.

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

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

Сценарий

Новый разработчик реализует сервис регистрации пользователей и готовит его к выводу в продакшн.

Пример кода метода registerUser

async registerUser(email: string, username: string): Promise<User> {
    const existingUser = await this.userRepository.findOne({
        where: { 
            email: email          
        }
    });
 
    if (existingUser) {
        throw new Error("Email already exists");
    }
    ...
}

Проблема

Метод registerUser неправильно обрабатывает регистр и полагается на базу данных или UI-фреймворк для обеспечения нечувствительности к регистру. В результате пользователи могут регистрировать дублирующиеся email-адреса, отличающиеся только регистром (например, user@example.com и USER@example.com).

Влияние

  • Возникают проблемы с аутентификацией: несоответствие регистра в email приводит к сбоям при входе.

  • Появляются уязвимости безопасности из-за дублирующихся учётных записей.

  • Несогласованность данных усложняет управление учетными записями пользователей.

Метод тестирования 1: юнит-тесты

Такие тесты проверяют только сам код, поэтому проверка учета регистра для email фактически перекладывается на базу данных, где выполняются SQL-запросы. Так как юнит-тесты не выполняются на реальной базе данных, они не способны поймать проблемы с регистром.

Метод тестирования 2: E2E-тесты или ручные проверки

Такие проверки обнаружат проблему только после выката кода на стенд (staging). Автоматизация может помочь, но позднее обнаружение в цикле разработки задерживает обратную связь для разработчиков и делает исправления более долгими и дорогими.

Метод тестирования 3: использование моков для симуляции взаимодействия с БД в юнит-тестах

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

test('should prevent registration with same email in different case', async () => {
  const userService = new UserRegistrationService(new MockRepository());
  await userService.registerUser({ email: 'user@example.com', password: 'password123' });
  await expect(userService.registerUser({ email: 'USER@example.com', password: 'password123' }))
    .rejects.toThrow('Email already exists');
});

В приведенном выше примере сервис User создается с мок-репозиторием, который хранит in-memory представление базы данных — по сути, мапу (map) пользователей. Этот мок-репозиторий обнаружит повторную регистрацию пользователя, вероятно, используя имя пользователя в качестве ключа без учета регистра, и вернет нужную ошибку.

По сути, мы дублируем логику валидации внутри мока — то, что должен делать сам сервис User или база данных. Всякий раз, когда требования к валидации меняются (например, запрещаются специальные символы), нам нужно менять и мок. Иначе тесты будут проверять устаревшую логику. И чем больше моков в коде, тем тяжелее их сопровождать.

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

Метод тестирования 4: локальные интеграционные тесты в стиле shift-left с Testcontainers

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

Преимущества

  • Тесты выполняются за секунды и ловят баги на раннем этапе.

  • Проверка идёт на настоящей базе данных, а не на моках.

  • Критичная бизнес-логика ведёт себя предсказуемо — уверенность в продакшне выше.

Пример интеграционного теста

Сначала настроим контейнер PostgreSQL с помощью библиотеки Testcontainers и создадим userRepository для подключения к этому экземпляру PostgreSQL:

let userService: UserRegistrationService;
 
beforeAll(async () => {
        container = await new PostgreSqlContainer("postgres:16")
            .start();
         
        dataSource = new DataSource({
            type: "postgres",
            host: container.getHost(),
            port: container.getMappedPort(5432),
            username: container.getUsername(),
            password: container.getPassword(),
            database: container.getDatabase(),
            entities: [User],
            synchronize: true,
            logging: true,
            connectTimeoutMS: 5000
        });
        await dataSource.initialize();
        const userRepository = dataSource.getRepository(User);
        userService = new UserRegistrationService(userRepository);
}, 30000);

Теперь, когда userService инициализирован, мы можем вызвать registerUser и протестировать регистрацию с реальной PostgreSQL.

test('should prevent registration with same email in different case', async () => {
  await userService.registerUser({ email: 'user@example.com', password: 'password123' });
  await expect(userService.registerUser({ email: 'USER@example.com', password: 'password123' }))
    .rejects.toThrow('Email already exists');
});

Почему это работает

  • Реальная база PostgreSQL через Testcontainers.

  • Проверка уникальности email без учёта регистра.

  • Проверка формата хранения email.

Как помогает Testcontainers

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

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

Testcontainers Cloud: масштабируемое тестирование для продуктивных команд

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

Использование Testcontainers Cloud как локально, так и в CI гарантирует предсказуемые результаты тестирования и повышает уверенность в ваших изменениях кода. Кроме того, Testcontainers Cloud позволяет легко запускать интеграционные тесты в CI через несколько пайплайнов, что помогает поддерживать высокое качество при масштабной разработке. И наконец, Testcontainers Cloud более безопасен и оптимален для команд и предприятий с повышенными требованиями к механизмам безопасности контейнеров.

Бизнес-эффект от shift-left-тестирования

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

Традиционный процесс: общая интеграционная среда

Разбор процесса:

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

blog without shift left
Рис. 1 — процесс с общей интеграционной средой и временем на каждом шаге.

Lead Time for Changes (LTC): На обнаружение и исправление бага уходит минимум 1–2 часа (иногда больше, в зависимости от загруженности CI/CD и принятых практик). В лучшем случае от коммита до выката в продакшн пройдет около 2 часов. В худшем случае — несколько часов или даже дней, если потребуется несколько итераций.

Deployment Frequency (DF): Так как исправление ошибки в пайплайне занимает примерно 2 часа, а рабочий день ограничен 8 часами, реально можно выполнить только 3–4 деплоя в день. При множественных сбоях частота релизов падает еще сильнее.

Дополнительные сопутствующие издержки: минуты работы CI-агентов и затраты на поддержку общей интеграционной среды.

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

Shift-left-процесс (локальное интеграционное тестирование с Testcontainers)

Разбор процесса:

Shift-left-подход гораздо проще: он начинается с написания кода и запуска юнит-тестов. Вместо того чтобы выполнять интеграционные тесты во внешнем цикле, разработчики могут запускать их локально — во внутреннем цикле — для отладки и исправления проблем. После этого изменения повторно проверяются перед переходом к следующим шагам и внешнему циклу.

blog with shift left
Рисунок 2: Процесс локального интеграционного тестирования с Testcontainers с разбивкой по времени, затраченному на каждый этап. Цикл обратной связи здесь гораздо быстрее, экономит часы работы и снимает часть проблем на следующих этапах.

Lead Time for Changes (LTC): На обнаружение и исправление бага во внутреннем цикле у разработчика уходит меньше 20 минут. Таким образом, локальное интеграционное тестирование позволяет как минимум на 65% быстрее находить дефекты по сравнению с тестированием в общей интеграционной среде.

Deployment Frequency (DF): Поскольку дефект был выявлен и исправлен локально за 20 минут, пайплайн может пройти в продакшн, что дает возможность делать 10 и более релизов в день.

Дополнительные сопутствующие издержки: тратится 5 минут Testcontainers Cloud.

Переключение контекста у разработчиков: Переключение контекста отсутствует, так как локальные тесты дают мгновенную обратную связь по изменениям кода и позволяют разработчику оставаться в IDE и во внутреннем цикле.

Основные выводы

Традиционный процесс (общая интеграционная среда)

Shift-left-процесс (локальное интеграционное тестирование с Testcontainers)

Улучшения и дополнительные примеры

Более быстрый Lead Time for Changes (LTC)

Изменения кода проверяются часами или днями. Разработчики ждут общей CI/CD-среды.

Изменения кода проверяются за минуты. Тестирование выполняется сразу и локально.

65% ускорения LTC — Microsoft сократила время поставки изменений с дней до часов, внедрив практики shift-left.

Более высокая частота релизов (Deployment Frequency, DF)

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

Непрерывное тестирование позволяет выпускать несколько релизов в день.

Рост частоты релизов в 2 раза — отчет DORA 2024 показывает, что практики shift-left более чем удваивают частоту релизов. Лидирующие команды деплоят в 182 раза чаще.

Ниже уровень отказов при изменениях (Change Failure Rate, CFR)

Баги, попавшие в продакшн, ведут к дорогостоящим откатам и экстренным исправлениям.

Больше багов ловится раньше — на этапе CI/CD, что снижает количество сбоев в продакшне.

Снижение CFR — По данным IBM, баги, найденные в проде, стоят в 15 раз дороже, чем пойманные заранее.

Более быстрое среднее время восстановления (Mean Time to Recovery, MTTR)

Исправления занимают часы, дни или недели из-за сложной отладки в общих средах.

Быстрое устранение багов при локальном тестировании. Исправления проверяются за минуты.

Более быстрый MTTR — лучшие команды по данным DORA поднимают сервис меньше чем за час. У отстающих это растягивается на недели, а то и на месяц.

Снижение затрат

Дорогие общие среды, медленные пайплайны, высокая стоимость поддержки.

Не нужны интеграционные среды — меньше расходов на инфраструктуру.

Значительная экономия — ThoughtWorks Technology Radar отмечает общие интеграционные среды как хрупкие и затратные.

Таблица 1: Сводка улучшений ключевых метрик при использовании shift-left-процесса с локальным тестированием на Testcontainers

Заключение

Shift-left-тестирование действительно поднимает качество: баги ловятся раньше, на отладку уходит меньше времени, система стабильнее, а разработчики меньше выпадают из потока. Традиционные общие интеграционные среды, наоборот, тормозят: увеличивают Lead Time for Changes, задерживают релизы и заставляют прыгать между задачами.

Локальные интеграционные тесты с Testcontainers дают то, что нужно:

  • быстрый фидбэк — ошибки видно и правятся за минуты;

  • предсказуемое поведение приложения — тесты гоняются в реалистичной среде;

  • меньше инфраструктуры и расходов — не нужны тяжёлые общие стенды;

  • нормальный «флоу» у разработчиков — тесты запускаются прямо в IDE и не выбивают из контекста.

В итоге Testcontainers — это удобный способ гонять интеграционные тесты локально и находить дорогие проблемы до того, как они попадут в прод. Хотите копнуть глубже — подробности есть в документации проекта: Testcontainers и Testcontainers Cloud.


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

Для тех, кто хочет глубже и с нуля войти в автоматизацию, в OTUS есть курс QA Automation Engineer — практическая программа от экспертов с фреймворками, паттернами и актуальными инструментами тестирования.

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