Testcontainers для локального тестування розподілених AI-систем: як спростити складне
Ми часто стикаємося з тим, що тестування розподілених AI-систем — особливо коли в них задіяні векторні бази даних та LLM-моделі — стає справжнім викликом. Testcontainers, який уже перевищив мільйон завантажень на місяць, пропонує практичне рішення для цієї проблеми. Суть проста: кожен тест отримує власний Docker-контейнер без конфліктів та спільних ресурсів.
У цій статті ми розглянемо, як налаштувати тестове середовище для AI-додатків, реалізувати інтеграційне тестування RAG-конвеєрів та оптимізувати життєвий цикл контейнера для швидких і надійних тестів.
Виклики тестування розподілених AI-систем
Залежності від векторних баз даних
Векторні бази даних працюють за принципово іншою логікою, ніж реляційні системи. Замість точного пошуку за індексами вони виконують пошук за схожістю (similarity search) — повертають вектори, які «близькі» до запиту. Це створює специфічні вимоги до тестування: результати залежать від алгоритмів наближеного пошуку найближчих сусідів, порогів схожості та фільтрів метаданих.
Коли ми вставляємо документ у векторну базу даних, його текстовий вміст перетворюється на числовий масив (float[]) через модель embeddings. Векторна база зберігає ці embedding та забезпечує пошук схожості, але самі вектори не генерує. Це означає, що для створення тестового середовища потрібна координація між базою даних, моделями embedding та логікою додатка. Жодна мокована версія не здатна точно симулювати поведінку реального векторного індексу під навантаженням.
Інтеграція з LLM та embedding-моделями
Сучасні AI-системи створюють безпрецедентні виклики для тестування, які виходять далеко за межі традиційних підходів. Нейронні мережі функціонують як «чорні скриньки» — зв'язок між входами та виходами проходить через складні математичні операції у багатьох шарах. Ця непрозорість ускладнює розуміння того, чому модель генерує конкретний результат.
Недетерміністична поведінка створює додаткові проблеми. На відміну від rule-based систем, нейронні мережі можуть демонструвати різну поведінку при кількох запусках через випадкову ініціалізацію, dropout та інші стохастичні елементи. LLM-відповіді варіюються між запусками, тому інтеграційне тестування потребує статистичних підходів замість детерміністичної верифікації. Замість перевірки точного збігу тексту ми валідуємо структурні властивості відповіді: типи повідомлень, назви викликів інструментів, форми аргументів.
Високовимірний простір вхідних даних робить вичерпне тестування обчислювально неможливим. AI-системи часто працюють з даними, що мають мільйони вимірів — зображення, текст, аудіо. У таких випадках традиційні граничні тести стають неадекватними. Embedding-моделі обробляють текст як токени з максимальним лімітом контекстного вікна. Якщо ми спробуємо згенерувати embedding для надто великої кількості токенів за один виклик, це призведе до помилок або обрізаних векторів.
Складність відтворення production середовища
Побудувати staging-середовище, яке ідеально відображає production, — нереалістична мета. Залежності від зовнішніх API, платіжних шлюзів та downstream-мікросервісів поводяться інакше під реальним навантаженням. Обмеження швидкості (rate limits) та латентність відрізняються від тестових умов. Race conditions, мережеві таймаути та баги конкурентності рідко проявляються в staging, але в production під навантаженням з’являються майже миттєво.
У розподілених системах більшість нестабільних тестів викликана проблемами з тестовими даними, а не багами в коді. Затримки в синхронізації даних, конфлікти паралельних тестів та застарілі стани створюють періодичні збої. Тести проходять при повторному запуску, але знову падають у наступних спробах. Коли тести виконуються паралельно на спільній інфраструктурі, їхні дані не повинні конфліктувати. Забезпечення узгодженості даних між кількома сервісами для валідного тестового сценарію стає експоненційно складнішим.
Проблеми з мокованням AI-компонентів
Мокування симулює поведінку реальних залежностей під час тестування. Проте для AI-компонентів цей підхід створює специфічні ускладнення. Моки повинні залишатися актуальними в міру еволюції реальних сервісів. Коли API змінюють схеми відповідей, додають нові поля або модифікують поведінку, моки потребують відповідних оновлень. Без дисциплінованого супроводу моки розходяться з реальністю і знижують достовірність тестів.
Тести можуть проходити з моками, але падати з реальними інтеграціями. Моки базуються на припущеннях про те, як поводяться залежності. Якщо ці припущення некоректні або неповні, тести перевіряють уявну поведінку замість фактичних системних контрактів. У мікросервісних архітектурах з десятками залежностей створення комплексних моків перетворюється на значне інженерне зусилля. Замокувати векторний пошук, embedding-генерацію та LLM-відповіді одночасно означає будувати паралельну систему, яка ніколи не відобразить production реальність повністю.
Testcontainers як рішення для AI-інфраструктури
Testcontainers — це бібліотека з відкритим кодом, яка створює одноразові екземпляри баз даних, брокерів повідомлень, браузерів та будь-яких інших сервісів, що працюють у Docker-контейнерах. Замість моків або складних конфігурацій середовища ми описуємо тестові залежності як код. Під час запуску тестів контейнери автоматично створюються, а після завершення — видаляються.
Контейнер Docker для кожного AI-сервісу
Контейнери Docker — це автономні пакети, які включають усе необхідне для запуску програмного забезпечення: код, середовище виконання, системні інструменти, бібліотеки та налаштування. На відміну від віртуальних машин, контейнери віртуалізують операційну систему, а не обладнання. Це робить їх більш портативними та ефективними. Docker Model Runner перетворює LLM на OCI-сумісні контейнери, що забезпечує плавну інтеграцію з Testcontainers.
Відтворюваність гарантує, що AI-моделі та додатки працюють однаково незалежно від місця розгортання. Ізоляція дозволяє різним AI-проєктам працювати в окремих контейнерах без конфліктів. Ресурсна ефективність означає, що контейнери споживають менше ресурсів, ніж віртуальні машини, тому більше обчислювальної потужності залишається для AI-обробки.
Життєвий цикл контейнера в тестах
Testcontainers призначає спеціальну мітку під час створення контейнерів і використовує Ryuk Container для автоматичного видалення контейнерів з цією міткою після завершення JVM-процесу тестів. Бібліотека надає JUnit 5 Extension, який спрощує запуск та зупинку контейнерів через анотації.
Якщо поле оголошене як статичне (static field), контейнер запускається один раз перед виконанням усіх тестів класу і зупиняється після їхнього завершення. Для полів-екземплярів новий контейнер стартує перед кожним тестовим методом і зупиняється після його виконання. Spring Boot інтегрується з Testcontainers через концепцію ServiceConnection, що значно спрощує написання інтеграційних тестів. Модуль Ollama дозволяє легко запускати локальні моделі через анотацію ServiceConnection без ручного керування контейнерами.
Замість послідовного запуску контейнерів ми можемо запускати їх паралельно, використовуючи Startables.deepStart(postgres, kafka).join(). Цей підхід скорочує час підготовки тестового середовища для складних AI-систем з багатьма залежностями.
Ізоляція тестових середовищ
Контейнеризація компонентів забезпечує виконання кожного тесту у свіжому, відомому стані. Тестування баз даних в ізоляції усуває невідповідності, які виникають через різні локальні налаштування. Контейнеризовані браузери забезпечують надійніше UI-тестування, усуваючи варіації через локальні налаштування браузерів.
Ми можемо проводити наскрізне тестування в середовищі, максимально наближеному до production. Це зменшує розбіжності між середовищами та підвищує точність результатів тестів. Завдяки коротким термінам виконання контейнерів ми ефективніше керуємо ресурсами та забезпечуємо швидке виконання тестів.
Автоматичне керування залежностями
Testcontainers надає бібліотеку програмних API для роботи з контейнерами, приховуючи складні деталі кожної залежності. Це знижує операційні витрати проєктів, заохочує експериментування та спрощує процес розробки. Spring AI надає Spring Boot автоконфігурацію для встановлення з’єднання з модельним сервісом або векторним сховищем через Testcontainers.
Замість ручного конфігурування та розгортання моделі ми автоматизуємо процес за допомогою скриптів. Це знижує адміністративне навантаження на розробників і додає рівень абстракції для самої моделі. Навіть без явної зупинки контейнера Testcontainers автоматично видаляє контейнери через Ryuk Container, гарантуючи чисте середовище після тестів.
Налаштування тестового середовища для AI-додатків
Налаштування тестового середовища для AI-додатків вимагає координації між векторними базами даних, моделями та асинхронними компонентами. Testcontainers дозволяє запускати всю інфраструктуру локально через Docker-контейнери. Це забезпечує однаковість тестових та production середовищ.
Конфігурація векторної бази даних (PostgreSQL з pgvector)
pgvector — це розширення для PostgreSQL, яке додає векторний пошук схожості до класичної SQL-бази. Якщо ви вже використовуєте Postgres у додатку, можна зберігати все в одній базі даних: і структуровані дані, і вектори. Розширення додає векторний тип колонки до Postgres, куди ми вставляємо embeddings як масиви float. pgvector реалізує HNSW та IVF індекси на цих колонках для прискорення пошуку схожості.
Векторні таблиці об’єднуються з реляційними через JOIN. Ми застосовуємо фільтри в одному SQL-запиті, комбінуючи семантичний та структурований пошук у RAG-системах. Пошук найближчих сусідів виконується стандартним SQL-синтаксисом, що дозволяє комбінувати векторні оцінки в ширших SELECT-виразах. Розширення ліцензоване під MIT і працює на Postgres або сумісних сервісах, таких як AWS Aurora і Supabase, без спеціального обладнання.
Найбільша перевага — єдине сховище. Embeddings зберігаються поруч з даними додатка без необхідності синхронізації між системами. SQL, зовнішні ключі та складні фільтри використовуються природно. Водночас Postgres не призначений для масивних векторних навантажень — великі таблиці embeddings можуть уповільнити інші операції бази.
Підняття Ollama-контейнера для локальних моделей
Ollama доступний як офіційний Docker-образ, що значно спрощує запуск великих мовних моделей через контейнери. Всі взаємодії з моделями відбуваються локально, без передачі приватних даних третім сторонам. Ollama підтримує запуск моделей з GPU-прискоренням і надає CLI та REST API для взаємодії з додатками.
Для CPU-режиму команда виглядає так:
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama.
Для Nvidia GPU спочатку потрібно встановити Nvidia Container Toolkit, після чого контейнер запускається з параметром --gpus=all. Модель запускається всередині контейнера командою:
docker exec -it ollama ollama run llama2.
Порт 11434 є стандартним для Ollama.
Інтеграція Redis для кешування embeddings
Redis підтримує векторний пошук схожості через модуль RediSearch. Зокрема, Azure Managed Redis інтегрується з моделями embeddings для RAG, семантичного кешування та рекомендаційних систем. Векторні embeddings зберігаються в хешах або JSON-документах разом з метаданими: ID документа, назва, URL, категорія, timestamp.
RediSearch підтримує метрики відстані L2, COSINE та IP. Для пошуку використовуються KNN-алгоритми з FLAT та HNSW індексами. Гібридний пошук поєднує векторні запити з геопросторовою фільтрацією, числовими та текстовими фільтрами.
Налаштування Kafka для асинхронної обробки
Kafka 4.0 більше не використовує ZooKeeper, що спрощує налаштування через KRaft-режим. У конфігурації Docker Compose визначено брокер на порту 9092, контролер на порту 9093 та healthcheck через перевірку топіків.
Тестування асинхронних потоків потребує ізоляції. Ми додаємо кореляційні ID до заголовків повідомлень Kafka для фільтрації за тестами. Кожен тест отримує унікальний UUID як correlation_id. Споживачі використовують окремі consumer groups, щоб уникнути rebalance.
ПРИЄДНУЙСЯ ДО НАШОЇ КОМАНДИ
Інтеграційне тестування AI-workflows
Тестування RAG pipeline з реальною векторною базою
Тестування RAG-конвеєра означає одночасну перевірку механізмів пошуку, рівня оркестрації та самої генеративної моделі.
Якщо чат-бот відповідає некоректно, причина може бути в кількох місцях:
- пошук повернув нерелевантні документи;
- промпт не оптимізований для сценарію використання;
- правильний документ знайдено, але модель його проігнорувала;
- модель вигадала інформацію, відсутню в контексті;
- чат-бот не викликав інструмент для отримання даних;
- інструмент повернув помилку, яка не була передана моделі.
RAG рідко падає через одну точку збою, тому тестування вимагає поєднання традиційних QA-технік та методів оцінювання, спеціально розроблених для LLM-систем.
Перевірка генерації embeddings
Оцінюючи embeddings-моделі, ми балансуємо між трьома критичними показниками:
- Швидкість вимірюється загальним часом обробки (у секундах) для набору текстів. Низька затримка критична для застосунків реального часу — миттєвих пошукових підказок чи RAG-запитів.
- Розмір моделі (МБ/ГБ) визначає споживання пам'яті та обчислювальної потужності. Наприклад, масивна embeddinggemma (621 МБ) вимагає значно більше системних ресурсів, ніж компактна granite-embedding (62 МБ).
- Розмірність вектора (384, 768, 1024) задає рівень складності: більша розмірність означає більше числових "слотів" для захоплення нюансів значень, семантичних зв'язків та контексту тексту.
Проте головним критерієм залишається семантична точність — два вектори мають бути числово близькими тільки тоді, коли фрагменти тексту семантично схожі.
Валідація similarity search запитів
Пошук схожості працює через алгоритми наближеного пошуку найближчих сусідів (ANN), такі як HNSW та IVF.
Ми оцінюємо ефективність пошуку за кількома ключовими метриками:
- Recall@k показує, який відсоток релевантних контекстів потрапив у топ-k результатів. Ця метрика безпосередньо впливає на якість відповідей LLM та рівень галюцинацій.
- Латентність (p95/p99) визначає час отримання результатів на 95-му та 99-му персентилі. Це критично важливо для сприйняття швидкості всього конвеєра обробки.
Під час тестування ми порівнюємо вектори через функції відстані. Найчастіше використовується косинусна відстань — вона дає змогу визначити топ-10 фрагментів, найбільш схожих на запит користувача.
End-to-end тестування чат-бота з MCP інструментами
Model Context Protocol (MCP) дає змогу AI-агентам взаємодіяти з інфраструктурою тестування через стандартизований протокол.
MCP-сервер тестового раннера надає кілька ключових можливостей:
- виконання тестів та отримання результатів у структурованому JSON-форматі;
- перевірка покриття коду для конкретного файлу;
- запуск тестів, що відповідають певному патерну.
Коли AI-агент отримує доступ до MCP-сервера, він працює за таким циклом: генерує тест, негайно виконує його для перевірки працездатності, аналізує вплив на покриття коду та ітерує доти, доки тест не відповідатиме стандартам якості.
Завдяки цьому тісному циклу зворотного зв'язку якість AI-генерованих тестів суттєво зростає.
Evaluation тестування відповідей LLM
Оцінювання LLM систематично вимірює якість застосунків через кілька ключових вимірів: коректність, релевантність, безпеку та зв'язність.
Faithfulness (достовірність) оцінює фактичну узгодженість згенерованої відповіді з отриманим контекстом. Це дає змогу виявити галюцинації моделі. Answer relevancy (релевантність відповіді) перевіряє, наскільки стисла відповідь та чи безпосередньо вона адресує запит користувача. Contextual recall (повнота контексту) оцінює, чи містить знайдений контекст всю інформацію, необхідну для створення очікуваного результату.
Фреймворки DeepEval та Ragas надають тестові кейси в pytest-стилі, які виконуються локально та в CI. Вони оцінюють те, що просте порівняння рядків не може захопити — семантичну якість та коректність відповідей.
Практичні патерни та оптимізація
Повторне використання контейнерів між тестами
Повторне використання контейнерів (reusable containers) — це експериментальна можливість, що суттєво оптимізує продуктивність тестування. Контейнери продовжують працювати між запусками тестів, дозволяючи наступним виконанням використовувати ту саму конфігурацію замість повторного холодного старту.
Використовуючи цю функцію правильно, ми можемо скоротити 30-секундний цикл зворотного зв'язку до 2 секунд.
Коли контейнер маркується .withReuse(true) та повторне використання активоване в середовищі, Testcontainers обчислює хеш конфігурації контейнера. До хешу входять: образ, відкриті порти, змінні середовища, команда, скопійовані файли, стратегія очікування та інші налаштування.
Перед запуском нового контейнера система перевіряє, чи існує запущений контейнер з відповідним хешем. Якщо так — вона підключається до існуючого замість створення нового.
Для активації створіть файл ~/.testcontainers.properties та додайте testcontainers.reuse.enable=true.
Важливо зазначити: повторне використання — це оптимізація виключно для локальної розробки, ніколи для CI. Кожна збірка має бути відтворюваною та ізольованою, коли запускають контейнери заново.
Також пам'ятайте, що повторно використовувані контейнери виключаються з автоматичного очищення Ryuk. Це означає, що розробник несе відповідальність за їхню зупинку вручну.
Seed даних з production знімків
Синтетична генерація даних — це програмне створення записів через спеціалізовані бібліотеки, які генерують реалістичні значення: імена, адреси, електронні пошти, телефонні номери, грошові суми, дати.
Маскування production-даних працює інакше: воно копіює реальні production-дані та замінює чутливі поля реалістичними синтетичними замінниками. Результат — база даних з тією самою схемою, тією самою цілісністю посилань, тими самими розподілами даних та тим самим обсягом, як у production, але без реальних користувацьких даних.
Database branching створює миттєві ізольовані копії бази для кожного тестового запуску або pull request через Copy-on-Write сховище.
Коли створюється гілка, жодні дані фізично не копіюються — гілка містить лише вказівник на стан сховища батьківського елемента. Завдяки цьому база даних розміром 40 ГБ розгалужується менше ніж за секунду, оскільки байти не переміщуються до моменту першого запису.
Тестування міграцій схем векторних баз
Testcontainers надає ефемерні, реальні екземпляри баз даних для кожного набору тестів. Міграції запускаються автоматично при старті контейнера, що дає змогу виявляти дрейф схеми негайно.
Кожен набір тестів отримує свіжу базу даних без проблем зі зміненим станом чи залишковими даними з попередніх запусків.
Ідемпотентні seed-скрипти можуть запускатися кілька разів безпечно та зберігаються в Git разом з кодом, забезпечуючи повну відтворюваність тестового середовища.
Моніторинг ресурсів під час тестів
Моніторинг використання ресурсів під час тестування продуктивності критично важливий для перевірки, чи витримає система очікуване навантаження.
Чотири ключові області моніторингу:
CPU: використання процесора понад 80% протягом тривалого періоду сигналізує про вузьке місце в системі.
Пам'ять: витоки пам'яті виявляються через моніторинг доступних байтів та приватних байтів процесу. Поступове зростання споживання пам'яті без її звільнення вказує на проблему.
Диск: моніторинг операцій читання/запису та доступного простору допомагає виявити проблеми з продуктивністю дискової підсистеми.
Мережа: латентність мережі показує час, необхідний для відправки пакета даних через мережеве з'єднання. Висока латентність може суттєво вплинути на загальну продуктивність системи.
Висновок
Розглянувши комплексний підхід до тестування розподілених AI-систем через Testcontainers, можна дійти висновку, що замість моків і складних конфігурацій ми отримуємо реальні екземпляри векторних баз даних, LLM-моделей та асинхронних компонентів у Docker-контейнерах. Завдяки цьому кожен тест виконується в ізольованому середовищі без конфліктів даних.
Продемонстровані патерни налаштування pgvector, Ollama, Redis та Kafka спрощують локальну розробку та CI/CD-процеси. Оптимізація через повторне використання контейнерів скорочує цикли зворотного зв'язку з 30 до 2 секунд — це прискорення в 15 разів.
Результат — надійні інтеграційні тести, що відображають production-реальність та виявляють проблеми на ранніх етапах розробки AI-застосунків.
Підписатися на новини
-
Думка експертаСтворення PWA [1.1]у 2026: як зробити JavaScript-застосунки доступними офлайн
Як створити PWA, використовуючи service worker для офлайн-функціоналу, стратегії кешування даних та реалізацію у React-застосунках.
-
ЛайфхакиСкільки коштує ваше масштабування? FinOps-стратегії для критичних моментів
-
Думка експертаOperational Intelligence - Tech Pulse | Дайджест #3
-
Думка експерта
Spring AI vs LangChain4j: який фреймворк обрати для AI в Java
-
ЛайфхакиOpenTelemetry і eBPF для Python-мікросервісів: як працює distributed tracing і з чого почати
Як працює distributed tracing і з чого почати: практичне налаштування трейсингу, автоматичний збір метрик та інтеграція обох технологій.