Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Ten artykuł omawia wzorce tworzenia testów integracyjnych dla programu SQL Server przy użyciu sterownika go-mssqldb.
Wybierz odpowiedni typ testu
Wolę testować na prawdziwej instancji SQL Server. Kontener SQL Server (za pośrednictwem testcontainers-go, Docker Compose, lub usługi CI) wykrywa błędy składni SQL, niezgodności typów oraz zachowanie transakcji, którego mocki nie wykrywają. To jest takie samo podejście, jakiego sterownik go-mssqldb używa we własnym zestawie testów. Na Windows LocalDB to lekka alternatywa, która nie wymaga Dockera.
Uciekaj się do go-sqlmock tylko w przypadku szybkich testów jednostkowych w wewnętrznej pętli programistycznej, gdy czas uruchamiania kontenera zdominowałby czas wykonania. Na przykład używaj go do testowania logiki powtórek na warstwie aplikacji lub mapowania wyników.
| Typ testu | Użyj go do | Unikaj tego, gdy |
|---|---|---|
Testy integracyjne z testcontainers-go |
Reprodukowalne uruchomienia CI i zestawy testów, które wymagają rzeczywistej instancji SQL Server, bez konieczności zarządzania współdzieloną infrastrukturą. | Szybkie testy w wewnętrznej pętli, w których czas uruchamiania kontenera przeważałby nad czasem wykonania. |
| Testy integracyjne z współdzielonym lub lokalnym SQL Server | Procedury przechowywane, obiekty schematu, zachowanie transakcji, tabele tymczasowe oraz zachowanie sterowników end-to-end. | Testy wymagają izolowanej infrastruktury lub muszą działać konsekwentnie w CI bez zewnętrznych zależności. |
Testy jednostkowe z go-sqlmock |
Logika warstwy aplikacji, jak pętle powtórek, mapowanie wyników i rozgałęzianie błędów, gdy nie trzeba weryfikować składni SQL. | Musisz zweryfikować zachowanie sterowników, składnię SQL względem SQL Server lub semantykę transakcji. |
Testowa konfiguracja bazy danych
Użyj zmiennych środowiskowych, aby skonfigurować testowe parametry połączenia na potrzeby testów integracyjnych. Takie podejście sprawia, że poświadczenia nie znajdują się w kodzie źródłowym, a integracja CI/CD jest prosta:
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())
}
Ustaw zmienną środowiskową przed uruchomieniem testów:
export TEST_MSSQL_URL="sqlserver://<user>:<password>@<server>:1433?database=<database>&encrypt=true&TrustServerCertificate=true"
go test ./...
Wykorzystaj transakcje do izolacji testów
Umieść każdy test w transakcji i wycofaj ją na końcu. To podejście utrzymuje bazę danych w czystości między testami:
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)
}
}
Ten wzorzec najlepiej sprawdza się w testach, które wykonują kod repozytorium wewnątrz jednej granicy transakcji. Nie nadaje się do kodu, który otwiera i commituje własne transakcje wewnętrznie, ani do testów wymagających weryfikacji zachowania między wieloma połączeniami.
SQL Server w Dockerze dla CI/CD
Użyj kontenera SQL Server Linux do testów integracyjnych w potokach 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
Następnie ustaw testowe parametry połączenia:
env:
TEST_MSSQL_URL: "sqlserver://sa:<password>@localhost:1433?database=AdventureWorks2025"
Pomiń testy, gdy nie ma dostępnej bazy danych
W projektach, w których instancja SQL Server może nie zawsze być dostępna, elegancko pomijaj testy integracyjne:
func TestQueryEmployees(t *testing.T) {
if os.Getenv("TEST_MSSQL_URL") == "" {
t.Skip("TEST_MSSQL_URL not set, skipping integration test")
}
// ... test body
}
Pomocnik testu: tworzenie i usuwanie tabel
Stwórz funkcję pomocniczą, która tworzy tabelę testową i oczyszcza ją po teście:
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()
}
Testy jednostkowe z go-sqlmock
Jeśli czas uruchamiania kontenera jest zbyt długi dla twojego wewnętrznego cyklu programistycznego, go-sqlmock tworzy działający w pamięci *sql.DB, który zwraca wstępnie zdefiniowane wyniki. Używaj go do logiki warstwy aplikacji (pętle powtórek, mapowanie wyników, rozgałęzianie błędów), gdzie nie musisz weryfikować składni SQL na prawdziwym serwerze:
go get github.com/DATA-DOG/go-sqlmock
Symuluj zapytanie
Ustaw oczekiwane zapytania i sprawdź, czy aplikacja poprawnie przetwarza wyniki:
Note
sqlmock.ExpectQuery traktuje swoje wejście jako wyrażenie regularne, a nie zwykły ciąg SQL. Znaki takie jak (, ), + i . muszą być poprzedzone znakiem ucieczki, aby można je było dopasować dosłownie w tekście SQL. W literalach stringowych Go te escape pojawiają się podwójnie (na przykład \\( dla literala ( w regexie).
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)
}
}
Symuluj błąd
Zwróć błąd z obiektu mock, aby przetestować scenariusze obsługi błędów:
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)
}
}
Wskazówka
Projektuj funkcje dostępu do danych tak, aby przyjmowały parametr *sql.DB (lub interfejs), zamiast używać globalnej zmiennej na poziomie pakietu. Ten schemat ułatwia zastąpienie go-sqlmock baz danych w testach.
Testy integracyjne z testcontainers-go
testcontainers-go uruchamia kontener SQL Server dla każdego zestawu testów i automatycznie go usuwa. To podejście jest zalecane dla większości zestawów testowych, ponieważ weryfikuje rzeczywiste zachowanie SQL Server bez konieczności zarządzania współdzieloną infrastrukturą:
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/mssql
Aby uruchomić ten przykład lokalnie:
- Upewnij się, że działa Docker Desktop lub inny lokalny silnik Docker.
- Zapisz test w pliku
_test.gow swoim module. - Uruchom
go test -run TestWithContainer -v ./...z root modułu.
Stosuj to podejście do większości swojego zestawu testów. Uruchomienie kontenera zajmuje kilka dodatkowych sekund, ale zyskujesz rzeczywistą walidację w SQL Server, która wykrywa problemy, których mocki nie wychwytują.
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: błąd certyfikatu x509
W wersji Go 1.23 i nowszych możesz zobaczyć ten błąd podczas łączenia z kontenerem SQL Server:
x509: negative serial number
Go 1.23 ściśle egzekwuje RFC 5280, a certyfikat wygenerowany przez SQL Server w Dockerze używa ujemnego numeru seryjnego. Ponieważ kontenery testowe nie wymagają protokołu TLS na poziomie produkcyjnym, dodaj TrustServerCertificate=true lub encrypt=disable do parametrów połączenia testowego:
connStr, err := container.ConnectionString(ctx, "TrustServerCertificate=true")
if err != nil {
t.Fatal(err)
}
Uwaga
Używaj TrustServerCertificate=true lub encrypt=disable tylko w środowiskach testowych. Do połączeń produkcyjnych stosuj właściwą walidację certyfikatów. Zobacz Szyfrowanie i certyfikaty.
Aby uzyskać więcej informacji, zobacz Rozwiązywanie problemów.
Benchmarki wydajności z testowaniem. B
Skorzystaj z wbudowanego frameworka benchmarków Go, aby mierzyć wydajność operacji bazy danych:
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")
}
Uruchamiaj testy wydajności:
go test -bench=BenchmarkInsert -benchmem -count=5
Kompletny przepływ pracy GitHub Actions
Ten przykład pokazuje kompletny potok CI, który konfiguruje kontener SQL Server, tworzy schemat testów i wykonuje zarówno testy jednostkowe, jak i integracyjne:
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 ./...
Ścieżki błędów testów i logika powtórek
Sprawdź, czy Twoja aplikacja poprawnie obsługuje błędy przejściowe i ponawia próby:
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)
}
}
Porównanie strategii testowych
| Strategy | Szybkość | Real DB | Zależności | Najlepsze dla |
|---|---|---|---|---|
testcontainers-go |
Średnia (sekundy) | Yes | Docker | Większość pakietów testów (zalecane). |
| Docker w CI | Średnia (sekundy) | Yes | Docker | Potoki CI/CD z GitHub Actions. |
| Cofnięcie transakcji | Szybko (ms) | Yes | SQL Server | Testy integracyjne na współdzielonej bazie danych. |
go-sqlmock |
Szybko (ms) | No | Żadne | Testy jednostkowe w pętli wewnętrznej tylko dla logiki aplikacji. |
t.Skip Z ENV VAR |
Instant | No | Żadne | Łagodna degradacja, gdy nie ma dostępnej bazy danych. |