Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Este artículo trata patrones para escribir pruebas de integración contra SQL Server al utilizar el go-mssqldb controlador.
Elige el tipo de prueba adecuado
Prefiero probar contra una instancia real de SQL Server. Un contenedor de SQL Server (mediante testcontainers-go, Docker Compose o un servicio de CI) detecta errores de sintaxis de SQL, incompatibilidades de tipos y el comportamiento de las transacciones que los mocks no pueden detectar. Este enfoque es el mismo que utiliza el go-mssqldb controlador para su propio conjunto de pruebas. En Windows, LocalDB es una alternativa ligera que no requiere Docker.
Recurra a go-sqlmock solo para pruebas unitarias rápidas del ciclo interno, donde el tiempo de arranque del contenedor predominaría sobre la ejecución. Por ejemplo, úsalo para probar la lógica de reintentos o el mapeo de resultados a nivel de aplicación.
| Tipo de prueba | Úselo para | Evítalo cuando |
|---|---|---|
Pruebas de integración con testcontainers-go |
Ejecuciones y suites de CI reproducibles que necesitan una instancia real de SQL Server sin tener que gestionar infraestructura compartida. | Pruebas rápidas en bucle interno donde el tiempo de arranque del contenedor dominaría la ejecución. |
| Pruebas de integración contra un SQL Server compartido o local | Procedimientos almacenados, objetos de esquema, comportamiento de transacciones, tablas temporales y comportamiento de controladores de extremo a extremo. | Las pruebas necesitan infraestructura aislada o deben ejecutarse de forma consistente en CI sin dependencias externas. |
Pruebas unitarias con go-sqlmock |
Lógica de nivel de aplicación como bucles de reintento, mapeo de resultados y ramificación de errores cuando no necesitas validar la sintaxis SQL. | Necesitas verificar el comportamiento del driver, la sintaxis de SQL frente a SQL Server o la semántica de las transacciones. |
Configuración de la base de datos de pruebas
Utiliza variables de entorno para configurar la cadena de conexión de prueba para pruebas de integración. Este enfoque mantiene las credenciales fuera del código fuente y facilita la integración 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())
}
Establezca la variable de entorno antes de ejecutar pruebas:
export TEST_MSSQL_URL="sqlserver://<user>:<password>@<server>:1433?database=<database>&encrypt=true&TrustServerCertificate=true"
go test ./...
Usar transacciones para el aislamiento de pruebas
Envuelve cada prueba en una transacción y vuelve atrás al final. Este enfoque mantiene la base de datos limpia entre pruebas:
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)
}
}
Este patrón funciona mejor para pruebas que ejercen código de repositorio dentro de un único límite de transacción. No es adecuado para código que abre y confirma sus propias transacciones internamente, ni para pruebas que necesitan validar el comportamiento en varias conexiones.
SQL Server en Docker para CI/CD
Utiliza un contenedor Linux de SQL Server para pruebas de integración en pipelines de 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
Luego establece la cadena de conexión de prueba:
env:
TEST_MSSQL_URL: "sqlserver://sa:<password>@localhost:1433?database=AdventureWorks2025"
Omitir las pruebas cuando no haya ninguna base de datos disponible
Para proyectos donde una instancia de SQL Server no siempre esté disponible, salta las pruebas de integración con elegancia:
func TestQueryEmployees(t *testing.T) {
if os.Getenv("TEST_MSSQL_URL") == "" {
t.Skip("TEST_MSSQL_URL not set, skipping integration test")
}
// ... test body
}
Ayuda para la prueba: crear y soltar tablas
Crea una función auxiliar que configure una tabla de pruebas y la limpie después de la prueba:
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()
}
Pruebas unitarias con go-sqlmock
Si el tiempo de arranque del contenedor es demasiado lento para tu bucle interno de desarrollo, go-sqlmock crea una memoria *sql.DB interna que devuelve resultados predefinidos. Úsala para lógica de nivel de aplicación (bucles de reintento, mapeo de resultados, ramificación de errores) donde no necesitas validar la sintaxis SQL frente a un servidor real:
go get github.com/DATA-DOG/go-sqlmock
Simular una consulta
Configura las consultas esperadas y verifica que la aplicación gestiona correctamente los resultados:
Note
sqlmock.ExpectQuery trata su entrada como una expresión regular, no como una cadena SQL simple. Caracteres como (, ), +, y . deben ser evadidos para coincidir literalmente con ellos en texto SQL. En los literales de cadena de Go, estas secuencias de escape aparecen duplicadas (por ejemplo, \\( para un ( literal en la expresión regular).
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)
}
}
Simular un error
Devuelve un error del mock para probar las rutas de manejo de errores:
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
Diseña tus funciones de acceso a datos para que las acepten *sql.DB (o una interfaz) como parámetro en lugar de usar un global a nivel de paquete. Este patrón facilita sustituir las bases de datos go-sqlmock en las pruebas.
Pruebas de integración con testcontainers-go
testcontainers-goCrea un contenedor de SQL Server por suite de pruebas y lo desmonta automáticamente. Este enfoque se recomienda para la mayoría de las suites de pruebas porque valida el comportamiento real de SQL Server sin gestionar la infraestructura compartida:
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/mssql
Para ejecutar este ejemplo localmente:
- Asegúrate de que Docker Desktop u otro motor local de Docker esté funcionando.
- Guarda el examen en un
_test.goarchivo dentro de tu módulo. - Ejecuta
go test -run TestWithContainer -v ./...desde la raíz del módulo.
Utiliza este enfoque para la mayor parte de tu suite de pruebas. El inicio del contenedor añade unos segundos, pero obtienes una validación real de SQL Server que detecta problemas y simulacros fallados.
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: error de certificado X.509
Con Go 1.23 y posteriores, podrías ver este error al conectarte a un contenedor de SQL Server:
x509: negative serial number
Go 1.23 aplica estrictamente la RFC 5280, y el certificado autofirmado generado por SQL Server en Docker usa un número de serie negativo. Como los contenedores de prueba no necesitan TLS de nivel de producción, añade TrustServerCertificate=true o encrypt=disable a la cadena de conexión de prueba:
connStr, err := container.ConnectionString(ctx, "TrustServerCertificate=true")
if err != nil {
t.Fatal(err)
}
Precaución
Úsalo TrustServerCertificate=true o encrypt=disable solo en entornos de prueba. Para conexiones de producción, utilice la validación adecuada de certificados. Consulta Cifrado y certificados.
Para obtener más información, consulte Solución de problemas.
Pruebas comparativas de rendimiento mediante pruebas.B
Utiliza el marco de benchmarks integrado de Go para medir el rendimiento de operaciones de bases de datos:
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")
}
Ejecutar pruebas de rendimiento:
go test -bench=BenchmarkInsert -benchmem -count=5
Flujo de trabajo completo de Acciones de GitHub
Este ejemplo muestra una pipeline completa de CI que configura un contenedor de SQL Server, crea un esquema de pruebas y ejecuta tanto pruebas unitarias como de integración:
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 ./...
Rutas de error de prueba y lógica de reintentos
Prueba que tu aplicación gestiona correctamente los errores transitorios y los intentos:
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)
}
}
Comparación de estrategias de prueba
| Strategy | Velocidad | Real DB | Dependencias | Más adecuado para |
|---|---|---|---|---|
testcontainers-go |
Medio (segundos) | Sí | Docker | La mayoría de los conjuntos de pruebas (recomendados). |
| Docker en CI | Promedio (segundos) | Sí | Docker | Canales de CI/CD con Acciones de GitHub. |
| Reversión de transacciones | Rápido (ms) | Sí | SQL Server | Pruebas de integración en una base de datos compartida. |
go-sqlmock |
Rápido (ms) | No | Ninguno | Pruebas unitarias de bucle interno solo para lógica de aplicaciones. |
t.Skip con variable de entorno |
Instantánea | No | Ninguno | Degradación gradual cuando no hay base de datos disponible. |