In @JdbcTest, how do the embedded database replacement and per-test rollback work, and how do you override them?
answer
- @AutoConfigureTestDatabase replace = ANY default
- Replace.NONE + Testcontainers = real DB
- @Transactional -> rollback per test
- @Commit / @Rollback(false) to keep data
- schema.sql/data.sql or @Sql for setup
basics
~20 s@JdbcTest swaps your DataSource for an in-memory DB (via @AutoConfigureTestDatabase, default Replace.ANY) and runs each test in a transaction that rolls back. Use Replace.NONE to keep your real datasource; use @Commit or @Rollback(false) to keep committed data.
solid answer
~40 s@JdbcTest is meta-annotated with @AutoConfigureTestDatabase, whose default replace = Replace.ANY substitutes any configured DataSource with an auto-configured in-memory embedded DB (H2/HSQL/Derby from the classpath). It is also meta-annotated with @Transactional, so Spring's TestContext framework starts a transaction before each test method and rolls it back afterward — giving isolation for free. To point at a real database (e.g. a Testcontainers Postgres so tests match production SQL), set @AutoConfigureTestDatabase(replace = Replace.NONE). To keep changes committed instead of rolled back, add @Commit or @Rollback(false) at method or class level. Schema and seed data come from schema.sql/data.sql on the classpath, @Sql scripts, or migration tools. This combination is what makes the slice both isolated and fast, at the cost of DB fidelity when you use H2 against a Postgres production.
code
java · 26 lines@JdbcTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // keep the real DataSource
@Testcontainers
@Import(OrderRepository.class)
class OrderRepositoryPgTest {
@Container
static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16");
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", pg::getJdbcUrl);
r.add("spring.datasource.username", pg::getUsername);
r.add("spring.datasource.password", pg::getPassword);
}
@Autowired OrderRepository repository;
@Test
@Sql("/schema-orders.sql")
void persistsOrder() {
repository.save(new Order("A-1"));
assertThat(repository.findByCode("A-1")).isPresent();
// rolled back after the test unless @Commit is present
}
}go deeper
Know embedded DB + rollback happen automatically.
Explain Replace.ANY/NONE/AUTO_CONFIGURED and @Commit/@Rollback plus schema loading.
Argue H2-vs-real-DB tradeoffs, wire Testcontainers with @DynamicPropertySource, and reason about rollback hiding commit-time behavior.
Set org policy for persistence-test fidelity, standardize on Testcontainers where dialect matters, and account for CI cost/speed of real-DB slices.
## Two mechanisms, two annotations `@JdbcTest` composes two behaviors you can tune independently. ### 1) DataSource replacement — `@AutoConfigureTestDatabase` `@JdbcTest` is meta-annotated with `@AutoConfigureTestDatabase`. Its `replace` attribute controls what happens to your `DataSource`: - **`Replace.ANY`** (the default for `@JdbcTest`): replace **any** DataSource — auto-configured or explicitly defined — with an auto-configured **in-memory embedded database**. Spring Boot picks H2, HSQLDB, or Derby from the classpath; if none is present, context startup fails. - **`Replace.NONE`**: do not replace anything; use the `DataSource` your configuration provides. This is how you test against a **real** engine, e.g. a **Testcontainers** PostgreSQL, so dialect-specific SQL is validated. - **`Replace.AUTO_CONFIGURED`**: replace only a DataSource that Spring Boot itself auto-configured (not one you defined explicitly). Why this matters: H2 is convenient but its SQL/dialect differs from Postgres/MySQL, so a green H2 test can still fail in prod. Teams often standardize on `Replace.NONE` + Testcontainers for persistence tests. ### 2) Transaction + rollback — `@Transactional` (TestContext) `@JdbcTest` is also meta-annotated with `@Transactional`. Spring's **TestContext framework** treats a transactional test specially: it **opens a transaction before each `@Test` and rolls it back after**, regardless of pass/fail. Benefits: perfect isolation, no cleanup code, fast. Overrides: - **`@Commit`** or **`@Rollback(false)`** — commit instead of rolling back (use sparingly; breaks isolation). - **`@BeforeTransaction` / `@AfterTransaction`** — hooks that run outside the managed transaction. - Note the classic **rollback pitfall**: because everything is one connection/transaction, some DB behaviors (auto-generated IDs sequences, deferred constraints, isolation-level effects) may not surface the same way they would with real commits. ## Loading schema and data The embedded DB starts empty. Provide structure/data via: - `schema.sql` and `data.sql` on the classpath (auto-run for embedded DBs), - **`@Sql("/scripts/init.sql")`** on the class or method, - Flyway/Liquibase when using `Replace.NONE` against a real DB. ## Putting it together ``` @JdbcTest @AutoConfigureTestDatabase(replace = Replace.NONE) // real DB @Testcontainers // e.g. Postgres ``` gives production-fidelity SQL with slice speed. Drop the two extra annotations and you get the default fast in-memory + rollback flavor. ## Gotchas recap - No embedded engine on classpath under `Replace.ANY` → startup failure. - Expecting committed data across methods → it is rolled back; use `@Commit` only if truly needed. - H2-vs-prod SQL drift → prefer Testcontainers for anything dialect-sensitive. - `@Sql` scripts vs `schema.sql`: both work, but know precedence and phase (`executionPhase`).
- Why might a @JdbcTest pass on H2 but the same SQL fail in production Postgres?H2 emulates a generic SQL dialect; Postgres-specific syntax, functions, type coercions, sequence/identity behavior, and constraint semantics can differ. Testing against a real engine via Replace.NONE + Testcontainers avoids this dialect drift.
- You need one test's inserted rows to survive for a manual inspection or a follow-on assertion after commit. How?Add @Commit (or @Rollback(false)) so the transaction commits instead of rolling back. Use it sparingly and clean up explicitly, because it breaks the automatic isolation the slice normally gives you.
saying these in an interview costs you the question
- Thinking Replace.NONE is the default (it is Replace.ANY).
- Believing rollback is optional/off by default (it is on via @Transactional).
- Assuming H2 behaves identically to production Postgres/MySQL.
- Not knowing you can keep committed data with @Commit/@Rollback(false).