go-mssqldb için test desenleri

Bu makale, sürücü go-mssqldb kullanılırken SQL Server'a karşı entegrasyon testleri yazma kalıplarını ele alır.

Doğru test türünü seçin

Gerçek bir SQL Server örneğine karşı test yapmayı tercih ederim. Bir SQL Server konteyneri (Docker Compose veya bir CI servisi aracılığıylatestcontainers-go) SQL sözdizimi hatalarını, tür uyumsuzluklarını ve mock'ların tespit edemediği işlem davranışlarını yakalar. Bu yaklaşım, sürücünün go-mssqldb kendi test paketi için kullandığı yöntemdir. Windows'ta ise LocalDB, Docker gerektirmeyen hafif bir alternatiftir.

Sadece go-sqlmock hızlı iç döngü birim testleri için geri dönün, burada konteyner başlatma süresi yürütmeyi kontrol eder. Örneğin, uygulama katmanı yeniden deneme mantığı veya sonuç eşlemesini test etmek için kullanın.

Test türü Şunun için kullanın Bundan kaçının:
testcontainers-go ile entegrasyon testleri Paylaşılan altyapıyı yönetmeden gerçek bir SQL Server örneği gerektiren tekrarlanabilir CI çalışmaları ve test kümeleri. Konteyner başlatma süresinin yürütmeye hakim olduğu hızlı iç döngü testleri.
Paylaşılan veya yerel SQL Server'a karşı entegrasyon testleri Depolanan prosedürler, şema nesneleri, işlem davranışı, geçici tablolar ve uçtan uca sürücü davranışı. Testler izole altyapıya ihtiyaç duyar veya CI'de harici bağımlılıklar olmadan tutarlı şekilde çalışmalıdır.
go-sqlmock ile birim testleri SQL söz dizimini doğrulamanız gerekmediğinde, yeniden deneme döngüleri, sonuçların eşlenmesi ve hata durumlarına göre dallanma gibi uygulama katmanı mantığı Sürücü davranışını, SQL sözdizimini SQL Server'a karşı doğrulamanız veya işlem semantiği doğrulamanız gerekir.

Test veritabanı kurulumu

Entegrasyon testleri için test bağlantı dizesi'i yapılandırmak amacıyla ortam değişkenlerini kullanın. Bu yaklaşım, kimlik bilgilerini kaynak kodundan uzak tutar ve CI/CD entegrasyonunu kolaylaştırır:

package myapp_test

import (
    "database/sql"
    "os"
    "testing"

    _ "github.com/microsoft/go-mssqldb"
)

var testDB *sql.DB

func TestMain(m *testing.M) {
    connString := os.Getenv("TEST_MSSQL_URL")
    if connString == "" {
        panic("TEST_MSSQL_URL is not set")
    }

    var err error
    testDB, err = sql.Open("sqlserver", connString)
    if err != nil {
        panic("Failed to open test DB: " + err.Error())
    }
    defer testDB.Close()

    if err = testDB.Ping(); err != nil {
        panic("Failed to connect to test DB: " + err.Error())
    }

    os.Exit(m.Run())
}

Testleri çalıştırmadan önce ortam değişkenini ayarlayın:

export TEST_MSSQL_URL="sqlserver://<user>:<password>@<server>:1433?database=<database>&encrypt=true&TrustServerCertificate=true"
go test ./...

Test izolasyonu için işlemleri kullanın

Her testi bir işlem içine alın ve sonunda geri alın. Bu yaklaşım, testler arasında veritabanını temiz tutar:

func TestInsertDepartment(t *testing.T) {
    tx, err := testDB.Begin()
    if err != nil {
        t.Fatal(err)
    }
    defer tx.Rollback() // Always roll back - never commits

    _, err = tx.Exec(
        "INSERT INTO HumanResources.Department (Name, GroupName) VALUES (@p1, @p2)",
        sql.Named("p1", "TestDept"),
        sql.Named("p2", "TestGroup"))
    if err != nil {
        t.Fatal(err)
    }

    var count int
    err = tx.QueryRow("SELECT COUNT(*) FROM HumanResources.Department WHERE Name = @p1",
        sql.Named("p1", "TestDept")).Scan(&count)
    if err != nil {
        t.Fatal(err)
    }

    if count != 1 {
        t.Errorf("Expected 1 row, got %d", count)
    }
}

Bu desen, tek bir işlem sınırı içinde depo kodunu kullanan testler için en iyi şekilde çalışır. Kendi işlemlerini dahili olarak başlatıp onaylayan kodlar veya birden fazla bağlantı genelinde davranışı doğrulaması gereken testler için pek uygun değildir.

CI/CD için Docker’da SQL Server

CI boru hatlarında entegrasyon testleri için SQL Server Linux konteyneri kullanın:

# GitHub Actions example
services:
  mssql:
    image: mcr.microsoft.com/mssql/server:2025-latest
    env:
      ACCEPT_EULA: "Y"
      MSSQL_SA_PASSWORD: "<password>"
    ports:
      - 1433:1433

Sonra test bağlantı dizesi'i ayarlayın:

env:
    TEST_MSSQL_URL: "sqlserver://sa:<password>@localhost:1433?database=AdventureWorks2025"

Veritabanı yokken testleri atlayın

SQL Server örneği her zaman erişilebilir olmayabilecek projelerde, entegrasyon testlerini zarifçe atlayın:

func TestQueryEmployees(t *testing.T) {
    if os.Getenv("TEST_MSSQL_URL") == "" {
        t.Skip("TEST_MSSQL_URL not set, skipping integration test")
    }
    // ... test body
}

Test yardımcısı: tablo oluşturma ve silme

Test tablosu oluşturan ve testten sonra temizleyen bir yardımcı fonksiyon oluşturun:

func withTestTable(t *testing.T, db *sql.DB, fn func()) {
    t.Helper()

    _, err := db.Exec(`
        IF OBJECT_ID('dbo.TestItems', 'U') IS NOT NULL DROP TABLE dbo.TestItems;
        CREATE TABLE dbo.TestItems (Id INT IDENTITY PRIMARY KEY, Name NVARCHAR(50));
    `)
    if err != nil {
        t.Fatal("Setup failed:", err)
    }

    defer func() {
        db.Exec("DROP TABLE IF EXISTS dbo.TestItems")
    }()

    fn()
}

go-sqlmock ile birim testleri

Eğer konteyner başlatma süresi iç geliştirme döngünüz için çok yavaşsa, go-sqlmock önceden tanımlanmış sonuçları geri getiren bir bellek *sql.DB içi oluşturur. SQL sözdizimini gerçek bir sunucuya karşı doğrulamanıza gerek kalmadığı uygulama katmanı mantığı için kullanın (tekrar deneme döngüleri, sonuç eşleme, hata dallanma):

go get github.com/DATA-DOG/go-sqlmock

Bir sorgunun benzerini oluşturun

Beklenen sorguları kurun ve uygulamanın sonuçları doğru şekilde ele aldığından emin olun:

Uyarı

sqlmock.ExpectQuery girdisini sıradan bir SQL dizisi değil, düzenli ifade olarak ele alır. (, ), + ve . gibi karakterler, SQL metninde birebir eşleştirilebilmeleri için öncelemeli yazılmalıdır. Go dize literallerinde bu kaçışlar çift olarak görünür (örneğin, \\( regex'teki bir literal ( için).

package myapp_test

import (
    "testing"
    "github.com/DATA-DOG/go-sqlmock"
)

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

    rows := sqlmock.NewRows([]string{"BusinessEntityID", "Name", "Location"}).
        AddRow(1, "Alice", "Canada")

    mock.ExpectQuery("SELECT TOP \\(1\\) BusinessEntityID, FirstName \\+ ' ' \\+ LastName AS Name, CountryRegionName AS Location FROM Sales\\.vSalesPerson WHERE BusinessEntityID = @p1").
        WithArgs(1).
        WillReturnRows(rows)

    emp, err := GetEmployee(db, 1)
    if err != nil {
        t.Fatal(err)
    }
    if emp.Name != "Alice" {
        t.Errorf("Expected Alice, got %s", emp.Name)
    }

    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("Unmet expectations: %v", err)
    }
}

Bir hatayı simüle et

Hata işleme akışlarını test etmek için mock nesnesinden bir hata döndürün:

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

    mock.ExpectQuery("SELECT").
        WithArgs(999).
        WillReturnError(sql.ErrNoRows)

    _, err = GetEmployee(db, 999)
    if err == nil {
        t.Error("Expected error for nonexistent employee")
    }

    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("Unmet expectations: %v", err)
    }
}

Tip

Veri erişim işlevlerinizi, paket düzeyinde genel bir değişken kullanmak yerine, parametre olarak *sql.DB'ı (veya bir arayüzü) kabul edecek şekilde tasarlayın. Bu desen, testlerde go-sqlmock veritabanlarının yerine kullanılmasını kolaylaştırır.

testcontainers-go ile entegrasyon testleri

testcontainers-go her test paketi için bir SQL Server kapsayıcısı oluşturur ve iş bitince onu otomatik olarak kaldırır. Bu yaklaşım, paylaşılan altyapıyı yönetmeden gerçek SQL Server davranışını doğruladığı için çoğu test paketi için önerilir:

go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/mssql

Bu örneği yerel olarak çalıştırmak için:

  1. Docker Desktop veya başka bir yerel Docker motorunun çalıştığından emin olun.
  2. Testi modülünüzdeki bir _test.go dosyaya kaydedin.
  3. Modül kökünden çalıştırın go test -run TestWithContainer -v ./... .

Test paketinizin çoğunda bu yaklaşımı kullanın. Konteynerin başlatılması birkaç saniye sürer, ancak mock nesnelerinin gözden kaçırdığı sorunları yakalayan gerçek SQL Server doğrulamasını elde edersiniz.

package myapp_test

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

    _ "github.com/microsoft/go-mssqldb"
    "github.com/testcontainers/testcontainers-go/modules/mssql"
)

func TestWithContainer(t *testing.T) {
    ctx := context.Background()

    container, err := mssql.Run(ctx,
        "mcr.microsoft.com/mssql/server:2025-latest",
        mssql.WithAcceptEULA(),
        mssql.WithPassword("<password>"))
    if err != nil {
        t.Fatal(err)
    }
    defer container.Terminate(ctx)

    connStr, err := container.ConnectionString(ctx)
    if err != nil {
        t.Fatal(err)
    }

    db, err := sql.Open("sqlserver", connStr)
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    // Create schema.
    _, err = db.ExecContext(ctx, `
        CREATE TABLE dbo.TestDepartments (
            Id INT IDENTITY PRIMARY KEY,
            Name NVARCHAR(50),
            GroupName NVARCHAR(50)
        )`)
    if err != nil {
        t.Fatal(err)
    }

    // Run tests against the real database.
    _, err = db.ExecContext(ctx,
        "INSERT INTO dbo.TestDepartments (Name, GroupName) VALUES (@p1, @p2)",
        sql.Named("p1", "Data Science"),
        sql.Named("p2", "Research and Development"))
    if err != nil {
        t.Fatal(err)
    }

    var count int
    err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM dbo.TestDepartments").Scan(&count)
    if err != nil {
        t.Fatal(err)
    }
    if count != 1 {
        t.Errorf("Expected 1 row, got %d", count)
    }
}

Testcontainers: x509 sertifika hatası

Go 1.23 ve sonrasında bir SQL Server konteynerine bağlanırken şu hatayı görebilirsiniz:

x509: negative serial number

Go 1.23, RFC 5280'i sıkı bir şekilde uygular ve Docker'da SQL Server tarafından oluşturulan kendi kendine imzalanmış sertifika negatif seri numarası kullanır. Test konteynerleri üretim seviyesinde TLS gerektirmediğinden, test bağlantı dizesi'e ekleyin TrustServerCertificate=true veya encrypt=disable ekleyin:

connStr, err := container.ConnectionString(ctx, "TrustServerCertificate=true")
if err != nil {
    t.Fatal(err)
}

Dikkat

Sadece test ortamlarında kullanınTrustServerCertificate=true.encrypt=disable Üretim bağlantıları için uygun sertifika doğrulaması kullanın. Şifreleme ve sertifikalar bölümünü incele.

Daha fazla bilgi için bkz. Sorun giderme.

Testlerle performans kıyaslamaları.B

Go'nun yerleşik kıyaslama çerçevesini kullanarak veritabanı operasyon performansını ölçebilirsiniz:

func BenchmarkInsert(b *testing.B) {
    connString := os.Getenv("TEST_MSSQL_URL")
    if connString == "" {
        b.Skip("TEST_MSSQL_URL not set")
    }

    db, err := sql.Open("sqlserver", connString)
    if err != nil {
        b.Fatalf("open database: %v", err)
    }
    defer db.Close()

    ctx := context.Background()
    db.ExecContext(ctx, `
        IF OBJECT_ID('dbo.BenchItems', 'U') IS NOT NULL DROP TABLE dbo.BenchItems;
        CREATE TABLE dbo.BenchItems (Id INT IDENTITY PRIMARY KEY, Name NVARCHAR(100))`)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        db.ExecContext(ctx,
            "INSERT INTO dbo.BenchItems (Name) VALUES (@p1)",
            sql.Named("p1", fmt.Sprintf("item-%d", i)))
    }

    b.StopTimer()
    db.ExecContext(ctx, "DROP TABLE IF EXISTS dbo.BenchItems")
}

Kıyaslamaları çalıştırın:

go test -bench=BenchmarkInsert -benchmem -count=5

Tam GitHub Actions iş akışı

Bu örnek, SQL Server konteyneri kuran, test şeması oluşturan ve hem birim hem de entegrasyon testlerini çalıştıran tam bir CI boru hattını gösterir:

name: Go SQL Server Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      mssql:
        image: mcr.microsoft.com/mssql/server:2025-latest
        env:
          ACCEPT_EULA: "Y"
          MSSQL_SA_PASSWORD: "<password>"
        ports:
          - 1433:1433
        options: >-
          --health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '<password>' -C -Q 'SELECT 1'"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.22"

      - name: Create test schema
        run: |
          /opt/mssql-tools18/bin/sqlcmd \
            -S localhost -U sa -P "<password>" -C \
                        -Q "CREATE DATABASE AdventureWorks2025"

          /opt/mssql-tools18/bin/sqlcmd \
            -S localhost -U sa -P "<password>" -C \
                        -d AdventureWorks2025 \
            -i ./schema/setup.sql

      - name: Run unit tests
        run: go test -v -short ./...

      - name: Run integration tests
        env:
                    TEST_MSSQL_URL: "sqlserver://sa:<password>@localhost:1433?database=AdventureWorks2025"
        run: go test -v -race -count=1 ./...

Test hata yolları ve yeniden deneme mantığı

Uygulamanızın geçici hataları ve denemeleri doğru şekilde ele alıp almadığını test edin:

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

    // First call fails with a transient error.
    mock.ExpectQuery("SELECT").WillReturnError(fmt.Errorf("mssql: timeout"))

    // Second call succeeds.
    rows := sqlmock.NewRows([]string{"Id"}).AddRow(1)
    mock.ExpectQuery("SELECT").WillReturnRows(rows)

    result, err := queryWithRetry(db, "SELECT ProductID FROM Production.Product WHERE ProductID = @p1", 1)
    if err != nil {
        t.Fatalf("Expected success after retry, got: %v", err)
    }
    if result != 1 {
        t.Errorf("Expected 1, got %d", result)
    }
}

Test stratejisi karşılaştırması

Strategy Hız Gerçek Veritabanı Bağımlılıklar En iyi kullanım alanları
testcontainers-go Orta (saniye) Evet Docker Çoğu test paketi (önerilen).
CI'de Docker Orta (saniye) Evet Docker GitHub Actions ile CI/CD işlem hatları.
İşlemin geri alınması Hızlı (ms) Evet SQL Server Ortak veritabanında entegrasyon testleri.
go-sqlmock Hızlı (ms) Hayır Hiçbiri Sadece uygulama mantığı için iç döngü birim testleri.
t.Skip env var ile Instant Hayır Hiçbiri Hiç DB olmadığında zarif bir bozulma.