Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Este artigo aborda padrões para escrever testes de integração contra o SQL Server ao utilizar o go-mssqldb driver.
Escolha o tipo de teste certo
Prefiro testar contra uma instância real do SQL Server. Um contentor SQL Server (via testcontainers-go, Docker Compose ou um serviço CI) detém erros de sintaxe SQL, incompatibilidades de tipos e comportamentos de transações que os mocks não conseguem detetar. Esta abordagem é a mesma que o go-mssqldb driver utiliza para o seu próprio conjunto de testes. No Windows, o LocalDB é uma alternativa leve que não requer Docker.
Recorra a go-sqlmock apenas para testes unitários rápidos do ciclo interno de desenvolvimento, em que o tempo de arranque do contentor dominaria a execução. Por exemplo, use-o para testar lógica de retentativas na camada de aplicação ou mapeamento de resultados.
| Tipo de ensaio | Usa-o para | Evite-o quando |
|---|---|---|
Testes de integração com testcontainers-go |
Execuções de CI reproduzíveis e suites que precisam de uma instância real de SQL Server sem gerir a infraestrutura partilhada. | Testes rápidos de ciclo interno onde o tempo de arranque do contentor dominaria a execução. |
| Testes de integração contra um SQL Server partilhado ou local | Procedimentos armazenados, objetos de esquema, comportamento de transações, tabelas temporárias e comportamento de drivers de ponta a ponta. | Os testes necessitam de infraestrutura isolada ou devem correr consistentemente em CI sem dependências externas. |
Testes unitários com go-sqlmock |
Lógica da camada da aplicação, como ciclos de repetição, mapeamento de resultados e tratamento condicional de erros, quando não é necessário validar a sintaxe SQL. | É preciso verificar o comportamento do driver, a sintaxe SQL em relação ao SQL Server ou a semântica das transações. |
Configuração da base de dados de teste
Utilize variáveis de ambiente para configurar a cadeia de ligação de teste para testes de integração. Esta abordagem mantém credenciais fora do código-fonte e torna a integração CI/CD simples:
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())
}
Defina a variável de ambiente antes de executar os testes:
export TEST_MSSQL_URL="sqlserver://<user>:<password>@<server>:1433?database=<database>&encrypt=true&TrustServerCertificate=true"
go test ./...
Usar transações para isolamento de testes
Execute cada teste numa transação e anule-a no final. Esta abordagem mantém a base de dados limpa entre os testes:
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 padrão funciona melhor para testes que exercem código de repositório dentro de um único limite de transação. Não é adequado para código que abre e confirma as suas próprias transações internamente, nem para testes que precisam de validar o comportamento em múltiplas ligações.
SQL Server no Docker para CI/CD
Use um contentor Linux do SQL Server para testes de integração em 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
Depois, defina a cadeia de ligação de teste:
env:
TEST_MSSQL_URL: "sqlserver://sa:<password>@localhost:1433?database=AdventureWorks2025"
Saltar testes quando não existe uma base de dados disponível
Para projetos onde uma instância do SQL Server pode nem sempre estar disponível, evite os testes de integração com facilidade:
func TestQueryEmployees(t *testing.T) {
if os.Getenv("TEST_MSSQL_URL") == "" {
t.Skip("TEST_MSSQL_URL not set, skipping integration test")
}
// ... test body
}
Auxiliar de teste: criar e eliminar tabelas
Crie uma função auxiliar que configure uma mesa de teste e a limpe após o teste:
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()
}
Testes unitários com go-sqlmock
Se o tempo de arranque do contentor for demasiado lento para o seu ciclo interno de desenvolvimento, go-sqlmock cria-se uma memória *sql.DB interna que devolve resultados pré-definidos. Utilize-o para lógica da camada de aplicação (ciclos de repetição, mapeamento de resultados, tratamento de erros com ramificações condicionais) nos casos em que não seja necessário validar a sintaxe SQL num servidor real:
go get github.com/DATA-DOG/go-sqlmock
Simular uma consulta
Configure as consultas esperadas e verifique se a aplicação gere corretamente os resultados:
Note
sqlmock.ExpectQuery trata a sua entrada como uma expressão regular, não como uma string SQL simples. Caracteres como (, ), +, e . têm de ser escapados para corresponder literalmente a eles em texto SQL. Nos literais da cadeia Go, estes escapes aparecem duplicados (por exemplo, \\( para um literal ( no 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)
}
}
Simular um erro
Faça com que o mock devolva um erro para testar os fluxos de tratamento de erros:
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
Projete as suas funções de acesso a dados para aceitarem *sql.DB (ou uma interface) como parâmetro, em vez de usar um global ao nível de pacote. Este padrão facilita a substituição de go-sqlmock bases de dados nos testes.
Testes de integração com testcontainers-go
testcontainers-golança um contentor SQL Server por cada conjunto de testes e desmonta-o automaticamente. Esta abordagem é recomendada para a maioria das suites de testes porque valida o comportamento real do SQL Server sem gerir a infraestrutura partilhada:
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/mssql
Para executar este exemplo localmente:
- Certifica-te de que o Docker Desktop ou outro motor Docker local está a funcionar.
- Guarda o teste num
_test.goficheiro no teu módulo. - Executa
go test -run TestWithContainer -v ./...a partir da raiz do módulo.
Use esta abordagem para a maior parte do seu conjunto de testes. O arranque do contentor adiciona alguns segundos, mas permite uma validação real do SQL Server que deteta problemas que os mocks não detetam.
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: erro de certificado x509
Com o Go 1.23 e versões posteriores, pode ver este erro ao ligar a um contentor do SQL Server:
x509: negative serial number
O Go 1.23 aplica rigorosamente o RFC 5280, e o certificado autoassinado gerado pelo SQL Server no Docker usa um número de série negativo. Como os contentores de teste não precisam de TLS com nível de produção, adicione TrustServerCertificate=true ou encrypt=disable à cadeia de ligação de teste:
connStr, err := container.ConnectionString(ctx, "TrustServerCertificate=true")
if err != nil {
t.Fatal(err)
}
Caution
Uso TrustServerCertificate=true ou encrypt=disable apenas em ambientes de teste. Para ligações de produção, utilize a validação adequada dos certificados. Veja Encriptação e certificados.
Para mais informações, consulte Resolução de Problemas.
Referências de desempenho com testes.B
Use a estrutura de benchmarks incorporada do Go para medir o desempenho operacional da base de dados:
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")
}
Executar testes de desempenho:
go test -bench=BenchmarkInsert -benchmem -count=5
Fluxo de trabalho completo do GitHub Actions
Este exemplo mostra um pipeline completo de CI que configura um contentor SQL Server, cria um esquema de teste e executa tanto testes unitários como de integração:
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 ./...
Teste os cenários de erro e a lógica de nova tentativa
Verifique se a sua aplicação lida corretamente com erros transitórios e faz novas tentativas:
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)
}
}
Comparação de estratégias de teste
| Strategy | Velocidade | Real DB | Dependencies | Melhor para |
|---|---|---|---|---|
testcontainers-go |
Médio (segundos) | Sim | Docker | A maioria das suítes de testes (recomendada). |
| Docker em CI | Médio (segundos) | Sim | Docker | Pipelines de CI/CD com o GitHub Actions. |
| Reversão de transações | Rápido (ms) | Sim | SQL Server | Testes de integração numa base de dados partilhada. |
go-sqlmock |
Rápido (ms) | No | None | Testes unitários de ciclo interno apenas para a lógica da aplicação. |
t.Skip Com Env Var |
Instantâneo | No | None | Degradação gradual quando não há base de dados disponível. |