本文介绍了在使用go-mssqldb驱动时针对SQL Server编写集成测试的模式。
选择合适的测试类型
更倾向于用真实的 SQL Server 实例进行测试。 SQL Server 容器(通过 testcontainers-go、Docker 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)
}
}
该模式最适合在单个事务边界内进行存储库代码的测试。 它不适合那些内部开启并提交事务的代码,也不适合需要验证多连接行为的测试。
Docker 中用于 CI/CD 的 SQL Server
在CI管道中使用SQL Server Linux容器进行集成测试:
# 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 字符串字面量中,这些转义序列会显示为双写(例如,\\( 表示正则表达式中的字面值 ()。
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 添加到测试连接字符串中:
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 | Speed | 真实数据库 | Dependencies | 最适用于 |
|---|---|---|---|---|
testcontainers-go |
中等(秒) | 是的 | Docker | 大多数测试套件(推荐)。 |
| Docker 在 CI 中的应用 | 中位数(秒) | 是的 | Docker | 使用 GitHub Actions 的 CI/CD 流水线 |
| 事务回滚 | 快(毫秒) | 是的 | SQL Server | 在共享数据库上进行集成测试。 |
go-sqlmock |
快速(ms) | 否 | None | 仅用于应用逻辑的内环单元测试。 |
t.Skip 使用环境变量 |
即时 | 否 | None | 在没有可用数据库时实现优雅降级。 |