# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project - Step 2: Test Container Setup ## Purpose This guide provides step-by-step instructions for setting up a PostgreSQL Docker container for tests, exposing an `io.ebean.Database` instance for use in test classes. This is Step 2 of 3. Complete this step before configuring the production database in Step 3. Getting the test container working first gives you a fast feedback loop - you can verify entity changes compile, enhance, and persist correctly with `mvn verify` before wiring up production datasource configuration. --- ## Prerequisites - **Step 1 complete**: `pom.xml` includes `ebean-postgres`, `ebean-maven-plugin`, `querybean-generator`, and **`ebean-test`** as a test-scoped dependency (see `add-ebean-postgres-maven-pom.md`) - **Step 0 answers recorded**: DI framework choice and PostGIS requirement - **Docker** is installed and running on the developer machine --- ## Overview: Choosing your approach The approach depends on the DI framework choice made in Step 0: | DI framework | Approach | How | |--------------|----------|-----| | **Avaje Inject** | Programmatic | `@TestScope @Factory` class with injectable `Database` bean | | **Spring** | Programmatic | `@TestConfiguration` class with `@Bean` methods | | **None** | Declarative | `application-test.yaml` + plain JUnit test | Follow the path that matches your choice below. --- ## Path A — Programmatic with Avaje Inject (recommended) This approach uses `@TestScope @Factory` to expose the container and `Database` as injectable beans. It offers more control (image mirrors, custom config) and makes `Database` directly injectable into test classes. ### A.1 — Verify Avaje Inject test dependencies Confirm the following are present in `pom.xml` (in addition to `ebean-test`): ```xml io.avaje avaje-inject ${avaje-inject.version} io.avaje avaje-inject-test ${avaje-inject.version} test ``` And the `avaje-inject-generator` annotation processor in `maven-compiler-plugin`: ```xml io.avaje avaje-inject-generator ${avaje-inject.version} ``` ### A.2 — Create a `@TestScope @Factory` class Create a new class in the test source tree (e.g., `src/test/java/.../testconfig/TestConfiguration.java`): ```java package com.example.testconfig; import io.avaje.inject.Bean; import io.avaje.inject.Factory; import io.avaje.inject.test.TestScope; import io.ebean.Database; @TestScope @Factory class TestConfiguration { // bean methods added below } ``` ### A.3 — Add a container bean and a Database bean #### Plain PostgreSQL ```java import io.ebean.test.containers.PostgresContainer; @TestScope @Factory class TestConfiguration { @Bean PostgresContainer postgres() { return PostgresContainer.builder("17") // Postgres image version .dbName("my_app") // database to create inside the container .build() .start(); } @Bean Database database(PostgresContainer container) { return container.ebean() .builder() .build(); } } ``` #### PostGIS (PostgreSQL + PostGIS extension) Use `PostgisContainer` instead. The default image is `ghcr.io/baosystems/postgis:{version}` and the extensions `hstore`, `pgcrypto`, and `postgis` are installed automatically. ```java import io.ebean.test.containers.PostgisContainer; @TestScope @Factory class TestConfiguration { @Bean PostgisContainer postgres() { return PostgisContainer.builder("17") .dbName("my_app") .build() .start(); } @Bean Database database(PostgisContainer container) { return container.ebean() .builder() .build(); } } ``` #### Key differences | | PostgresContainer | PostgisContainer | |---|---|---| | Docker image | `postgres:{version}` | `ghcr.io/baosystems/postgis:{version}` | | Default extensions | `hstore, pgcrypto` | `hstore, pgcrypto, postgis` | | Default port | 6432 | 6432 | | Optional LW mode | — | `.useLW(true)` (see Optional section) | ### A.4 — Write a test Annotate the test class with `@InjectTest` and inject `Database` with `@Inject`: ```java package com.example.testconfig; import io.avaje.inject.test.InjectTest; import io.ebean.Database; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @InjectTest class DatabaseTest { @Inject Database database; @Test void database_isAvailable() { assertThat(database).isNotNull(); } } ``` ### A.5 — Verify ```bash mvn verify ``` Expected log output: ``` INFO Container ut_postgres running with port:6432 ... INFO connectivity confirmed for ut_postgres INFO DataSourcePool [my_app] autoCommit[false] ... INFO DatabasePlatform name:my_app platform:postgres INFO Executing db-create-all.sql - ... ``` **Important:** Verify this step passes with `mvn verify` before proceeding to Step 3 (production database configuration). --- ## Path B — Programmatic with Spring Use Spring’s `@TestConfiguration` to provide the container and `Database` beans. ### B.1 — Create a `@TestConfiguration` class ```java package com.example.testconfig; import io.ebean.Database; import io.ebean.test.containers.PostgresContainer; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; @TestConfiguration class TestDatabaseConfig { @Bean PostgresContainer postgres() { return PostgresContainer.builder("17") .dbName("my_app") .build() .start(); } @Primary @Bean Database database(PostgresContainer container) { return container.ebean() .builder() .build(); } } ``` For PostGIS, use `PostgisContainer` instead (same pattern as Path A). ### B.2 — Write a test ```java package com.example; import io.ebean.Database; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest class DatabaseTest { @Autowired Database database; @Test void database_isAvailable() { assertThat(database).isNotNull(); } } ``` ### B.3 — Verify Run `mvn verify` and confirm the same log output as Path A. --- ## Path C — Declarative (no DI framework) This is the simplest approach but offers less control. `ebean-test` reads a YAML config file and automatically manages the Docker container and `Database` instance. Use this when the project has no DI framework. ### C.1 — Create `application-test.yaml` Create `src/test/resources/application-test.yaml`: ```yaml ebean: test: platform: postgres ddlMode: dropCreate dbName: my_app ``` For PostGIS, use `platform: postgis` instead. ### C.2 — Write a test Use `DB.getDefault()` to obtain the `Database` instance: ```java package com.example; import io.ebean.DB; import io.ebean.Database; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class DatabaseTest { @Test void database_isAvailable() { Database database = DB.getDefault(); assertThat(database).isNotNull(); } } ``` ### C.3 — Verify ```bash mvn verify ``` Expected log output: ``` INFO Container ut_postgres running with port:6432 ... INFO connectivity confirmed for ut_postgres INFO DataSourcePool [my_app] autoCommit[false] ... INFO DatabasePlatform name:my_app platform:postgres ``` **Important:** Verify this passes before proceeding to Step 3. Skip to [Optional configurations](#optional-configurations) or proceed to Step 3. --- ## Optional configurations ### Image mirror (for CI / private registry) If CI builds pull images from a private registry (e.g., AWS ECR) instead of Docker Hub or GitHub Container Registry, specify a mirror. The mirror is **only used in CI** - it is ignored on local developer machines (where Docker Hub / GHCR is used directly). ```java @Bean PostgresContainer postgres() { return PostgresContainer.builder("16") .dbName("my_app") .mirror("123456789.dkr.ecr.ap-southeast-2.amazonaws.com/mirrored") .build() .start(); } ``` Alternatively, set the mirror globally via a system property or `ebean.test.containers.mirror` in a properties file, avoiding code changes per project. ### Read-only datasource (for tests using read-replica simulation) Call `.autoReadOnlyDataSource(true)` on the `DatabaseBuilder` to automatically create a second read-only datasource pointing at the same container: ```java @Bean Database database(PostgresContainer container) { return container.ebean() .builder() .autoReadOnlyDataSource(true) // test read-only queries against same container .build(); } ``` ### Dump metrics on shutdown Useful for performance analysis during test runs: ```java @Bean Database database(PostgresContainer container) { return container.ebean() .builder() .dumpMetricsOnShutdown(true) .dumpMetricsOptions("loc,sql,hash") .build(); } ``` ### PostGIS: LW mode (HexWKB) For PostGIS with DriverWrapperLW (HexWKB binary geometry encoding), set `.useLW(true)`. This switches the JDBC URL prefix to `jdbc:postgresql_lwgis://` and requires the `net.postgis:postgis-jdbc` dependency on the test classpath: ```xml net.postgis postgis-jdbc 2024.1.0 test ``` ```java @Bean PostgisContainer postgres() { return PostgisContainer.builder("16") .dbName("my_app") .useLW(true) // use HexWKB + DriverWrapperLW .build() .start(); } ``` > **Note**: LW mode is not required for most PostGIS use cases. Only enable it if > your entities use binary geometry types (e.g., `net.postgis.jdbc.geometry.Geometry`) > that require the `DriverWrapperLW` driver. --- ## Keeping the container running (local development) By default, `ebean-test` stops the Docker container when tests finish. To keep it running between test runs (much faster for local development), create a marker file: ```bash mkdir -p ~/.ebean && touch ~/.ebean/ignore-docker-shutdown ``` On CI servers, omit this file so containers are cleaned up after each build. --- ## Next Steps - **Add `TestEntityBuilder`** to your test configuration for rapid test data creation with auto-populated random values. See `testing-with-testentitybuilder.md`. - **Proceed to Step 3** — production database configuration (`add-ebean-postgres-database-config.md`). Verify this step passes with `mvn verify` before continuing.