0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ENT(Go)でトランザクションを楽にテストする方法について

Posted at

ENT(Go)でトランザクションを楽にテストする方法について

前提:Go 1.24系 / ent v0.14.x / testify + mockery

対象読者

  • Ent を触り始め、Txまわりのユニットテストでハマっている
  • testify/mockmockery でモック生成している
  • panic“expectation unmet”(期待未充足)のエラーが出る

ゴール

  • panic の正体(内部で何が起きているか)を理解する
  • ユニットテストと統合テストの境界を見極める
  • 実物を一部使う方針で、スッキリ動くテストの書き方を示す

1. 事の発端──panic とモック未充足

よく見るエラー

panic: interface conversion: interface {} is *tx.MockTxManager, not *ent.Tx
...snip...
interface conversion: dialect.Driver is nil, not *ent.txDriver

もしくは

assert: mock: I don't know what to return...

(= Commit/Rollback の呼び出し期待が満たされていない)

Tx なんだから Commit() を呼べば満たされるはず…」
→ その Tx が“本物”じゃない(内部 driver がない)と、Commit() の途中で落ちます。


2. Ent の *ent.Tx の中で何が起きているか

client.Tx(ctx) が返すのは次のような構造体(簡略化)です。

type Tx struct {
    config
    driver dialect.Driver // ← 実ドライバが入る(*ent.txDriver を期待)
}
  • コンストラクタは非公開。手で &ent.Tx{} を new しても driver == nil

  • Commit() / Rollback() の内部で

    d := tx.driver.(*txDriver) // 型アサーション
    

    が走るので、driver == nil や異なる型だと ここで panic

つまり:

  • MockTxManager のような別型を返す → 型変換で即 panic
  • &ent.Tx{} のように driver が nil な “空ハコ”を返す → Commit 中に panic

3. 「全部モックでやる」が難しい理由

正常系までユニットテストだけで完結させるなら、

  1. *ent.Tx埋め込みしたラッパーを作り
  2. 内部 driver自作モックドライバを差し込み
  3. Commit/Rollback呼び回数をそこで計測

…という、コスパの悪い作業が必要になります。
ここは発想を転換し、「悪いルートをユニットで、良いルートは統合で」が楽です。


4. どこまでをユニットで担保するか

観点 ユニットで担保 手段
Tx 開始に失敗したらそのエラーを返す client.Tx(ctx) をエラーで返す
fnerror を返したら Rollback し、その error を返す 本物の *ent.Tx を返し、fn に任意エラーを返させる
fnpanic したら Rollback して panic 再送出 同上(assert.Panics で検証)
正常系で Commit される △(統合へ) 統合/結合テストで DB に反映された事実を後検証

5. 実装:Tx 境界のヘルパと“実Txを一部だけ使う”テスト

5.1 Tx 境界のヘルパ(ユニット対象)

// txutil/withtx.go
package txutil

import (
    "context"

    "your/module/ent"
)

// TxBeginner: ent.Client と同じ形だけを要求(最小インターフェース)
type TxBeginner interface {
    Tx(ctx context.Context) (*ent.Tx, error)
}

// WithTx: 例外(panic)も含め、Tx境界の標準パターン
func WithTx(ctx context.Context, b TxBeginner, fn func(context.Context, *ent.Tx) error) (err error) {
    tx, err := b.Tx(ctx)
    if err != nil {
        return err
    }

    // panic も Rollback してから再送出
    defer func() {
        if r := recover(); r != nil {
            _ = tx.Rollback()
            panic(r)
        }
    }()

    if err = fn(ctx, tx); err != nil {
        _ = tx.Rollback()
        return err
    }
    if err = tx.Commit(); err != nil {
        _ = tx.Rollback()
        return err
    }
    return nil
}

5.2 モックの用意(mockery)

# 例:ent.Client 互換の TxBeginner をモック生成
mockery --name TxBeginner --dir txutil --output txutil/mocks --case underscore

既存の ent.Client を直接モックしてもOKです(Tx(ctx) だけ使う想定)。

5.3 “実Tx” を返す準備(テスト側)

// ユニットテストで使う in-memory SQLite の実client
import (
    "entgo.io/ent/enttest"
    _ "github.com/mattn/go-sqlite3" // driver 登録(blank import 必須)
)

func newRealClient(t *testing.T) *ent.Client {
    t.Helper()
    cli := enttest.Open(t, "sqlite3", "file:memdb?mode=memory&_fk=1")
    t.Cleanup(func() { _ = cli.Close() })
    return cli
}

5.4 失敗ルートのユニットテスト(3ケース)

package txutil_test

import (
    "context"
    "testing"

    "your/module/ent"
    "your/module/txutil"
    "your/module/txutil/mocks" // mockery 生成物

    "entgo.io/ent/enttest"
    _ "github.com/mattn/go-sqlite3"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/require"
)

// 実Txを取るヘルパ
func realTx(t *testing.T) *ent.Tx {
    cli := enttest.Open(t, "sqlite3", "file:memdb?mode=memory&_fk=1")
    t.Cleanup(func() { _ = cli.Close() })
    tx, err := cli.Tx(context.Background())
    require.NoError(t, err)
    return tx
}

func Test_WithTx_BeginError(t *testing.T) {
    ctx := context.Background()
    m := mocks.NewTxBeginner(t)
    m.On("Tx", mock.Anything).Return((*ent.Tx)(nil), assert.AnError)

    err := txutil.WithTx(ctx, m, func(context.Context, *ent.Tx) error {
        t.Fatal("fn should not be called")
        return nil
    })
    require.Error(t, err)
    assert.Equal(t, assert.AnError, err)
    m.AssertExpectations(t)
}

func Test_WithTx_FnReturnsError_Rollback(t *testing.T) {
    ctx := context.Background()
    m := mocks.NewTxBeginner(t)
    m.On("Tx", mock.Anything).Return(realTx(t), nil)

    boom := assert.AnError
    err := txutil.WithTx(ctx, m, func(context.Context, *ent.Tx) error {
        // ここで好きに ent を使ってよい(実Txなので panic しない)
        return boom
    })
    require.Error(t, err)
    assert.Equal(t, boom, err)
    m.AssertExpectations(t)
}

func Test_WithTx_FnPanics_RollbackAndRepanic(t *testing.T) {
    ctx := context.Background()
    m := mocks.NewTxBeginner(t)
    m.On("Tx", mock.Anything).Return(realTx(t), nil)

    assert.Panics(t, func() {
        _ = txutil.WithTx(ctx, m, func(context.Context, *ent.Tx) error {
            panic("boom") // panic 経路
        })
    })
    m.AssertExpectations(t)
}

ポイント

  • モックは 「Tx が呼ばれた」 だけを検証。Commit/Rollback回数は数えない(観点外)。
  • 返すのは“実物の *ent.Tx” なので、Commit/Rollback 中に driver 変換で落ちない

6. 正常コミットは「統合テスト」が楽で堅い

本当にコミットされた」を保証するには、実DB に対する後検証が最もシンプルです。

  • 例:WithTx 内で INSERT → 関数戻り後に 別コネクション/別プロセス で SELECT し、永続化を確認。
    ※ in-memory SQLite(file:memdb?...)はコネクション共有に注意。確実に検証したい場合はファイルSQLiteDocker上のPostgresに切り替えると吉。
  • 代替として sqlmockBEGIN/INSERT/COMMIT を期待にしてもよいが、Ent の SQL 生成が変わると壊れやすい

7. よくある落とし穴(失敗例 → 解)

❌ 失敗1:空の &ent.Tx{} を返す

m.On("Tx", mock.Anything).Return(&ent.Tx{}, nil) // driver==nil
// → Commit 中に panic

✅ 解enttest.Open から 実Tx を取って返す。


❌ 失敗2:独自 MockTxManager を返す

type MockTxManager struct{}
m.On("Tx", mock.Anything).Return(&MockTxManager{}, nil)
// → *ent.Tx への型変換で panic

✅ 解戻り値は必ず *ent.Tx(=本物)にする。


❌ 失敗3:Commit() 呼び回数をモックで数える

  • そもそも Commit*ent.Tx の内部実装
  • **“何回呼ばれたか”**は Tx境界のユースケース上ほぼ価値がない(呼ぶべき所で呼べたかが重要)。

✅ 解ユニットでは 失敗ルートのふるまい(Rollback+再送出/エラー透過)に集中。
正常コミット統合テストに回す。


8. 仕上げのチェックリスト

  • TxBeginner最小インターフェースTx(ctx) のみ)に切れている
  • WithTxpanic も Rollback して再送出する
  • ユニットでは Tx開始エラー / fnエラー / fn panic の3系統を網羅
  • テストで返すのは enttest.Open 由来の ent.Tx
  • 正常コミットは統合テスト(実DBで後検証 or sqlmock 監視)
  • mock.AssertExpectations(t)入口だけ検証(回数数え地獄に行かない)

9. まとめ

やりたいこと 推奨手段
Tx開始エラー、fn の error/panic ハンドリング Unit enttest.Open実Tx をモック経由で返す
正常系コミットの保証 IT/E2E 実DBの後検証(別コネクションで SELECT)or sqlmock
Commit/Rollback の回数を数えたい やらない(コスパ悪・価値小)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?