Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
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:
- Ujistěte se, že běží Docker Desktop nebo jiný lokální Docker engine.
- Ulož test do
_test.gosouboru ve svém modulu. - 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. |