Testovací vzory pro go-mssqldb

Tento článek se zabývá vzory psaní integračních testů proti SQL Server při použití ovladačego-mssqldb.

Vyberte správný typ testu

Upřednostněte testování na skutečné instanci SQL Serveru. Kontejner SQL Server (prostřednictvím testcontainers-go, Docker Compose, nebo CI služby) zachytí chyby SQL syntaxe, nesoulady typů a chování transakcí, které mocky nedokážou detekovat. Tento přístup je stejný, jaký ovladač go-mssqldb používá pro vlastní testovací sadu. Na Windows je LocalDB lehká alternativa, která Docker nevyžaduje.

Uchylte se k go-sqlmock pouze u rychlých jednotkových testů v rámci inner-loop, kde by době běhu dominoval čas spuštění kontejneru. Například jej použijte pro testování logiky opakovaného pokusu na aplikační vrstvě nebo mapování výsledků.

Typ testu Použijte jej pro Vyhněte se tomu, když
Integrační testy s testcontainers-go Reprodukovatelné běhy CI a testovací sady, které vyžadují skutečnou instanci SQL Serveru bez nutnosti spravovat sdílenou infrastrukturu. Rychlé testy v rámci inner loopu, kde by spuštění kontejneru tvořilo většinu doby běhu.
Integrační testy proti sdílenému nebo lokálnímu SQL Server Uložené procedury, objekty schématu, chování transakcí, dočasné tabulky a chování ovladačů od začátku do konce. Testy vyžadují izolovanou infrastrukturu nebo musí běžet konzistentně v CI bez externích závislostí.
Jednotkové testy s go-sqlmock Logika aplikační vrstvy jako opakované pokusy, mapování výsledků a větvení chyb, když není potřeba ověřovat syntaxi SQL. Musíte ověřit chování ovladačů, SQL syntaxi vůči SQL Server nebo transakční sémantiku.

Testovací nastavení databáze

Použijte proměnné prostředí ke konfiguraci testovacího připojovacího řetězce pro integrační testy. Tento přístup uchovává přihlašovací údaje mimo zdrojový kód a výrazně usnadňuje integraci CI/CD:

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())
}

Nastavte proměnnou prostředí před spuštěním testů:

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

Použijte transakce k izolaci testů

Každý test zabalte do transakce a na konci se vraťte zpět. Tento přístup udržuje databázi čistou mezi testy:

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)
    }
}

Tento vzor nejlépe funguje pro testy, které procvičují kód repozitáře uvnitř jedné transakční hranice. Není vhodný pro kód, který otevírá a commituje vlastní transakce interně, ani pro testy, které potřebují ověřovat chování napříč více připojeními.

SQL Server v Dockeru pro CI/CD

Použijte SQL Server Linux kontejner pro integrační testy v CI pipeline:

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

Poté nastavte testovací připojovací řetězec:

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

Přeskočit testy, když není dostupná žádná databáze

U projektů, kde instance SQL Server nemusí být vždy dostupná, integrační testy přeskočejte s lehkostí:

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

Pomocník pro test: vytvářet a ukládat tabulky

Vytvořte pomocnou funkci, která nastaví testovací tabulku a po testu ji vyčistí:

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()
}

Jednotkové testy s go-sqlmock

Pokud je doba spouštění kontejneru pro váš interní vývojový cyklus příliš pomalá, go-sqlmock vytvoří v paměti *sql.DB, který vrací předem definované výsledky. Použijte ho pro logiku aplikační vrstvy (opakované pokusy, mapování výsledků, větvení chyb), kde není potřeba ověřovat SQL syntaxi na skutečném serveru:

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

Simulace dotazu

Nastavte očekávané dotazy a ověřte, že aplikace správně zpracovává výsledky:

Note

sqlmock.ExpectQuery považuje svůj vstup za regulární výraz, nikoli za obyčejný SQL řetězec. Znaky jako (, ), +, a . musí být escapovány, aby se s nimi doslova shodovaly v SQL textu. V literálech řetězců Go se tyto úniky zdvojují (například \\( u literálu ( v regexu).

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)
    }
}

Simulovat chybu

Vraťte chybu z mock pro testování cest zpracování chyb:

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

Navrhněte své funkce pro přístup k datům tak, aby přijímaly *sql.DB (nebo rozhraní) jako parametr, místo abyste používaly globální systém na úrovni balíčku. Tento vzorec usnadňuje nahrazování go-sqlmock databází v testech.

Integrační testy s testcontainers-go

testcontainers-gospustí jeden SQL Server kontejner pro každou testovací sadu a automaticky ho rozloží. Tento přístup je doporučován pro většinu testovacích sad, protože ověřuje skutečné chování SQL Server bez nutnosti spravovat sdílenou infrastrukturu:

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

Pro lokální spuštění tohoto příkladu:

  1. Ujistěte se, že běží Docker Desktop nebo jiný lokální Docker engine.
  2. Ulož test do _test.go souboru ve svém modulu.
  3. Spusť go test -run TestWithContainer -v ./... z kořene modulu.

Tento přístup použijte pro většinu testovací sady. Spuštění kontejneru zabere o několik sekund navíc, ale získáte skutečnou validaci SQL Serveru, která odhalí problémy, jež mocky nezachytí.

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)
    }
}

Testkontejnery: x509 chyba certifikátu

U Go 1.23 a novějších se může při připojení ke SQL Server kontejneru objevit tato chyba:

x509: negative serial number

Go 1.23 přísně vynucuje RFC 5280 a samopodepsaný certifikát generovaný SQL Server v Dockeru používá záporné sériové číslo. Protože testovací kontejnery nepotřebují TLS na úrovni produkčního prostředí, přidejte do testovacího připojovacího řetězce TrustServerCertificate=true nebo encrypt=disable:

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

Caution

Používejte TrustServerCertificate=true nebo encrypt=disable pouze v testovacích prostředích. Pro produkční připojení používejte správnou validaci certifikátů. Viz Šifrování a certifikáty.

Další informace najdete v tématu Řešení potíží.

Benchmarky výkonu s testováním. B

Použijte vestavěný benchmarkový rámec Go k měření výkonu provozu databáze:

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")
}

Spouštějte benchmarky:

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

Kompletní pracovní postup GitHub Actions

Tento příklad ukazuje kompletní CI pipeline, která nastavuje SQL Server kontejner, vytváří testovací schéma a spouští jak jednotkové, tak integrační testy:

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 ./...

Cesty chyb testu a logika opakování

Otestujte, zda vaše aplikace správně zpracovává přechodné chyby a opakované pokusy:

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)
    }
}

Srovnání testovacích strategií

Strategy Rychlost Real DB Závislosti Nejlepší pro
testcontainers-go Střední (sekundy) Ano Docker Většina testovacích sad (doporučováno).
Docker v CI Střední (sekundy) Ano Docker CI/CD pipeline s GitHub Actions.
Vrácení transakce Rychlost (ms) Ano SQL Server Integrační testy na sdílené databázi.
go-sqlmock Rychlé (ms) Ne None Jednotkové testy vnitřního cyklu slouží pouze k testování logiky aplikace.
t.Skip s ENV VAR Instant Ne None Elegantní degradace, když není k dispozici DB.