Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
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:
- Se till att Docker Desktop eller någon annan lokal Docker-motor körs.
- Spara testet i en
_test.gofil i din modul. - 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. |