Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Questo articolo tratta i modelli per scrivere test di integrazione su SQL Server durante l'utilizzo del go-mssqldb driver.
Scegli il tipo di test giusto
Preferisco testare contro un'istanza reale di SQL Server. Un container SQL Server (tramite testcontainers-goDocker Compose o un servizio CI) rileva errori di sintassi SQL, disallineamenti di tipo e comportamenti di transazioni che i mock non possono rilevare. Questo approccio è lo stesso che il go-mssqldb driver utilizza per la propria suite di test. Su Windows, LocalDB è un'alternativa leggera che non richiede Docker.
Ripiega su go-sqlmock solo per i test unitari rapidi del ciclo interno, in cui il tempo di avvio del container supererebbe il tempo di esecuzione. Ad esempio, usarlo per testare la logica di ritenti a livello applicativo o la mappatura dei risultati.
| Tipo di test | Usalo per | Evitalo quando |
|---|---|---|
Test di integrazione con testcontainers-go |
Eseguimenti e suite riproducibili CI che richiedono un'istanza SQL Server reale senza gestire l'infrastruttura condivisa. | Test rapidi a ciclo interno in cui il tempo di avvio del container dominerebbe l'esecuzione. |
| Test di integrazione su un SQL Server condiviso o locale | Procedure memorizzate, oggetti dello schema, comportamento delle transazioni, tabelle temporanee e comportamento dei driver end-to-end. | I test necessitano di un'infrastruttura isolata o devono essere eseguiti in modo coerente in CI senza dipendenze esterne. |
Test unitari con go-sqlmock |
Logica del livello applicativo come cicli di nuovo tentativo, mappatura dei risultati e diramazione in base agli errori quando non è necessario convalidare la sintassi SQL. | Devi verificare il comportamento dei driver, la sintassi SQL rispetto a SQL Server o la semantica delle transazioni. |
Configurazione del database di prova
Usa le variabili di ambiente per configurare la stringa di connessione di test per i test di integrazione. Questo approccio tiene le credenziali fuori dal codice sorgente e rende semplice l'integrazione 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())
}
Imposta la variabile di ambiente prima di eseguire i test:
export TEST_MSSQL_URL="sqlserver://<user>:<password>@<server>:1433?database=<database>&encrypt=true&TrustServerCertificate=true"
go test ./...
Usa le transazioni per l'isolamento dei test
Ingloba ogni test in una transazione e torna indietro alla fine. Questo approccio mantiene il database pulito tra un test e l'altro:
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)
}
}
Questo modello funziona meglio per test che esercitano codice repository all'interno di un singolo confine di transazione. Non è adatto al codice che apre e conferma internamente le proprie transazioni, né ai test che devono verificare il comportamento attraverso più connessioni.
SQL Server in Docker per la CI/CD
Usa un container Linux SQL Server per i test di integrazione nelle pipeline CI:
# GitHub Actions example
services:
mssql:
image: mcr.microsoft.com/mssql/server:2025-latest
env:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "<password>"
ports:
- 1433:1433
Quindi imposta la stringa di connessione di test:
env:
TEST_MSSQL_URL: "sqlserver://sa:<password>@localhost:1433?database=AdventureWorks2025"
Saltare i test quando non è disponibile un database
Per progetti in cui un'istanza di SQL Server potrebbe non essere sempre disponibile, saltare con grazia i test di integrazione:
func TestQueryEmployees(t *testing.T) {
if os.Getenv("TEST_MSSQL_URL") == "" {
t.Skip("TEST_MSSQL_URL not set, skipping integration test")
}
// ... test body
}
Aiuto al test: creare e lasciare tabelle
Crea una funzione helper che configuri un tavolo di test e lo ripulisca dopo il test:
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()
}
Test unitari con go-sqlmock
Se il tempo di avvio del container è troppo lento per il tuo ciclo interno di sviluppo, go-sqlmock si crea un inmemory *sql.DB che restituisce risultati predefiniti. Usalo per la logica a livello applicativo (cicli di ritento, mappatura dei risultati, ramificazione degli errori) dove non devi validare la sintassi SQL contro un server reale:
go get github.com/DATA-DOG/go-sqlmock
Simula una query
Imposta le query previste e verifica che l'applicazione gestisca correttamente i risultati:
Note
sqlmock.ExpectQuery tratta il suo input come un'espressione regolare, non come una semplice stringa SQL. Caratteri come (, ), + e . devono essere preceduti da un carattere di escape perché vengano interpretati letteralmente nel testo SQL. Nei letterali della stringa Go, questi escape appaiono raddoppiati (ad esempio, \\( per un letterale ( nel regex).
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)
}
}
Simula un errore
Restituisci un errore dalla mock per testare i percorsi di gestione degli errori:
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
Progetta le funzioni di accesso ai dati per accettare *sql.DB (o un'interfaccia) come parametro invece di usare un globale a livello di pacchetto. Questo schema rende semplice sostituire go-sqlmock i database nei test.
Test di integrazione con testcontainers-go
testcontainers-goavvia un container SQL Server per ogni suite di test e lo smonta automaticamente. Questo approccio è raccomandato per la maggior parte delle suite di test perché valida il comportamento reale di SQL Server senza gestire l'infrastruttura condivisa:
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/mssql
Per eseguire questo esempio localmente:
- Assicurati che Docker Desktop o un altro motore Docker locale siano in funzione.
- Salva il test in un
_test.gofile nel tuo modulo. - Esegui
go test -run TestWithContainer -v ./...dalla radice del modulo.
Usa questo approccio per la maggior parte della tua suite di test. L'avvio del container aggiunge qualche secondo, ma si ottiene una validazione reale di SQL Server che individua problemi che i mock non riescono a rilevare.
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: errore del certificato X.509
Con Go 1.23 e versioni successive, potresti riscontrare questo errore quando ti connetti a un container SQL Server:
x509: negative serial number
Go 1.23 applica rigorosamente la RFC 5280, e il certificato autofirmato generato da SQL Server in Docker utilizza un numero di serie negativo. Poiché i container di test non necessitano di TLS di livello produttivo, aggiungi TrustServerCertificate=true o encrypt=disable alla stringa di connessione di test:
connStr, err := container.ConnectionString(ctx, "TrustServerCertificate=true")
if err != nil {
t.Fatal(err)
}
Attenzione
Da usare TrustServerCertificate=true o encrypt=disable solo in ambienti di test. Per le connessioni di produzione, usa una validazione dei certificati adeguata.
Vedi Crittografia e certificati.
Per ulteriori informazioni, vedere Risoluzione dei problemi.
Benchmark di prestazioni con test. B
Utilizza il framework benchmark integrato di Go per misurare le prestazioni operative del database:
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")
}
Esegui benchmark:
go test -bench=BenchmarkInsert -benchmem -count=5
Flusso di lavoro completo di GitHub Actions
Questo esempio mostra una pipeline CI completa che configura un container SQL Server, crea uno schema di test ed esegue sia test unitari che di integrazione:
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 ./...
Verifica i percorsi di errore e la logica di ripetizione dei tentativi
Verifica che la tua applicazione gestisca correttamente gli errori temporanei e i nuovi tentativi:
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)
}
}
Confronto delle strategie di prova
| Strategy | Velocità | Real DB | Dipendenze | Ideale per |
|---|---|---|---|---|
testcontainers-go |
Medio (secondi) | Sì | Docker | La maggior parte delle suite di test (consigliate). |
| Docker in CI | Medio (secondi) | Sì | Docker | Pipeline CI/CD con GitHub Actions. |
| Annullamento della transazione | Veloce (ms) | Sì | SQL Server | Test di integrazione su un database condiviso. |
go-sqlmock |
Veloce (ms) | No | None | Test unitari a ciclo interno solo per la logica delle app. |
t.Skip con env var |
Istantaneo | No | None | Degrado graduale quando non è disponibile alcun database. |