Testcontainers – open-source фреймворк, позволяющий развернуть одноразовый экземпляр бд в Docker контейнере.
Контейнеры создаются/запускаются перед выполнением тестов и останавливаются/удаляются после выполнения всех тестов.
Материалы:
- Доки на testcontainers.com, в частности:
- Testcontainers container lifecycle management using JUnit 5 – жизненный цикл контейнеров и способы их хранения в памяти
- Доки на java.testcontainers.org
- Пример на github
- Database Testing with Testcontainers and Kotlin Exposed ORM (vuongdang.dev)
- Testcontainers Best Practices (docker.com)
Подключение с использованием PostgreSQL:
1 |
testImplementation("org.testcontainers:postgresql:1.19.8") |
Разворачивать и удалять экземпляр бд в контейнере докера для каждого тестового класса (и тем более для каждого теста) слишком ресурсозатратно. По-хорошему нужно, чтобы экземпляр БД был автоматически создан 1 раз перед запуском любого теста, и удален при завершении всех тестов.
Для этого можно сохранить созданный экземпляр контейнера как Singleton (см. Using Singleton Containers). Другими способами (через Extension annotations, JUnit5 BeforeAllCallback и др.) “из коробки” лично у меня не получилось сделать один единственный запуск контейнера для всех тестовых классов. Можно было добавить проверку с использованием AtomicBoolean
и т.п., но стоит ли оно того.
Т.е. создаете контейнер в статической области памяти с указанием имени образа в докере, где крутится субд (в данном примере PostgreSQL), сразу запускаете (создаете) его и подключаетесь к только что созданной бд. Вызывать удаление контейнера не нужно, это либа берет на себя.
1 2 3 4 5 6 7 8 |
companion object { private val postgreSQLContainer = PostgreSQLContainer("postgres:16.2") init { postgreSQLContainer.start() Database.connect(...) } } |
По-умолчанию либа создает бд со “случайным” именем, логином, паролем и т.п. (их значения можно узнать после создания контейнера), но можно задать и свои.
Также, существует экспериментальный флаг reusable
, при включении которого повторные запуски контейнера (.start()
) автоматически будут осуществляться с одинаковыми параметрами, т.е. не будут меняться от запуска к запуску (пока вручную не вызовешь .stop()
).
Ниже пример:
- с указанием собственных параметров запуска (в т.ч. портов, что НЕ рекомендуется делать, но иногда может понадобиться)
- с использованием подключения к бд через JDBC-коннектор HikariCP
- с запуском механизма миграции бд с помощью Flyway
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
companion object { private const val DOCKER_IMAGE_NAME = "postgres:16.2" private val postgreSQLContainer = PostgreSQLContainer("postgres:16.2") //.withPorts(containerPort = 5432, localPort = 54321) .withDatabaseName("delivery_test") .withUsername("test") .withPassword("test") init { postgreSQLContainer.start() postgreSQLContainer.connectToDatabase() } // если нужно вручную указать конкретные порты, что НЕ рекомендуется private fun PostgreSQLContainer<*>.withPorts(containerPort: Int, localPort: Int): PostgreSQLContainer<*> { return this .withExposedPorts(containerPort) .withCreateContainerCmdModifier { cmd -> cmd.withHostConfig( HostConfig().withPortBindings( PortBinding(Ports.Binding.bindPort(localPort), ExposedPort(containerPort)) ) ) } } private fun PostgreSQLContainer<*>.connectToDatabase() { val container = this println("TEST CONNECT: jdbcUrl=${container.jdbcUrl}, username=${container.username}, password=${container.password}") val hikariTestConfig = HikariConfig().apply { jdbcUrl = container.jdbcUrl username = container.username password = container.password driverClassName = "org.postgresql.Driver" maximumPoolSize = 10 } val testDataSource = HikariDataSource(hikariTestConfig) val flyway = Flyway.configure().dataSource(testDataSource).load() flyway.repair() flyway.migrate() Database.connect(testDataSource) } } |