Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье рассматриваются шаблоны написания интеграционных тестов против SQL Server при использовании драйвераgo-mssqldb.
Выберите правильный тип теста
Предпочитаю тестирование на реальных экземплярах SQL Server. Контейнер SQL Server (через testcontainers-goDocker Compose или CI-сервис) фиксирует ошибки SQL-синтаксиса, несоответствия типов и поведение транзакций, которые моки не могут обнаружить. Этот подход — тот же подход, который драйвер go-mssqldb использует для собственного тестового комплекса. В Windows LocalDB — это лёгкая альтернатива, не требующая Docker.
Используйте go-sqlmock только как запасной вариант для быстрых модульных тестов внутреннего цикла, где время запуска контейнера будет составлять основную часть времени выполнения. Например, используйте его для тестирования логики повторных попыток на уровне приложений или отображения результатов.
| Тип теста | Используйте его для | Избегайте этого, когда |
|---|---|---|
Интеграционные тесты с testcontainers-go |
Воспроизводимые CI-запуски и наборы тестов, которым требуется реальный экземпляр SQL Server без необходимости управлять общей инфраструктурой. | Быстрые тесты внутреннего цикла, в которых время запуска контейнера составляло бы основную часть времени выполнения. |
| Интеграционные тесты с общим или локальным SQL Server | Хранимые процедуры, объекты схемы, поведение транзакций, временные таблицы и поведение драйверов от конца до конца. | Тесты требуют изолированной инфраструктуры или должны выполняться стабильно в CI без внешних зависимостей. |
Модульные тесты с go-sqlmock |
Логика на уровне прикладного уровня, такая как циклы повторного тестирования, отображение результатов и ветвление ошибок, когда не нужно проверять синтаксис SQL. | Вам нужно проверить поведение драйвера, синтаксис SQL с SQL Server или семантику транзакций. |
Настройка тестовой базы данных
Используйте переменные среды для настройки тестовой строки подключения для интеграционных тестов. Такой подход позволяет не хранить учётные данные в исходном коде и упрощает интеграцию с 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())
}
Задайте переменную среды перед запуском тестов:
export TEST_MSSQL_URL="sqlserver://<user>:<password>@<server>:1433?database=<database>&encrypt=true&TrustServerCertificate=true"
go test ./...
Используйте транзакции для изоляции тестов
Выполняйте каждый тест в транзакции и откатывайте её в конце. Такой подход поддерживает чистоту базы данных между тестами:
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)
}
}
Этот паттерн лучше всего работает для тестов, которые реализуют код репозитория внутри одной границы транзакции. Он не подходит для кода, который открывает и коммитирует свои транзакции внутри, или для тестов, которые требуют проверки поведения на нескольких соединениях.
SQL Server в Docker для CI/CD
Используйте контейнер Linux SQL Server для интеграционных тестов в 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
Затем установите тестовую строку подключения:
env:
TEST_MSSQL_URL: "sqlserver://sa:<password>@localhost:1433?database=AdventureWorks2025"
Пропуск тестов, когда база данных недоступна
Для проектов, где экземпляр SQL Server может быть не всегда доступен, корректно пропускайте интеграционные тесты:
func TestQueryEmployees(t *testing.T) {
if os.Getenv("TEST_MSSQL_URL") == "" {
t.Skip("TEST_MSSQL_URL not set, skipping integration test")
}
// ... test body
}
Помощник для тестирования: создание и удаление таблиц
Создайте вспомогательную функцию, которая настраивает тестовую таблицу и очищает её после теста:
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()
}
Модульные тесты с помощью go-sqlmock
Если запуск контейнера занимает слишком много времени для вашего внутреннего цикла разработки, go-sqlmock создаёт хранилище *sql.DB в памяти, которое возвращает предопределённые результаты. Используйте его для логики прикладного уровня (циклы повторного тестирования, отображение результатов, ветвление ошибок), где не нужно проверять синтаксис SQL на реальных серверах:
go get github.com/DATA-DOG/go-sqlmock
Имитация запроса
Настройте ожидаемые запросы и убедитесь, правильно ли приложение обрабатывает результаты:
Замечание
sqlmock.ExpectQuery воспринимает свой вход как регулярное выражение, а не как обычную SQL-строку. Символы, такие как (, )+, , и . должны быть скрыты, чтобы буквально совпадать с ними в SQL-тексте. В литералах строк Go эти escapes выглядят удвоенными (например, \\( для литерала ( в 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)
}
}
Имитируйте ошибку
Вернёт ошибку из макета для проверки путей обработки ошибок:
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
Спроектируйте функции доступа к данным так, чтобы они принимали *sql.DB (или использовали интерфейс) как параметр, а не использовали глобальный параметр на уровне пакета. Эта схема облегчает замену go-sqlmock баз данных в тестах.
Интеграционные тесты с testcontainers-go
testcontainers-go запускает контейнер SQL Server для каждого набора тестов и автоматически удаляет его. Этот подход рекомендуется для большинства тестовых наборов, поскольку он проверяет реальное поведение SQL Server без управления общей инфраструктурой:
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/mssql
Чтобы провести этот пример локально:
- Убедитесь, что работает Docker Desktop или другой локальный движок Docker.
- Сохраните тест в
_test.goфайле в вашем модуле. - Запускайте
go test -run TestWithContainer -v ./...с корня модуля.
Используйте этот подход для большей части набора тестов. Запуск контейнера добавляет несколько секунд, но вы получаете реальную валидацию SQL Server, которая ловит проблемы, которые пропущены имитации.
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: ошибка сертификата x509
В Go 1.23 и более поздних версиях вы можете увидеть такую ошибку при подключении к контейнеру SQL Server:
x509: negative serial number
Go 1.23 строго применяет RFC 5280, а самоподписанный сертификат, генерируемый SQL Server в Docker, использует отрицательный серийный номер. Поскольку тестовым контейнерам не требуется производственный TLS, добавьте TrustServerCertificate=true или encrypt=disable к test строка подключения:
connStr, err := container.ConnectionString(ctx, "TrustServerCertificate=true")
if err != nil {
t.Fatal(err)
}
Предостережение
Используйте TrustServerCertificate=true или encrypt=disable только в тестовых средах. Для производственных соединений используйте правильную валидацию сертификатов. См. Шифрование и сертификаты.
Дополнительные сведения см. в разделе "Устранение неполадок".
Бенчмарки производительности с тестированием. B
Используйте встроенный фреймворк для бенчмарков Go для оценки эффективности работы с базой данных:
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")
}
Запускайте бенчмарки:
go test -bench=BenchmarkInsert -benchmem -count=5
Полный рабочий процесс в GitHub Actions
В этом примере показан полный CI-конвейер, который настраивает контейнер SQL Server, создаёт тестовую схему и запускает как модульные, так и интеграционные тесты:
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 ./...
Тестируйте пути ошибок и повторяйте логику
Проверьте, правильно ли ваше приложение обрабатывает временные ошибки и повторения:
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)
}
}
Сравнение стратегий тестирования
| Strategy | Скорость | Реальная БД | Зависимости | лучше всего подходит для |
|---|---|---|---|---|
testcontainers-go |
Средний (секунды) | Да | Docker | Большинство наборов тестов (рекомендуется). |
| Docker в CI | Средний (секунды) | Да | Docker | CI/CD конвейеры с GitHub Actions. |
| Откат транзакций | Быстро (ms) | Да | SQL Server | Интеграционные тесты на общей базе данных. |
go-sqlmock |
Быстро (ms) | Нет | None | Модульные тесты для внутреннего цикла разработки только для логики приложения. |
t.Skip с ENV VAR |
Instant | Нет | None | Плавная деградация при недоступности базы данных. |