Mert Tosun
← Yazılar
Go ile Integration Test: Mock’lama, Veritabanı, Kuyruk ve Dış Çağrılar — İleri Seviye Rehber

Go ile Integration Test: Mock’lama, Veritabanı, Kuyruk ve Dış Çağrılar — İleri Seviye Rehber

Mert TosunGo

Integration test, birim test ile uçtan uca test arasında köprüdür: gerçek süreçleri (veritabanı protokolü, SQL sürücüsü, serileştirme) doğrular; dış dünyanın kontrolsüz kısımlarını ise mock, stub veya test sunucularıyla deterministik hale getirirsiniz.

Bu yazıda Go ekosisteminde veritabanı mock’u, mesaj kuyruğu sahtesi, HTTP dış çağrı mock’u ve Testcontainers ile “gerçek” bağımlılıkları bir arada kullanma stratejilerini ileri seviye örneklerle inceliyoruz.


1. Test piramidi ve integration testin yeri

                         /\
                        /  \        E2E (az, flaky riski yüksek)
                       /____\
                      /      \      Integration (DB, kuyruk, gerçek protokol)
                     /________\
                    /          \    Unit (saf mantık, milisaniye)
                   /____________\

Integration test tipik olarak:

  • Gerçek PostgreSQL veya Redis (Testcontainers / yerel Docker)
  • HTTP üzerinden mock ödeme API’si (httptest.Server)
  • Mesaj kuyruğu için ya gerçek RabbitMQ container’ı ya da uygulama içi in-memory implementasyon

Birim testte ise sqlmock ile sürücü seviyesinde beklenen SQL’i doğrularsınız; bu “integration” değil, izole veritabanı etkileşimi testi olarak düşünülmelidir.


2. Ne mock’lanır, ne gerçek çalıştırılır?

+-------------------+---------------------------+------------------------+
| Bağımlılık        | Öneri                     | Gerekçe                |
+-------------------+---------------------------+------------------------+
| Kendi domain      | Unit + fake yok           | Saf fonksiyon / struct |
| SQL sürücüsü      | sqlmock VEYA gerçek DB    | Maliyet / hız tradeoff |
| Postgres davranışı| Testcontainers Postgres   | Dialect, index, tx     |
| Dış REST API      | httptest veya gock        | Maliyet, rate limit    |
| Kuyruk protokolü  | Gerçek broker VEYA arayüz| AMQP detayları         |
| Saat / UUID       | Enjekte edilen Clock      | Deterministik          |
+-------------------+---------------------------+------------------------+

3. Mimari: portlar ve test edilebilir sınırlar

Üretim kodunu test etmek için arayüzler (ports) tanımlayın; altyapı (adapters) testte değiştirilebilir olsun.

                    +------------------+
                    |   HTTP Handler  |
                    +--------+---------+
                             |
                    +--------v---------+
                    |  OrderService    |
                    +--+-----------+---+
                       |           |
           +-----------+           +------------+
           |                                      |
   +-------v--------+                    +--------v--------+
   | OrderRepository |                    |  EventPublisher |
   |   (interface)   |                    |   (interface)   |
   +--------+--------+                    +--------+--------+
            |                                      |
   +--------v--------+                    +--------v--------+
   | postgres.Repo   |                    | amqp.Publisher  |
   |   (prod)        |                    |   (prod)        |
   +-----------------+                    +-----------------+

   Test ortamında:
   * Repo  -> sqlmock veya tc.Postgres + gerçek SQL
   * Events -> inMemoryPublisher veya tc.RabbitMQ

Bu çizim hexagonal / ports & adapters düşüncesinin özeti: integration test, adapter katmanında gerçek protokolle konuşur; domain yine hızlı birim testle korunur.


4. Veritabanı: sqlmock ile sürücü seviyesi

github.com/DATA-DOG/go-sqlmock beklenen sorguları ve dönüş satırlarını tanımlar. Migration veya karmaşık SQL davranışını yakalamaz; sürücünün çağırdığını doğrular.

package repo

import (
    "context"
    "database/sql"
    "regexp"
    "testing"

    "github.com/DATA-DOG/go-sqlmock"
)

func TestGetUserByID(t *testing.T) {
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    rows := sqlmock.NewRows([]string{"id", "email"}).
        AddRow(1, "[email protected]")

    mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, email FROM users WHERE id = $1`)).
        WithArgs(1).
        WillReturnRows(rows)

    r := NewUserRepo(db)
    u, err := r.GetByID(context.Background(), 1)
    if err != nil {
        t.Fatal(err)
    }
    if u.Email != "[email protected]" {
        t.Fatalf("email: got %q", u.Email)
    }
    if err := mock.ExpectationsWereMet(); err != nil {
        t.Fatal(err)
    }
}

Ne zaman yeterli? Repository katmanında SQL’in doğru üretildiğini kilitlemek için. Ne zaman yetmez? JSON operatörleri, özel index, transaction isolation — bunlar için gerçek Postgres gerekir.


5. Veritabanı: Testcontainers ile gerçek PostgreSQL

  [go test] ---> [Testcontainers] ---> [Docker]
                         |
                         v
                  +--------------+
                  | postgres:16  |
                  | :5432        |
                  +--------------+

Örnek iskelet (kütüphane sürümünü modülünüze göre güncelleyin):

package orders_test

import (
    "context"
    "database/sql"
    "testing"

    _ "github.com/lib/pq"
    tc "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
)

func setupPostgres(t *testing.T) (dsn string, terminate func()) {
    t.Helper()
    ctx := context.Background()
    req := tc.ContainerRequest{
        Image:        "postgres:16-alpine",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_USER":     "test",
            "POSTGRES_PASSWORD": "test",
            "POSTGRES_DB":       "testdb",
        },
        WaitingFor: wait.ForListeningPort("5432/tcp"),
    }
    c, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    if err != nil {
        t.Fatal(err)
    }
    host, _ := c.Host(ctx)
    port, _ := c.MappedPort(ctx, "5432")
    dsn = "postgres://test:test@" + host + ":" + port.Port() + "/testdb?sslmode=disable"
    return dsn, func() { _ = c.Terminate(ctx) }
}

Ardından migration çalıştırıp repository integration testlerinizi gerçek bağlantıyla koşturursunuz. CI’da Docker socket gerekir.


6. Mesaj kuyruğu: arayüz + in-memory sahte

Üretimde AMQP kullanıyorsanız, publish işlemini arayüz arkasına alın:

type EventPublisher interface {
    Publish(ctx context.Context, routingKey string, body []byte) error
}

type InMemoryBroker struct {
    Messages []Published
}

type Published struct {
    Key  string
    Body []byte
}

func (b *InMemoryBroker) Publish(ctx context.Context, routingKey string, body []byte) error {
    b.Messages = append(b.Messages, Published{Key: routingKey, Body: append([]byte(nil), body...)})
    return nil
}

Integration test senaryosu: OrderService siparişi kaydettikten sonra InMemoryBroker içinde bir mesaj oluştu mu diye assert edilir — gerçek RabbitMQ olmadan uçtan uca servis mantığı doğrulanır.


7. Mesaj kuyruğu: RabbitMQ ile Testcontainers

Protokol detayı (ACK/NACK, header, TTL) kritikse gerçek broker container’ı tercih edilir.

   [Service] --publish--> [RabbitMQ container]
                               |
                               +-- queue "orders.created"

Test akışı:

  1. Container’ı başlat, yönetim API veya AMQP ile kuyruk + exchange tanımla
  2. Uygulama kodunu gerçek DSN/host ile çalıştır
  3. Mesajı consume eden test tarafında assert veya testcontainers ile ikinci bir tüketici stub’ı

Bu testler yavaş ve paralelde dikkat ister (port çakışması); t.Parallel() ile birlikte benzersiz vhost veya rastgele kuyruk adı kullanın.


8. Dış HTTP çağrıları: httptest.Server

Standart kütüphane ile kontrol edilen bir HTTP sunucusu — en temiz mock’tur.

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/v1/charge" {
        http.NotFound(w, r)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    _, _ = w.Write([]byte(`{"id":"ch_123","status":"succeeded"}`))
}))
defer srv.Close()

client := payments.NewClient(srv.URL) // base URL enjekte edilir

İleri seviye: Yanıtı senaryoya göre değiştirmek için closure içinde sayaç veya kanal kullanın; hata kodlarını (429, 500) sırayla simüle edin.


9. Dış HTTP: go-resty + gock ile eşleşme

HTTP client’ı gock ile intercept etmek, URL ve method eşleşmesini deklaratif yazar:

import "github.com/h2non/gock"

defer gock.Off()
gock.New("https://api.partner.com").
    Get("/users/1").
    Reply(200).
    JSON(map[string]any{"name": "Ada"})

// client BaseURL = https://api.partner.com olmalı

Dikkat: gock global transport’u patch’ler; paralel testlerde paket bazlı seri çalıştırma veya gock.DisableNetworking() ile sıkılaştırma gerekebilir.


10. Uçtan uca örnek: sipariş + ödeme + event

  Test()
    |
    +-- httptest: ödeme API 200
    |
    +-- sqlmock VEYA tc.Postgres: sipariş satırı insert
    |
    +-- InMemoryBroker: "order.paid" mesajı var mı?

Servis katmanı tek bir func TestOrderFlow_Integration içinde bu üçünü birlikte doğrulayabilir; dosya adına _test.go ve build tag //go:build integration ile yavaş testleri ayırabilirsiniz:

//go:build integration

package orders_test

Çalıştırma:

go test -tags=integration ./...

11. Paralellik ve temizlik

  • t.Cleanup ile container terminate ve db.Close kayıt edin
  • Global gock kullanıyorsanız test başına gock.Off() + gock.Clean()
  • Testcontainers için reuse modları (sürüme bağlı) CI süresini kısaltabilir

12. Anti-pattern’ler

Kötü pratik Sonuç
Her şeyi mock’lamak Gerçek SQL/AMQP hatalarını prod’da görürsünüz
Hiç mock kullanmamak Yavaş test, flaky dış API
init() içinde global DB Test sırası birbirini kirletir
Integration testte iş kurallarını tekrar etmemek Unit test eksik kalır

Özet

  • sqlmock: SQL çağrı sözleşmesini hızlı doğrular; şema davranışını değil.
  • Testcontainers: Postgres, Redis, Kafka, RabbitMQ için protokol düzeyinde güven.
  • httptest / gock: Dış HTTP’yi deterministik yapar.
  • Arayüz + in-memory broker: Kuyruk mantığını RabbitMQ olmadan entegre test eder.

Go’da integration test, doğru sınırı çizmek ve doğru aracı seçmek sanatıdır: aynı projede hem hızlı sqlmock testleri hem ağır Testcontainers suite’i barındırmak yaygın ve sağlıklı bir düzendir.

Test piramidinde integration katmanını geniş tutun; ama E2E’yi sadece kritik kullanıcı yolları için saklayın.