go-mssqldbのテストパターン

この記事では、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)
    }
}

このパターンは、リポジトリコードを単一のトランザクション境界内で行うテストに最適です。 内部でトランザクションを開いてコミットするコードや、複数の接続で動作を検証する必要があるテストには適していません。

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

この例をローカルで実行するために:

  1. Docker Desktopや他のローカルDockerエンジンが動いていることを確認してください。
  2. テスト結果をモジュール内の _test.go ファイルに保存してください。
  3. モジュールのルートから 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=trueencrypt=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ワークフローの完成

この例は、SQL Serverコンテナを設定し、テストスキーマを作成し、ユニットテストと統合テストの両方を実行する完全なCIパイプラインを示しています。

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)
    }
}

テスト戦略比較

戦略 速度 リアルDB(実DB) 依存関係 最適な用途
testcontainers-go 中(秒) はい Docker 大半のテストスイート(推奨)
CIにおけるDocker 中程度(秒) はい Docker GitHub Actionsを使ったCI/CDパイプライン。
トランザクションロールバック 高速(ms) はい SQL Server 共有データベースでの統合テスト。
go-sqlmock 高速 (ms) いいえ None アプリロジックのみのインナーループユニットテスト。
t.Skip ENV VARと共に すぐに いいえ None DBが利用できない場合の段階的な機能低下。