Tampilan
Kenapa penting
Repository, query JPQL, native query, constraint unik, dan migrasi Flyway semuanya baru benar-benar teruji ketika dijalankan di database yang sama dengan production. Banyak tim pernah merasakan test hijau di laptop, lalu deployment gagal karena query native memakai fungsi PostgreSQL yang tidak dikenali database test. Testcontainers menyelesaikan masalah itu dengan cara sederhana: setiap kali test berjalan, ia menyalakan PostgreSQL asli di container Docker, memberi tahu Spring cara terhubung, lalu membuangnya setelah selesai. Hasilnya test integrasi yang jujur, bisa dijalankan siapa saja dengan satu perintah, dan sama persis di laptop maupun di CI. Di bab ini kamu menyiapkan Testcontainers 2.0 di proyek Spring Boot 4 dan menulis test repository serta test migrasi.
Konsep inti
Kenapa bukan H2
H2 in-memory memang cepat dan tanpa instalasi, tetapi ia bukan PostgreSQL. Mode kompatibilitasnya hanya meniru sebagian sintaks. Perbedaan yang sering menggigit:
| Area | PostgreSQL | H2 |
|---|---|---|
| Tipe khusus | jsonb, uuid, array, citext | sebagian tidak ada atau berperilaku lain |
| Index parsial / ekspresi | didukung | terbatas |
ON CONFLICT ... DO UPDATE | didukung | sintaks berbeda |
| Perilaku locking & isolasi | MVCC PostgreSQL | model berbeda |
| Collation & perbandingan teks | ikut konfigurasi database | aturan sendiri |
Akibatnya kamu terpaksa menulis migrasi dua versi atau menghindari fitur PostgreSQL — dua-duanya buruk. Dengan Testcontainers, migrasi Flyway yang sama dijalankan di PostgreSQL yang sama versinya dengan production.
Prasyarat: Docker
Testcontainers adalah library Java yang mengendalikan Docker lewat API-nya. Jadi mesin yang menjalankan test butuh Docker daemon atau runtime yang kompatibel (Docker Desktop, Docker Engine di Linux, Podman dengan socket kompatibel, atau layanan Docker jarak jauh). Cek dulu:
bash
docker infoBila perintah itu gagal, test berbasis Testcontainers akan gagal dengan pesan bahwa lingkungan Docker tidak ditemukan. Ingat: image pertama kali harus diunduh, jadi run pertama terasa lambat; run berikutnya memakai image yang sudah tersimpan.
Dependency Testcontainers 2.0
Testcontainers 2.0 membawa perubahan yang membuat banyak contoh lama di internet tidak lagi bisa dikompilasi:
- Artefak modul memakai awalan
testcontainers-:postgresqlmenjaditestcontainers-postgresql,junit-jupitermenjaditestcontainers-junit-jupiter. - Class container pindah ke paket per modul:
org.testcontainers.postgresql.PostgreSQLContainer(duluorg.testcontainers.containers.PostgreSQLContainer), dan tidak lagi generik. - Dukungan JUnit 4 dihapus; integrasinya fokus ke JUnit Jupiter.
Spring Boot mengelola versi Testcontainers di BOM-nya, jadi kamu tidak perlu menulis nomor versi:
xml
<!-- Potongan pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-postgresql</artifactId>
<scope>test</scope>
</dependency>spring-boot-testcontainers menyediakan @ServiceConnection, sedangkan spring-boot-data-jpa-test (modul test di Spring Boot 4 yang lebih modular) menyediakan @DataJpaTest. Starter spring-boot-starter-test tetap kamu pakai untuk JUnit, AssertJ, dan Mockito.
@Testcontainers, @Container, dan @ServiceConnection
Tiga anotasi ini bekerja sama:
@Testcontainers(dariorg.testcontainers.junit.jupiter) mengaktifkan ekstensi JUnit yang mengelola siklus hidup container.@Containermenandai field container. Fieldstaticdinyalakan sekali per class test; field non-static dinyalakan ulang untuk setiap method.@ServiceConnection(dariorg.springframework.boot.testcontainers.service.connection) membuat Spring Boot membaca host, port acak, username, dan password langsung dari container, lalu membuatJdbcConnectionDetails. Kamu tidak perlu menulisspring.datasource.urlsama sekali.
java
// Potongan
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class PurchaseOrderRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:18-alpine");
@Autowired
PurchaseOrderRepository orders;
@Test
void savesAndFindsByStatus() {
orders.save(PurchaseOrder.create("INV-1001", "budi@example.com", new BigDecimal("150000")));
assertThat(orders.findByStatus(OrderStatus.PENDING, PageRequest.of(0, 10)))
.extracting(PurchaseOrder::getOrderNumber)
.containsExactly("INV-1001");
}
}TIP
Kunci tag image ke versi mayor yang sama dengan production (postgres:18-alpine), jangan postgres:latest. Tag latest bisa melompat versi mayor dan membuat test tiba-tiba berperilaku lain.
Sebelum @ServiceConnection ada, orang memakai @DynamicPropertySource untuk menyuntikkan URL JDBC dari container. Cara itu masih valid untuk properti yang tidak tercakup service connection, tetapi untuk PostgreSQL, @ServiceConnection lebih ringkas.
@DataJpaTest vs @SpringBootTest
| Aspek | @DataJpaTest | @SpringBootTest |
|---|---|---|
| Bean yang dimuat | JPA, repository, DataSource, Flyway | seluruh aplikasi (controller, service, security, ...) |
| Transaksi | setiap test di-rollback otomatis | tidak ada rollback otomatis |
| Kecepatan | cepat | lebih lambat |
| Cocok untuk | query repository, mapping entity, constraint | alur end-to-end: HTTP → service → database |
| Penggantian DB | default mencoba memakai DB embedded → set Replace.NONE | memakai DataSource dari konfigurasi/service connection |
Gunakan @DataJpaTest untuk sebagian besar test data, dan @SpringBootTest untuk sedikit test alur penting. Karena @DataJpaTest me-rollback tiap test, ingat bahwa perilaku saat commit (misalnya constraint yang ditunda) tidak selalu terlihat; panggil flush() dari TestEntityManager atau repository bila ingin constraint dicek saat itu juga.
Test migrasi Flyway
Bila Flyway ada di classpath, ia menjalankan semua migrasi ke container saat konteks dibuat. Gabungkan dengan spring.jpa.hibernate.ddl-auto=validate agar Hibernate membandingkan mapping entity dengan skema hasil migrasi. Satu test kecil sudah cukup untuk menangkap migrasi yang rusak atau entity yang tidak sinkron:
java
// Potongan
@Testcontainers
@DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=validate")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class MigrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:18-alpine");
@Autowired
Flyway flyway;
@Test
void allMigrationsAppliedAndSchemaMatchesEntities() {
// Konteks berhasil dibuat = migrasi sukses dan validasi Hibernate lolos
assertThat(flyway.info().pending()).isEmpty();
}
}Reuse container dan kecepatan
Menyalakan PostgreSQL di container butuh beberapa detik. Agar suite tetap cepat:
- Container
static— satu container per class, bukan per method. - Konfigurasi konteks yang seragam — Spring meng-cache
ApplicationContextdan memakainya ulang bila konfigurasinya identik. Setiap@MockitoBeanatau properti yang berbeda menghasilkan konteks baru. - Container bersama — deklarasikan container di satu tempat (misalnya interface atau class konfigurasi
@TestConfigurationdengan bean ber-@ServiceConnection) lalu@Importdi semua test, sehingga satu container dipakai seluruh suite. - Reuse lintas run (opsional, lokal) —
.withReuse(true)pada container ditambahtestcontainers.reuse.enable=truedi~/.testcontainers.propertiesmembuat container tetap hidup setelah test selesai. Ini mempercepat iterasi di laptop, tetapi data dari run sebelumnya bisa tertinggal, jadi jangan mengandalkannya di CI.
Testcontainers di CI
Runner Linux GitHub Actions (ubuntu-latest) sudah menyediakan Docker, jadi test cukup dijalankan seperti biasa:
yaml
# Potongan .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "25"
cache: maven
- run: ./mvnw -B verifyBila CI menjalankan job di dalam container, job itu butuh akses ke Docker daemon (socket atau layanan Docker terpisah). Untuk menghindari batas unduh Docker Hub, banyak tim memakai mirror registry internal.
Contoh di dunia backend
Test berikut memastikan query paginasi dari bab sebelumnya benar-benar memakai urutan deterministik dan constraint unik nomor pesanan bekerja di PostgreSQL.
java
// Potongan
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderHistoryRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:18-alpine");
@Autowired
PurchaseOrderRepository orders;
@Test
void historyIsStableWhenTotalsAreEqual() {
for (int i = 1; i <= 5; i++) {
orders.save(PurchaseOrder.create("INV-" + i, "sari@example.com", new BigDecimal("50000")));
}
Sort stable = Sort.by("totalAmount").and(Sort.by(Sort.Direction.DESC, "id"));
Page<OrderSummary> first = orders.findSummariesByCustomerEmail("sari@example.com", PageRequest.of(0, 3, stable));
Page<OrderSummary> second = orders.findSummariesByCustomerEmail("sari@example.com", PageRequest.of(1, 3, stable));
assertThat(first.getTotalElements()).isEqualTo(5);
assertThat(first.getContent()).extracting(OrderSummary::orderNumber)
.containsExactly("INV-5", "INV-4", "INV-3");
assertThat(second.getContent()).extracting(OrderSummary::orderNumber)
.containsExactly("INV-2", "INV-1");
}
@Test
void rejectsDuplicateOrderNumber() {
orders.saveAndFlush(PurchaseOrder.create("INV-9", "a@example.com", new BigDecimal("10000")));
assertThatThrownBy(() ->
orders.saveAndFlush(PurchaseOrder.create("INV-9", "b@example.com", new BigDecimal("20000"))))
.isInstanceOf(DataIntegrityViolationException.class);
}
}Perhatikan saveAndFlush: tanpa flush, INSERT bisa tertahan di persistence context sampai rollback dan constraint unik tidak pernah diuji.
Pertanyaan interview
1. Kenapa kamu memilih Testcontainers dibanding H2 untuk test repository?
Karena test harus membuktikan perilaku di database production. H2 hanya meniru sebagian sintaks PostgreSQL, sehingga migrasi atau query yang memakai fitur spesifik (jsonb, index parsial, ON CONFLICT) bisa gagal atau berperilaku lain. Testcontainers menjalankan PostgreSQL versi yang sama dengan production, dengan migrasi Flyway yang sama, sehingga test yang hijau benar-benar bermakna. Harganya: butuh Docker dan start-up beberapa detik.
2. Apa fungsi @ServiceConnection?
Anotasi Spring Boot yang membaca detail koneksi dari container (host, port yang dipetakan acak, kredensial) dan membuat bean connection details, misalnya JdbcConnectionDetails untuk PostgreSQL. Auto-configuration DataSource lalu memakainya, sehingga kamu tidak perlu @DynamicPropertySource atau menulis URL JDBC manual.
3. Bagaimana menjaga suite test integrasi tetap cepat?
Pakai container static atau container bersama untuk seluruh suite, jaga konfigurasi konteks Spring seragam agar konteks di-cache, utamakan @DataJpaTest dibanding @SpringBootTest, dan pakai tag image tetap agar image tersimpan di cache. Reuse container lintas run boleh untuk lokal, bukan untuk CI.
Jebakan umum
- Menyalin contoh lama:
org.testcontainers:postgresqldanorg.testcontainers.containers.PostgreSQLContainersudah diganti di Testcontainers 2.0. - Lupa
@AutoConfigureTestDatabase(replace = Replace.NONE)saat ada database embedded di classpath, sehingga@DataJpaTestdiam-diam tidak memakai container. - Memakai tag
postgres:latest— versi mayor bisa berubah tanpa kamu sadari. - Container non-static atau konfigurasi konteks yang berbeda-beda di tiap class, membuat suite makin lambat seiring bertambahnya test.
- Menguji constraint tanpa
flush(), padahal@DataJpaTestme-rollback transaksi sebelumINSERTsempat dikirim. - Mengaktifkan reuse container di CI, sehingga data dari run sebelumnya membuat test tidak deterministik.
Ringkasan
- Test repository dan migrasi sebaiknya berjalan di PostgreSQL asli; H2 menyembunyikan perbedaan dialek.
- Testcontainers 2.0 memakai artefak
testcontainers-*dan paket per modul sepertiorg.testcontainers.postgresql. @Testcontainers+@Containermengelola siklus hidup;@ServiceConnectionmenghubungkan Spring tanpa URL manual.@DataJpaTest(denganReplace.NONE) untuk test data yang cepat dan ter-rollback;@SpringBootTestuntuk alur penuh.- Satu test dengan
ddl-auto=validatemenangkap migrasi Flyway yang rusak atau entity yang tidak sinkron. - Docker wajib ada di mesin lokal dan CI; container
static/bersama dan cache konteks menjaga suite tetap cepat.