Testmönster för go-mssqldb

Denna artikel täcker mönster för att skriva integrationstester mot SQL Server när drivrutinen go-mssqldb används.

Välj rätt testtyp

Föredrar att testa mot en riktig SQL Server-instans. En SQL Server-container (via testcontainers-go, Docker Compose eller en CI-tjänst) fångar upp SQL-syntaxfel, typavvikelser och transaktionsbeteende som mocks inte kan upptäcka. Detta är samma metod som drivrutinen go-mssqldb använder för sin egen testsvit. På Windows är LocalDB ett lättviktigt alternativ som inte kräver Docker.

Använd go-sqlmock endast för snabba enhetstester i inner-loop där containerns starttid skulle dominera exekveringen. Använd det till exempel för att testa logik för nya försök på applikationsnivå eller mappning av resultat.

Testtyp Använd den för Undvik det när
Integrationstester med testcontainers-go Reproducerbara CI-körningar och sviter som kräver en riktig SQL Server-instans utan att hantera delad infrastruktur. Snabba inre loop-tester där containerstarttiden skulle dominera exekveringen.
Integrationstester mot en delad eller lokal SQL Server Lagrade procedurer, schemaobjekt, transaktionsbeteende, temporära tabeller och end-to-end-drivrutinsbeteende. Tester kräver isolerad infrastruktur eller måste köras konsekvent i CI utan externa beroenden.
Enhetstester med go-sqlmock Applikationsnivålogik som retry-loopar, resultatmappning och felgrening när du inte behöver validera SQL-syntax. Du behöver verifiera drivrutinsbeteende, SQL-syntax mot SQL Server eller transaktionssemantik.

Testdatabasuppsättning

Använd miljövariabler för att konfigurera testets reťazec pripojenia för integrationstester. Detta tillvägagångssätt håller inloggningsuppgifter utanför källkoden och gör CI/CD-integration enkel:

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

Ställ in miljövariabeln innan du kör tester:

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

Använd transaktioner för testisolering

Slå in varje test i en transaktion och rulla tillbaka i slutet. Denna metod håller databasen ren mellan testerna:

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

Detta mönster fungerar bäst för tester som använder repository-kod inom en enda transaktionsgräns. Det passar inte för kod som öppnar och commitar sina egna transaktioner internt, eller för tester som behöver validera beteende över flera anslutningar.

SQL Server i Docker för CI/CD

Använd en SQL Server Linux-container för integrationstester i CI-pipelines:

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

Sätt sedan testanslutningssträngen:

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

Hoppa över tester när ingen databas finns tillgänglig

För projekt där en SQL Server-instans kanske inte alltid är tillgänglig, hoppa över integrationstester på ett smidigt sätt:

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

Testhjälpare: skapa och släpp tabeller

Skapa en hjälpfunktion som sätter upp en testtabell och rensar upp den efter testet:

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

Enhetstester med go-sqlmock

Om containerns starttid är för långsam för din interna utvecklingsloop skapar go-sqlmock en *sql.DB i minnet som returnerar fördefinierade resultat. Använd det för applikationslagerslogik (återförsöksloopar, resultatmappning, felgrening) där du inte behöver validera SQL-syntax mot en riktig server:

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

Simulera en fråga

Ställ in förväntade frågor och verifiera att applikationen hanterar resultaten korrekt:

Anmärkning

sqlmock.ExpectQuery behandlar sin indata som ett reguljärt uttryck, inte en vanlig SQL-sträng. Tecken som (, ), , +och . måste undvikas för att bokstavligen matcha dem i SQL-text. I Go-stränglitteraler förekommer dessa escape-sekvenser dubblerade (till exempel \\( för ett bokstavligt ( i regexet).

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

Simulera ett fel

Returnera ett fel från mocken för att testa felhanteringsvägar:

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

Designa dina dataåtkomstfunktioner för att acceptera *sql.DB (eller ett gränssnitt) som en parameter istället för att använda en paketnivå-global. Detta mönster gör det enkelt att byta ut go-sqlmock databaser i tester.

Integrationstester med testcontainers-go

testcontainers-go startar en SQL Server-container per testsvit och avvecklar den automatiskt. Denna metod rekommenderas för de flesta testsviter eftersom den validerar verkligt SQL Server-beteende utan att hantera delad infrastruktur:

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

För att köra detta exempel lokalt:

  1. Se till att Docker Desktop eller någon annan lokal Docker-motor körs.
  2. Spara testet i en _test.go fil i din modul.
  3. Kör go test -run TestWithContainer -v ./... från modulens rot.

Använd detta tillvägagångssätt för större delen av din testsvit. Att starta containern lägger till några sekunder, men du får faktisk validering mot SQL Server som fångar problem som mockar missar.

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-certifikatfel

Med Go 1.23 och senare kan du få detta fel när du ansluter till en SQL Server-container:

x509: negative serial number

Go 1.23 upprätthåller strikt RFC 5280, och det självsignerade certifikatet som genereras av SQL Server i Docker använder ett negativt serienummer. Eftersom testcontainrar inte behöver produktionsnivå-TLS, lägg till TrustServerCertificate=true eller encrypt=disable till test-reťazec pripojenia:

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

Försiktighet

Använd TrustServerCertificate=true eller encrypt=disable endast i testmiljöer. För produktionsanslutningar, använd korrekt certifikatvalidering. Se Kryptering och certifikat.

Mer information finns i Felsökning.

Prestandamätningar med tester.B

Använd Gos inbyggda benchmark-ramverk för att mäta databasens prestanda:

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ör prestandatester:

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

Komplett GitHub Actions-arbetsflöde

Detta exempel visar en komplett CI-pipeline som sätter upp en SQL Server-container, skapar ett testschema och kör både enhets- och integrationstester:

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

Testfelvägar och omförsökslogik

Testa att din applikation hanterar tillfälliga fel och återförsök korrekt:

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

Jämförelse av teststrategi

Strategy Hastighet Real DB Beroenden Bäst för
testcontainers-go Medel (sekunder) Ja Docker De flesta testsviter (rekommenderas).
Docker i CI Medel (sekunder) Ja Docker CI/CD-pipelines med GitHub Actions.
Återställning av transaktion Snabbt (ms) Ja SQL Server Integrationstester på en delad databas.
go-sqlmock Snabbt (ms) No None Inner-loop-enhetstester endast för applogik.
t.Skip med env var Instant No None Graciös nedbrytning när ingen DB finns tillgänglig.