Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Cet article traite des schémas pour écrire des tests d’intégration avec SQL Server lors de l’utilisation du go-mssqldb pilote.
Choisissez le bon type de test
Je préfère tester contre une vraie instance SQL Server. Un conteneur SQL Server (via testcontainers-goDocker Compose ou un service CI) détecte les erreurs de syntaxe SQL, les incompatibilités de type et des comportements de transactions que les mocks ne peuvent pas détecter. Cette approche est la même que celle que le go-mssqldb pilote utilise pour sa propre suite de tests. Sous Windows, LocalDB est une alternative légère qui ne nécessite pas Docker.
Recourez à go-sqlmock uniquement pour des tests unitaires rapides de boucle interne, pour lesquels le temps de démarrage du conteneur dominerait le temps d’exécution. Par exemple, utilisez-le pour tester la logique de réévaluation au niveau applicatif ou la correspondance des résultats.
| Type de test | Utilisez-le pour | Évitez-le quand |
|---|---|---|
Tests d’intégration avec testcontainers-go |
Des systèmes de CI reproductibles et des suites qui nécessitent une véritable instance SQL Server sans gérer une infrastructure partagée. | Des tests rapides en boucle interne où le temps de démarrage du conteneur dominerait l’exécution. |
| Tests d’intégration sur un SQL Server partagé ou local | Procédures stockées, objets de schéma, comportement des transactions, tables temporaires et comportement des pilotes de bout en bout. | Les tests nécessitent une infrastructure isolée ou doivent fonctionner de manière cohérente en CI sans dépendances externes. |
Tests unitaires avec go-sqlmock |
La logique de la couche applicative, comme les boucles de nouvelle tentative, la mise en correspondance des résultats et le traitement conditionnel des erreurs lorsque vous n’avez pas besoin de valider la syntaxe SQL. | Vous devez vérifier le comportement des pilotes, la syntaxe SQL par rapport à SQL Server, ou la sémantique des transactions. |
Configuration de la base de données de test
Utilisez des variables d’environnement pour configurer la chaîne de connexion de test pour les tests d’intégration. Cette approche empêche les identifiants d’entrer dans le code source et facilite l’intégration 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())
}
Définissez la variable d’environnement avant d’effectuer les tests :
export TEST_MSSQL_URL="sqlserver://<user>:<password>@<server>:1433?database=<database>&encrypt=true&TrustServerCertificate=true"
go test ./...
Utiliser les transactions pour l’isolation des tests
Exécutez chaque test dans une transaction et annulez-la à la fin. Cette approche maintient la base de données propre entre les tests :
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)
}
}
Ce schéma fonctionne mieux pour les tests qui exercent du code de dépôt à l’intérieur d’une seule frontière de transaction. Ce n’est pas un bon choix pour du code qui ouvre et envoie ses propres transactions en interne, ni pour des tests qui doivent valider un comportement sur plusieurs connexions.
SQL Server dans Docker pour CI/CD
Utilisez un conteneur Linux SQL Server pour les tests d’intégration dans les pipelines 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
Ensuite, définissez la chaîne de connexion de test :
env:
TEST_MSSQL_URL: "sqlserver://sa:<password>@localhost:1433?database=AdventureWorks2025"
Ignorer les tests lorsqu’aucune base de données n’est disponible
Pour les projets où une instance SQL Server n’est pas toujours disponible, évitez les tests d’intégration avec aisance :
func TestQueryEmployees(t *testing.T) {
if os.Getenv("TEST_MSSQL_URL") == "" {
t.Skip("TEST_MSSQL_URL not set, skipping integration test")
}
// ... test body
}
Assistant de test : créer et supprimer des tables
Créez une fonction d’assistance qui configure une table de test et la nettoie après le 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()
}
Tests unitaires avec go-sqlmock
Si le temps de démarrage du go-sqlmock conteneur est trop lent pour votre boucle de développement interne, cela *sql.DB crée une mémoire interne qui restitue des résultats prédéfinis. Utilisez-le pour la logique au niveau applicatif (boucles de réessayage, mappage de résultats, embranchement d’erreurs) où vous n’avez pas besoin de valider la syntaxe SQL contre un vrai serveur :
go get github.com/DATA-DOG/go-sqlmock
Simuler une requête
Configurez les requêtes attendues et vérifiez que l’application gère correctement les résultats :
Note
sqlmock.ExpectQuery traite son entrée comme une expression régulière, et non comme une chaîne SQL simple. Des caractères comme (, ), +, et . doivent être échappés pour les correspondre littéralement dans le texte SQL. Dans les littéraux de chaînes Go, ces échappements apparaissent doublés (par exemple, \\( pour un littéral ( dans le 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)
}
}
Simuler une erreur
Retournez une erreur de la simulation pour tester les chemins de gestion des erreurs :
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
Concevez vos fonctions d’accès aux données pour qu’elles acceptent *sql.DB (ou utilisent une interface) comme paramètre plutôt qu’en utilisant un global au niveau du package. Ce modèle facilite le remplacement de go-sqlmock bases de données dans les tests.
Tests d’intégration avec testcontainers-go
testcontainers-go démarre un conteneur SQL Server par suite de tests et le supprime automatiquement. Cette approche est recommandée pour la plupart des suites de tests car elle valide le comportement réel de SQL Server sans gérer une infrastructure partagée :
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/mssql
Pour exécuter cet exemple localement :
- Assurez-vous que Docker Desktop ou un autre moteur Docker local fonctionne.
- Sauvegardez le test dans un
_test.gofichier de votre module. - Exécute
go test -run TestWithContainer -v ./...depuis la racine du module.
Utilisez cette approche pour la majeure partie de votre suite de tests. Le démarrage du conteneur ajoute quelques secondes, mais vous obtenez une vraie validation SQL Server qui détecte les problèmes et les simulations ratées.
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 : erreur de certificat x509
Avec Go 1.23 et versions ultérieures, vous pourriez voir cette erreur lors de la connexion à un conteneur SQL Server :
x509: negative serial number
Go 1.23 applique strictement la RFC 5280, et le certificat auto-signé généré par SQL Server dans Docker utilise un numéro de série négatif. Puisque les conteneurs de test n’ont pas besoin d’un TLS de niveau production, ajoutez TrustServerCertificate=true ou encrypt=disable à la chaîne de connexion de test :
connStr, err := container.ConnectionString(ctx, "TrustServerCertificate=true")
if err != nil {
t.Fatal(err)
}
Attention
À utiliser TrustServerCertificate=true ou encrypt=disable uniquement dans des environnements de test. Pour les connexions de production, utilisez une validation de certificat appropriée. Voir Chiffrement et certificats.
Pour plus d’informations, consultez la page Dépannage.
Des benchmarks de performance avec des tests. B
Utilisez le framework de benchmark intégré de Go pour mesurer la performance des opérations de base de données :
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")
}
Exécutez des benchmarks :
go test -bench=BenchmarkInsert -benchmem -count=5
Flux de travail complet sur GitHub Actions
Cet exemple montre un pipeline CI complet qui met en place un conteneur SQL Server, crée un schéma de test et exécute à la fois des tests unitaires et d’intégration :
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 ./...
Chemins d’erreur de test et logique de réévaluation
Vérifiez que votre application gère correctement les erreurs transitoires et les tentatives :
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)
}
}
Comparaison des stratégies de test
| Strategy | Vitesse | Real DB | Dependencies | Idéal pour |
|---|---|---|---|---|
testcontainers-go |
Moyen (secondes) | Oui | Docker | La plupart des suites de tests (recommandées). |
| Docker dans CI | Moyen (secondes) | Oui | Docker | Pipelines de CI/CD avec GitHub Actions. |
| Annulation des transactions | Rapide (ms) | Oui | SQL Server | Tests d’intégration sur une base de données partagée. |
go-sqlmock |
Rapide (ms) | Non | None | Tests unitaires en boucle interne uniquement pour la logique applicative. |
t.Skip avec variable d’environnement |
Immédiat | Non | None | Dégradation gracieuse lorsqu’aucune base de données n’est disponible. |