本文介紹了在使用go-mssqldb驅動程式時,針對 SQL Server 撰寫整合測試的模式。
選擇合適的測試類型
建議用真實的 SQL Server 實例來測試。 SQL Server 容器(透過 testcontainers-go、Docker Compose 或 CI 服務)會偵測 SQL 語法錯誤、型別不符以及 mock 無法偵測的交易行為。 這種方法與 go-mssqldb 驅動程式用於其自身測試套件的方法相同。 在 Windows 上,LocalDB 是一個輕量級的替代方案,不需要 Docker。
只有在快速的 inner-loop 單元測試中,且容器啟動時間會占執行時間大宗時,才回退至 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)
}
}
此模式最適合在單一交易邊界內執行儲存庫程式碼的測試。 它不適合那些在內部開啟並提交交易的程式碼,或是需要驗證多重連線行為的測試。
用於 CI/CD 的 Docker 中 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
模擬查詢
設定預期查詢並驗證應用程式是否正確處理結果:
Note
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,而 Docker 中 SQL Server 產生的自簽憑證使用負序號。 由於測試容器不需要生產環境等級的 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 | 速度 | 真實資料庫 | 依賴 | 最適合用於 |
|---|---|---|---|---|
testcontainers-go |
中等(秒) | 是的 | Docker | 大多數測試套件(推薦)。 |
| CI 中的 Docker | 中等(秒) | 是的 | Docker | 使用 GitHub Actions 的 CI/CD 流程 |
| 交易復原 | 快速(毫秒) | 是的 | SQL Server | 在共享資料庫上的整合測試。 |
go-sqlmock |
快速(毫秒) | No | None | 僅針對應用程式邏輯的內部迴圈單元測試。 |
t.Skip 搭配環境變數 |
立即 | No | None | 當資料庫無法使用時,平順降級。 |