惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Heimdal Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Security Archives - TechRepublic
Security Archives - TechRepublic
K
Kaspersky official blog
P
Palo Alto Networks Blog
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
V
Vulnerabilities – Threatpost
V2EX - 技术
V2EX - 技术
Security Latest
Security Latest
有赞技术团队
有赞技术团队
量子位
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Y
Y Combinator Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Check Point Blog
雷峰网
雷峰网
Blog — PlanetScale
Blog — PlanetScale
Cloudbric
Cloudbric
Simon Willison's Weblog
Simon Willison's Weblog
Vercel News
Vercel News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
L
LangChain Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Commits to openclaw:main
Recent Commits to openclaw:main
AWS News Blog
AWS News Blog
N
News and Events Feed by Topic
Forbes - Security
Forbes - Security
N
Netflix TechBlog - Medium
美团技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
Security Affairs
酷 壳 – CoolShell
酷 壳 – CoolShell
L
Lohrmann on Cybersecurity
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The Last Watchdog
The Last Watchdog
L
LINUX DO - 热门话题
The GitHub Blog
The GitHub Blog
O
OpenAI News
Google DeepMind News
Google DeepMind News
博客园 - 【当耐特】
P
Privacy International News Feed
aimingoo的专栏
aimingoo的专栏
J
Java Code Geeks
G
Google Developers Blog
Jina AI
Jina AI
T
The Exploit Database - CXSecurity.com

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Интеграционные тесты в Java: ускоряем Testcontainers через tmpfs и прединициализацию
Святослав Апанасёнок · 2026-06-17 · via Все публикации подряд на Хабре

Средний

6 мин

97

Введение

Testcontainers - Java-библиотека, которая управляет Docker-контейнерами прямо из тестового кода. Во время выполнения тестов она запускает нужный контейнер - базу данных, брокер сообщений, поисковый движок и т.д. - а по завершении останавливает и удаляет контейнер.

Зачем это нужно? Для интеграционных тестов на реальном ПО, а не на in-memory эмуляторах. Тест работает с тем же движком, что и в продакшене.

В этой статье я разберу, как можно оптимизировать работу с Testcontainers:

  1. tmpfs - перенос файлов в оперативную память.

  2. Прединициализация - перенос тяжёлой инициализацию в отдельный Docker-образ.

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

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

Часть 1. Testcontainers и tmpfs

Обычный тест на MySQL

Зависимости:

dependencies {
    testImplementation platform("org.testcontainers:testcontainers-bom:2.0.4")
    testImplementation "org.testcontainers:testcontainers-mysql"
    testImplementation "com.mysql:mysql-connector-j:9.6.0"
    testImplementation "org.testcontainers:testcontainers-junit-jupiter"
    testImplementation "org.junit.jupiter:junit-jupiter"
}

Поднимаем MySQL-контейнер, прокидываем имя БД и креды, скармливаем пару скриптов инициализации и стартуем:

MySQLContainer container = new MySQLContainer(DockerImageName.parse("mysql:8.0.45"));
container.withDatabaseName("testdb")
    .withUsername("user")
    .withPassword("password")
    .withInitScripts("mysql/init.tables.sql", "mysql/init.data.sql");
container.start();

Далее выполняются сами тесты.

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

Куда уходит время

Если упрощённо нарисовать путь от вызова start() до готового контейнера, получится примерно так:

container.start() → pull Docker-образа → запуск процесса СУБД → инициализация данных (DDL, миграции, тестовые данные) → сервис готов

После первого docker pull образ уже лежит локально, так что этот этап можно не учитывать. Основное время уходит на два других этапа: запуск процесса СУБД внутри контейнера и инициализацию данных. Первое почти не зависит от размера бд, но занимает много времени: mysqld стартует, инициализирует свои служебные каталоги и затем открывает порт. Второе зависит от размера бд: множество таблиц, индексы, объёмные тестовые данные, набор Flyway/Liquibase миграций - удлиняют запуск.

tmpfs: первый шаг

Самое простое, что можно сделать, не меняя логики, - положить каталог данных СУБД в tmpfs, то есть в RAM:

container.withTmpFs(Map.of("/var/lib/mysql", "rw"));

Создание таблиц и прочий I/O начинают работать быстрее.

Замеры

Замер времени container.start() с двумя профилями нагрузки:

  • пустая БД - чистый старт без данных;

  • 100 таблиц - 100 таблиц по 20 строк, то есть 2000 INSERT плюс DDL.

Медиана времени container.start(), мс.

MySQL: с tmpfs и без

Сценарий

Testcontainers, мс

Testcontainers + tmpfs, мс

Пустая БД

10 513

8 593

100 таблиц

28 437

13 613

На пустой базе выигрыш скромный - экономия пары секунд на старте процесса. А вот при тяжёлой инициализации разница уже двукратная: было ~28,4 секунды, стало ~13,6. Эффект налицо. Но 13,6 секунды на один запуск теста - это всё ещё очень много, особенно когда сам тест отрабатывает за десятки миллисекунд.

Часть 2. Прединициализация контейнеров

Идея: сделать инициализацию один раз

Ключевое наблюдение: если инициализация детерминирована (одни и те же скрипты, одни и те же креды, один и тот же docker-образ дают один и тот же результат), то нет смысла повторять её на каждом старте. Достаточно один раз:

  1. поднять временный контейнер;

  2. выполнить в нём всю инициализацию - DDL, миграции, заполнить тестовыми данными;

  3. превратить готовый контейнер в Docker-образ через docker commit;

  4. в дальнейших тестах использовать уже этот подготовленный образ.

Тяжёлая инициализация выполняется один раз при сборке образа, а не при каждом start(). Это и есть прединициализация.

На практике всё укладывается в две фазы:

Фаза

Когда

Что происходит

Сборка Docker-образа

Первый запуск

Сервис стартует, используя tmpfs, и проходит инициализацию. При остановке контейнера файлы сохраняются в отдельный каталог (не tmpfs), затем собирается Docker-образ.

Тестовый старт

Каждый start()

При старте файлы восстанавливаются в tmpfs, после чего запускается процесс СУБД. Скрипты инициализации повторно не выполняются.

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

Библиотека preinit-testcontainers

Всю эту логику я реализовал в библиотеке preinit-testcontainers.

Библиотека - модульная. Содержит несколько модулей, в частности preinit-testcontainers-mysql для MySQL. Так же есть модули для других сервисов: Clickhouse, PostgreSQL, redis. Библиотека универсальна и может быть использована для запуска любых других контейнеров, а не только вышеперечисленных.

Импорт MySQL-модуля:

testImplementation "com.sviat-tech:preinit-testcontainers-mysql:2.0.1"

Создание контейнера выглядит следующим образом:

import com.sviattech.preinittestcontainers.PreInitStartCallback;
import com.sviattech.preinittestcontainers.mysql.CreateMySQLContainerCommand;
import com.sviattech.preinittestcontainers.mysql.MySQLContainerFactory;
import org.testcontainers.mysql.MySQLContainer;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.List;

CreateMySQLContainerCommand command = CreateMySQLContainerCommand.builder()
    .withBaseImageName("mysql:8.0.45")
    .withInitScripts(List.of("mysql/init.tables.sql", "mysql/init.data.sql"))
    .withDbName("testdb")
    .withUsername("user")
    .withPassword("password")
    .withAfterPreInitStartCallback(PreInitStartCallback.of(
        "mysql-callback-seed-v1",
        container -> {
            MySQLContainer mysql = (MySQLContainer) container;
            try (Connection connection = DriverManager.getConnection(
                    mysql.getJdbcUrl(), "user", "password");
                 Statement statement = connection.createStatement()) {
                statement.execute("INSERT INTO users (id, name) VALUES (999, 'from-callback')");
            }
        }))
    .build();

try (MySQLContainer container = MySQLContainerFactory.createMySQLContainer(command)) {
    container.start();
    // assertions, JDBC, Spring Data...
}

Цифры

Здесь два разных этапа:

  • первый запуск - сборка подготовленного образа;

  • повторный start() - обычный старт уже после сборки.

Сначала посмотрим на повторный старт - ради него всё и затевалось.

Testcontainers + tmpfs против Preinit + tmpfs (повторный старт)

Сценарий

Testcontainers + tmpfs, мс

Preinit + tmpfs (повторный старт), мс

100 таблиц

13 613

1 389

Пустая БД

8 593

1 445

Для MySQL со 100 таблицами время старта падает с ~13,6 до ~1,4 секунды - примерно в 10 раз. На пустой БД эффект тоже значительный (~6×).

Preinit + tmpfs (первый старт) - однократные затраты

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

Сценарий

MySQL, мс

100 таблиц

15 174

Пустая БД

9 967

Отдельно фаза сборки для MySQL со 100 таблицами - ~13,8 с. Это сопоставимо с Testcontainers + tmpfs + инициализация.

Выгода очевидна: на первый запуск уходит ~15 секунд, а дальше каждый старт - ~1,4 секунды вместо ~13,6.

Кратко, как менялось время старта:

  • обычный контейнер со скриптами инициализации - десятки секунд;

  • tmpfs уменьшает это время примерно вдвое;

  • прединициализация - однократная сборка образа (~15 с);

  • повторный тестовый старт - около 1,4 секунды.

Замеры на разных СУБД

Медиана времени container.start(), мс. Два профиля нагрузки: пустая БД и 100 таблиц (100 таблиц × 20 строк).

Сценарий

Режим

MySQL, мс

PostgreSQL, мс

ClickHouse, мс

Пустая БД

Testcontainers

10 513

1 508

5 486

Пустая БД

Testcontainers + tmpfs

8 593

1 325

5 576

Пустая БД

Preinit + tmpfs (повторный старт)

1 445

451

2 388

100 таблиц

Testcontainers

28 437

15 068

29 526

100 таблиц

Testcontainers + tmpfs

13 613

3 663

14 256

100 таблиц

Preinit + tmpfs (повторный старт)

1 389

551

3 403

Заключение

Итого:

  1. tmpfs ускоряет работу контейнера, но не убирает повторную инициализацию.

  2. Прединициализация убирает главную повторяющуюся проблему - инициализацию на каждом старте, делая ее однокртано при первом запуске.

Для MySQL на тяжёлом сценарии разница между Testcontainers + tmpfs и preinit + tmpfs составила примерно 13,6 с против 1,4 с. Это уменьшение времени инициализации на порядок.

Исходники и примеры: