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

  1. Inisialisasi Container: Menggunakan API Docker untuk membuat dan menjalankan container dari image tertentu (misalnya, postgres:17-alpine untuk PostgreSQL).

  2. Konfigurasi: Set env vars (seperti username/password untuk DB), expose ports, atau tunggu hingga container siap (misalnya, tunggu hingga DB menerima koneksi).

  3. Interaksi dalam Tes: Aplikasi tes connect ke container (misalnya, via connection string yang didapat dari container).

  4. Cleanup: Setelah tes selesai, container dihentikan dan dihapus otomatis untuk menghindari resource leak.

  5. 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 seperti github.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 atau database/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

  1. Instal dependensi: go mod tidy.

  2. Jalankan tes: go test -v ./... (atau gunakan GoConvey web UI: goconvey jika diinstal).

  3. Untuk app: Set env export POSTGRES_DSN="postgres://testuser:testpass@localhost:5432/testdb?sslmode=disable" (ganti port jika perlu), lalu go 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