Testcontainers
Testcontainers adalah sebuah framework open-source yang dirancang untuk memfasilitasi pengujian (testing) aplikasi yang bergantung pada layanan eksternal seperti database, message queue, atau API server, dengan cara menjalankan container Docker secara programmatic di dalam proses testing. Framework ini pertama kali dikembangkan untuk Java, tetapi sekarang tersedia untuk berbagai bahasa pemrograman, termasuk Go (melalui library testcontainers-go
). Ide utamanya adalah menghindari ketergantungan pada mock object atau setup manual untuk dependensi eksternal, sehingga tes menjadi lebih realistis dan mendekati lingkungan produksi.
Sejarah dan Latar Belakang
Asal-usul: Testcontainers dimulai sebagai proyek Java pada tahun 2015 oleh Richard North. Sejak itu, ia berkembang menjadi ekosistem multi-bahasa, termasuk dukungan resmi untuk Go, Python, .NET, Node.js, dan lainnya.
Motivasi: Dalam pengembangan software, integration testing seringkali sulit karena memerlukan akses ke layanan nyata (misalnya, database PostgreSQL). Testcontainers menyelesaikan ini dengan secara otomatis memulai container Docker yang berisi layanan tersebut saat tes dijalankan, dan membersihkannya setelah selesai. Ini membuat tes idempoten (dapat diulang tanpa efek samping) dan portabel di berbagai mesin developer atau CI/CD pipeline.
Keuntungan Utama:
Realisme: Menggunakan instance nyata dari layanan (bukan mock), sehingga mendeteksi bug yang mungkin terlewat di mock.
Isolasi: Setiap tes mendapatkan container baru, menghindari konflik state antar-tes.
Portabilitas: Hanya memerlukan Docker yang terinstal; tidak perlu instalasi manual layanan.
Efisiensi: Container dimulai hanya saat dibutuhkan dan dihentikan otomatis.
Fleksibilitas: Dukung berbagai image Docker, termasuk custom image, dan konfigurasi seperti env vars, ports, volumes.
Kekurangan:
Memerlukan Docker runtime (seperti Docker Desktop atau Docker Engine), yang bisa menambah overhead di lingkungan non-Docker.
Tes bisa lebih lambat karena startup container (meskipun bisa dioptimasi dengan reuse atau caching).
Tidak cocok untuk unit test murni; lebih untuk integration/end-to-end test.
Cara Kerja Testcontainers
Inisialisasi Container: Menggunakan API Docker untuk membuat dan menjalankan container dari image tertentu (misalnya,
postgres:17-alpine
untuk PostgreSQL).Konfigurasi: Set env vars (seperti username/password untuk DB), expose ports, atau tunggu hingga container siap (misalnya, tunggu hingga DB menerima koneksi).
Interaksi dalam Tes: Aplikasi tes connect ke container (misalnya, via connection string yang didapat dari container).
Cleanup: Setelah tes selesai, container dihentikan dan dihapus otomatis untuk menghindari resource leak.
Fitur Tambahan:
Waiting Strategies: Tunggu hingga container ready (e.g., log matching, port probing, atau custom checks).
Network: Buat jaringan custom untuk multiple containers (e.g., app + DB + cache).
Volumes: Mount file dari host ke container.
Reuse: Opsional reuse container antar-tes untuk speed up (tapi kurangi isolasi).
Modules: Pre-built modules untuk layanan populer seperti PostgreSQL, MySQL, Kafka, dll., yang menyederhanakan setup.
Testcontainers di Go
Library resmi:
github.com/testcontainers/testcontainers-go
.Instalasi:
go get github.com/testcontainers/testcontainers-go
dan module spesifik sepertigithub.com/testcontainers/testcontainers-go/modules/postgres
.Kompatibilitas: Bekerja dengan Go versi terbaru (termasuk Go 1.25, asumsi kompatibel karena Go backward-compatible). Memerlukan Docker API version yang sesuai.
Penggunaan Umum: Diintegrasikan dengan framework testing Go seperti
testing
bawaan, atau BDD-style seperti GoConvey (github.com/smartystreets/goconvey/convey
).
Contoh Penggunaan untuk Integration Test
Testcontainers sering digunakan untuk integration test, di mana kita menguji interaksi aplikasi dengan database nyata. Misalnya:
Start container Postgres.
Connect menggunakan driver seperti
pgx
ataudatabase/sql
.Jalankan query (create table, insert, select).
Verifikasi hasil.
Gunakan GoConvey untuk struktur tes yang lebih readable (BDD-style: Given-When-Then).
Sekarang, saya akan berikan kode lengkap untuk contoh ini menggunakan Go 1.25 (asumsi environment Go terbaru), PostgreSQL 17, Testcontainers, dan GoConvey. Kode ini adalah sebuah package sederhana dengan file main (untuk demo app) dan file test. Asumsikan Anda punya Docker terinstal.
Kode Lengkap: Integration Test dengan Testcontainers, Go, Postgres, dan GoConvey
Buat struktur direktori:
myapp/
├── go.mod
├── main.go
├── db.go
└── db_test.go
File: go.mod
module myapp
go 1.25
require (
github.com/jackc/pgx/v5 v5.7.1 // Driver PostgreSQL
github.com/smartystreets/goconvey v1.8.1 // GoConvey untuk BDD testing
github.com/testcontainers/testcontainers-go v0.33.0 // Testcontainers core
github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0 // Module Postgres
)
require (
// Dependensi otomatis dari go mod tidy
)
Jalankan go mod tidy
untuk resolve dependensi.
File: main.go
(Contoh App Sederhana)
package main
import (
"context"
"database/sql"
"fmt"
"log"
"os"
_ "github.com/jackc/pgx/v5/stdlib" // Register pgx dengan database/sql
)
func main() {
// Contoh penggunaan: Connect ke DB dari env
dsn := os.Getenv("POSTGRES_DSN")
if dsn == "" {
log.Fatal("POSTGRES_DSN not set")
}
db, err := sql.Open("pgx", dsn)
if err != nil {
log.Fatalf("Failed to connect to DB: %v", err)
}
defer db.Close()
// Buat table jika belum ada
_, err = db.ExecContext(context.Background(), `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
)
`)
if err != nil {
log.Fatalf("Failed to create table: %v", err)
}
// Insert data
_, err = db.ExecContext(context.Background(), "INSERT INTO users (name) VALUES ($1)", "John Doe")
if err != nil {
log.Fatalf("Failed to insert: %v", err)
}
// Query data
var name string
err = db.QueryRowContext(context.Background(), "SELECT name FROM users WHERE id = 1").Scan(&name)
if err != nil {
log.Fatalf("Failed to query: %v", err)
}
fmt.Printf("User found: %s\n", name)
}
File: db.go
(Fungsi DB Helper untuk Diuji)
package myapp
import (
"context"
"database/sql"
"fmt"
)
// InitDB initializes the database connection and creates table.
func InitDB(ctx context.Context, dsn string) (*sql.DB, error) {
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, err
}
_, err = db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
)
`)
if err != nil {
return nil, err
}
return db, nil
}
// InsertUser inserts a user and returns the ID.
func InsertUser(ctx context.Context, db *sql.DB, name string) (int64, error) {
var id int64
err := db.QueryRowContext(ctx, "INSERT INTO users (name) VALUES ($1) RETURNING id", name).Scan(&id)
if err != nil {
return 0, err
}
return id, nil
}
// GetUserByID gets a user by ID.
func GetUserByID(ctx context.Context, db *sql.DB, id int64) (string, error) {
var name string
err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id).Scan(&name)
if err != nil {
return "", err
}
return name, nil
}
File: db_test.go
(Integration Test dengan Testcontainers dan GoConvey)
package myapp_test
import (
"context"
"fmt"
"testing"
"time"
"myapp" // Ganti dengan module name Anda
"github.com/smartystreets/goconvey/convey"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestDatabaseIntegration(t *testing.T) {
ctx := context.Background()
convey.Convey("Given a PostgreSQL container is started", t, func() {
// Start Postgres container menggunakan module postgres
pgContainer, err := postgres.Run(
ctx,
"postgres:17-alpine", // Image Postgres 17 Alpine
postgres.WithDatabase("testdb"),
postgres.WithUsername("testuser"),
postgres.WithPassword("testpass"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2). // Tunggu log muncul 2 kali
WithStartupTimeout(30*time.Second),
),
)
convey.So(err, convey.ShouldBeNil)
defer func() {
if err := pgContainer.Terminate(ctx); err != nil {
t.Fatalf("Failed to terminate container: %v", err)
}
}()
// Dapatkan connection string dari container
dsn, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
convey.So(err, convey.ShouldBeNil)
convey.Convey("When we initialize the DB and insert a user", func() {
db, err := myapp.InitDB(ctx, dsn)
convey.So(err, convey.ShouldBeNil)
defer db.Close()
id, err := myapp.InsertUser(ctx, db, "Jane Doe")
convey.So(err, convey.ShouldBeNil)
convey.So(id, convey.ShouldBeGreaterThan, 0)
convey.Convey("Then we should be able to retrieve the user", func() {
name, err := myapp.GetUserByID(ctx, db, id)
convey.So(err, convey.ShouldBeNil)
convey.So(name, convey.ShouldEqual, "Jane Doe")
})
})
})
}
Cara Menjalankan
Instal dependensi:
go mod tidy
.Jalankan tes:
go test -v ./...
(atau gunakan GoConvey web UI:goconvey
jika diinstal).Untuk app: Set env
export POSTGRES_DSN="postgres://testuser:testpass@localhost:5432/testdb?sslmode=disable"
(ganti port jika perlu), lalugo run main.go
.

Kode ini lengkap dan self-contained. Container Postgres 17-alpine akan otomatis dimulai saat tes, diinisialisasi, diuji, dan dihentikan. GoConvey memberikan struktur BDD yang jelas (Convey nested untuk Given-When-Then). Jika ada error, pastikan Docker berjalan. Untuk customisasi lebih lanjut, lihat docs resmi Testcontainers-Go.
Last updated