Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Artikel ini membahas pola untuk menulis pengujian integrasi terhadap SQL Server saat menggunakan go-mssqldb driver.
Pilih jenis tes yang tepat
Lebih suka pengujian terhadap instans SQL Server nyata. Kontainer SQL Server (melalui testcontainers-go, Docker Compose, atau layanan CI) menangkap kesalahan sintaks SQL, ketidakcocokan jenis, dan perilaku transaksi yang tidak dapat dideteksi oleh tiruan. Pendekatan ini sama dengan pendekatan yang digunakan go-mssqldb driver untuk suite pengujiannya sendiri. Di Windows, LocalDB adalah alternatif ringan yang tidak memerlukan Docker.
Kembali ke go-sqlmock hanya untuk pengujian unit loop dalam yang cepat di mana waktu startup kontainer akan mendominasi eksekusi. Misalnya, gunakan untuk menguji logika coba ulang lapisan aplikasi atau pemetaan hasil.
| Jenis pengujian | Gunakan untuk itu | Hindari hal ini saat |
|---|---|---|
Pengujian integrasi dengan testcontainers-go |
Eksekusi CI yang dapat direproduksi dan rangkaian pengujian yang memerlukan instans SQL Server nyata tanpa perlu mengelola infrastruktur bersama. | Pengujian loop dalam yang cepat di mana waktu startup kontainer akan mendominasi eksekusi. |
| Pengujian integrasi terhadap SQL Server bersama atau lokal | Prosedur tersimpan, objek skema, perilaku transaksi, tabel sementara, dan perilaku driver secara menyeluruh. | Pengujian memerlukan infrastruktur terisolasi atau harus berjalan secara konsisten di CI tanpa dependensi eksternal. |
Pengujian unit dengan go-sqlmock |
Logika lapisan aplikasi seperti perulangan coba ulang, pemetaan hasil, dan percabangan kesalahan saat Anda tidak perlu memvalidasi sintaks SQL. | Anda perlu memverifikasi perilaku driver, sintaks SQL terhadap SQL Server, atau semantik transaksi. |
Uji penyiapan database
Gunakan variabel lingkungan untuk mengonfigurasi string koneksi pengujian untuk pengujian integrasi. Pendekatan ini menjauhkan kredensial dari kode sumber dan membuat integrasi CI/CD menjadi mudah:
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())
}
Atur variabel lingkungan sebelum menjalankan pengujian:
export TEST_MSSQL_URL="sqlserver://<user>:<password>@<server>:1433?database=<database>&encrypt=true&TrustServerCertificate=true"
go test ./...
Gunakan transaksi untuk isolasi pengujian
Bungkus setiap pengujian dalam transaksi dan gulung kembali di akhir. Pendekatan ini menjaga database tetap bersih di antara pengujian:
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)
}
}
Pola ini berfungsi paling baik untuk pengujian yang menjalankan kode repositori di dalam batas transaksi tunggal. Ini tidak cocok untuk kode yang membuka dan melakukan transaksinya sendiri secara internal, atau untuk pengujian yang perlu memvalidasi perilaku di beberapa koneksi.
SQL Server di Docker untuk CI/CD
Gunakan kontainer SQL Server Linux untuk pengujian integrasi dalam alur 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
Kemudian atur string koneksi pengujian:
env:
TEST_MSSQL_URL: "sqlserver://sa:<password>@localhost:1433?database=AdventureWorks2025"
Lewati pengujian saat tidak ada database yang tersedia
Untuk proyek di mana instans SQL Server mungkin tidak selalu tersedia, lewati pengujian integrasi dengan anggun:
func TestQueryEmployees(t *testing.T) {
if os.Getenv("TEST_MSSQL_URL") == "" {
t.Skip("TEST_MSSQL_URL not set, skipping integration test")
}
// ... test body
}
Utilitas pengujian: membuat dan menghapus tabel
Buat fungsi pembantu yang menyiapkan tabel pengujian dan membersihkannya setelah pengujian:
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()
}
Pengujian unit dengan go-sqlmock
Jika waktu startup kontainer terlalu lambat untuk siklus pengembangan internal Anda, go-sqlmock membuat *sql.DB di memori yang mengembalikan hasil yang telah ditentukan. Gunakan untuk logika lapisan aplikasi (perulangan coba ulang, pemetaan hasil, percabangan kesalahan) di mana Anda tidak perlu memvalidasi sintaks SQL terhadap server nyata:
go get github.com/DATA-DOG/go-sqlmock
Menyimulasikan kueri
Siapkan kueri yang diharapkan dan verifikasi aplikasi menangani hasilnya dengan benar:
Note
sqlmock.ExpectQuery memperlakukan inputnya sebagai ekspresi reguler, bukan string SQL biasa. Karakter seperti (, ), , +dan . harus diloloskan agar mencocokkannya secara harfiah dalam teks SQL. Dalam literal string Go, karakter escape ini ditulis ganda (misalnya, \\( untuk literal ( pada 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)
}
}
Mengejek kesalahan
Mengembalikan kesalahan dari tiruan untuk menguji jalur penanganan kesalahan:
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
Rancang fungsi akses data Anda agar menerima *sql.DB (atau antarmuka) sebagai parameter, alih-alih menggunakan variabel global tingkat paket. Pola ini memudahkan untuk mengganti go-sqlmock database dalam pengujian.
Pengujian integrasi dengan testcontainers-go
testcontainers-go memulai kontainer SQL Server untuk setiap rangkaian pengujian dan menghapusnya secara otomatis. Pendekatan ini direkomendasikan untuk sebagian besar rangkaian pengujian karena memvalidasi perilaku SQL Server nyata tanpa mengelola infrastruktur bersama:
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/mssql
Untuk menjalankan contoh ini secara lokal:
- Pastikan Docker Desktop atau mesin Docker lokal lainnya sedang berjalan.
- Simpan pengujian dalam
_test.gofile di modul Anda. - Jalankan
go test -run TestWithContainer -v ./...dari root modul.
Gunakan pendekatan ini untuk sebagian besar rangkaian pengujian Anda. Startup container memerlukan beberapa detik tambahan, tetapi Anda mendapatkan validasi SQL Server yang sesungguhnya yang dapat menangkap masalah yang luput dari mock.
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: kesalahan sertifikat X.509
Dengan Go 1.23 dan yang lebih baru, Anda mungkin melihat kesalahan ini saat menyambungkan ke kontainer SQL Server:
x509: negative serial number
Go 1.23 memberlakukan RFC 5280 secara ketat, dan sertifikat yang ditandatangani sendiri yang dihasilkan oleh SQL Server di Docker menggunakan nomor seri negatif. Karena kontainer pengujian tidak memerlukan TLS tingkat produksi, tambahkan TrustServerCertificate=true atau encrypt=disable ke string koneksi pengujian:
connStr, err := container.ConnectionString(ctx, "TrustServerCertificate=true")
if err != nil {
t.Fatal(err)
}
Caution
Gunakan TrustServerCertificate=true atau encrypt=disable hanya di lingkungan pengujian. Untuk koneksi produksi, gunakan validasi sertifikat yang tepat. Lihat Enkripsi dan sertifikat.
Untuk informasi selengkapnya, lihat Pemecahan masalah.
Tolok ukur performa dengan pengujian. B
Gunakan kerangka kerja tolok ukur bawaan Go untuk mengukur performa operasi database:
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")
}
Jalankan tolok ukur:
go test -bench=BenchmarkInsert -benchmem -count=5
Alur kerja GitHub Actions lengkap
Contoh ini menunjukkan alur CI lengkap yang menyiapkan kontainer SQL Server, membuat skema pengujian, dan menjalankan pengujian unit dan integrasi:
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 ./...
Uji skenario error dan logika coba ulang
Uji apakah aplikasi Anda menangani kesalahan sementara dan percobaan ulang dengan benar:
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)
}
}
Perbandingan strategi pengujian
| Strategy | Kecepatan | DB Aktual | Ketergantungan | Paling cocok untuk |
|---|---|---|---|---|
testcontainers-go |
Sedang (detik) | Yes | Docker | Sebagian besar rangkaian pengujian (direkomendasikan). |
| Petugas pelabuhan di CI | Menengah (detik) | Yes | Docker | Alur CI/CD dengan GitHub Actions. |
| Pembatalan transaksi | Cepat (ms) | Yes | SQL Server | Pengujian integrasi pada database bersama. |
go-sqlmock |
Cepat (ms) | No | None | Pengujian unit loop dalam hanya untuk logika aplikasi. |
t.Skip Dengan ENV VAR |
Instan | No | None | Penurunan fungsi secara bertahap saat database tidak tersedia. |