Files
cryptoHermes/internal/repo/persistent/postgres/kline_repo_test.go
dela ee420f9c56 test(postgres): pgxmock 覆盖 5 个 repo,包级 87.6%
为什么用 pgxmock:testcontainers 需要 docker-in-CI 而当前还没有 CI;
mock 路线 1) 零基础设施 2) 把 SQL 字符串和参数顺序作为契约锁死,
schema 漂移立刻可见——这正是上生产前最需要的信号。pgxmock 升级到
testcontainers 不阻塞 v2,留 v2.1 做 nightly 集成测试。

关键改动:
- internal/repo/persistent/postgres/pool.go:新建 pgxPool 接口,
  列出 5 repo 实际用到的 Begin/Query/Exec 三个方法。生产代码继续
  传 *pgxpool.Pool(自动满足接口),测试传 pgxmock.PgxPoolIface。
- 5 个 repo struct 字段从 *pgxpool.Pool 改为 pgxPool 接口;构造器
  签名保留 *pgxpool.Pool,app.go DI 不动。
- 5 个 *_repo_test.go:每个 repo 至少
  · UpsertMany happy path(精确 SQL + 参数)
  · UpsertMany 跳过逻辑(空字段 / 未收线)
  · UpsertMany ExecError(Rollback 路径)
  · FindRecent happy path(验证 DESC → 反序为升序)
  · FindRecent QueryError
  KlineRepo 额外加了 EmptyShortCircuits 和 BeginError 两个边界。

覆盖率:postgres 包 87.6%(UpsertMany ~86%、FindRecent ~93%)。
New*Repo 构造器为 0% 是设计选择:测试直接构造 struct 避开
*pgxpool.Pool 依赖。

所有 12 条守卫扫过无输出。
2026-05-24 20:43:46 +08:00

176 lines
5.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package postgres
import (
"context"
"errors"
"testing"
"cryptoHermes/internal/entity"
"github.com/pashagolub/pgxmock/v4"
"github.com/stretchr/testify/require"
)
func TestKlineRepo_UpsertMany_HappyPath(t *testing.T) {
mock, err := pgxmock.NewPool()
require.NoError(t, err)
defer mock.Close()
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO market_klines").
WithArgs(
"binance", "BTCUSDT", "1h",
int64(1700000000000), int64(1700003599999),
"50000", "50500", "49800", "50200",
"123", "6190000",
int64(789), "60", "3000000",
).
WillReturnResult(pgxmock.NewResult("INSERT", 1))
mock.ExpectCommit()
mock.ExpectRollback() // defer Rollback after Commit is a no-op but mock sees it
r := &KlineRepo{pool: mock}
err = r.UpsertMany(context.Background(), []entity.Kline{{
Source: "binance", Symbol: "BTCUSDT", Interval: "1h",
OpenTime: 1700000000000, CloseTime: 1700003599999,
Open: "50000", High: "50500", Low: "49800", Close: "50200",
Volume: "123", QuoteVolume: "6190000",
TradeCount: 789, TakerBuyBaseVolume: "60", TakerBuyQuoteVolume: "3000000",
IsClosed: true,
}})
require.NoError(t, err)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestKlineRepo_UpsertMany_EmptyShortCircuits(t *testing.T) {
mock, err := pgxmock.NewPool()
require.NoError(t, err)
defer mock.Close()
r := &KlineRepo{pool: mock}
require.NoError(t, r.UpsertMany(context.Background(), nil))
require.NoError(t, mock.ExpectationsWereMet())
}
func TestKlineRepo_UpsertMany_SkipsUnclosed(t *testing.T) {
mock, err := pgxmock.NewPool()
require.NoError(t, err)
defer mock.Close()
mock.ExpectBegin()
// 第二根 K 线 IsClosed=false应跳过只 Exec 一次
mock.ExpectExec("INSERT INTO market_klines").
WithArgs(
"binance", "BTCUSDT", "1h",
int64(1), int64(2),
"1", "1", "1", "1", "1", "1",
int64(0), "", "",
).
WillReturnResult(pgxmock.NewResult("INSERT", 1))
mock.ExpectCommit()
mock.ExpectRollback()
r := &KlineRepo{pool: mock}
err = r.UpsertMany(context.Background(), []entity.Kline{
{Source: "binance", Symbol: "BTCUSDT", Interval: "1h", OpenTime: 1, CloseTime: 2,
Open: "1", High: "1", Low: "1", Close: "1", Volume: "1", QuoteVolume: "1",
IsClosed: true},
{Source: "binance", Symbol: "BTCUSDT", Interval: "1h", OpenTime: 3, CloseTime: 4,
IsClosed: false}, // 未收线,跳过
})
require.NoError(t, err)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestKlineRepo_UpsertMany_BeginError(t *testing.T) {
mock, err := pgxmock.NewPool()
require.NoError(t, err)
defer mock.Close()
mock.ExpectBegin().WillReturnError(errors.New("conn lost"))
r := &KlineRepo{pool: mock}
err = r.UpsertMany(context.Background(), []entity.Kline{{IsClosed: true}})
require.Error(t, err)
require.Contains(t, err.Error(), "begin")
require.NoError(t, mock.ExpectationsWereMet())
}
func TestKlineRepo_UpsertMany_ExecError(t *testing.T) {
mock, err := pgxmock.NewPool()
require.NoError(t, err)
defer mock.Close()
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO market_klines").
WithArgs(
pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(),
pgxmock.AnyArg(), pgxmock.AnyArg(),
pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(),
pgxmock.AnyArg(), pgxmock.AnyArg(),
pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(),
).
WillReturnError(errors.New("constraint violation"))
mock.ExpectRollback()
r := &KlineRepo{pool: mock}
err = r.UpsertMany(context.Background(), []entity.Kline{{
Source: "binance", Symbol: "BTCUSDT", Interval: "1h",
OpenTime: 1700000000000, IsClosed: true,
}})
require.Error(t, err)
require.Contains(t, err.Error(), "upsert kline")
require.NoError(t, mock.ExpectationsWereMet())
}
func TestKlineRepo_FindRecent_HappyPath(t *testing.T) {
mock, err := pgxmock.NewPool()
require.NoError(t, err)
defer mock.Close()
rows := mock.NewRows([]string{
"source", "symbol", "interval", "open_time", "close_time",
"open", "high", "low", "close",
"volume", "quote_volume",
"trade_count", "taker_buy_base_volume", "taker_buy_quote_volume",
}).
// DB 返回时间倒序t=2 先t=1 后
AddRow("binance", "BTCUSDT", "1h", int64(2), int64(3),
"50100", "50200", "50000", "50150",
"10", "500000", int64(100), "5", "250000").
AddRow("binance", "BTCUSDT", "1h", int64(1), int64(2),
"50000", "50100", "49900", "50050",
"9", "450000", int64(90), "4", "200000")
mock.ExpectQuery("SELECT source, symbol, interval").
WithArgs("BTCUSDT", "1h", 300).
WillReturnRows(rows)
r := &KlineRepo{pool: mock}
got, err := r.FindRecent(context.Background(), "BTCUSDT", "1h", 0) // limit<=0 → 300
require.NoError(t, err)
require.Len(t, got, 2)
// 返回应已反序为升序:[1,2]
require.Equal(t, int64(1), got[0].OpenTime)
require.Equal(t, int64(2), got[1].OpenTime)
require.True(t, got[0].IsClosed, "落库的都视为已收线")
require.True(t, got[1].IsClosed)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestKlineRepo_FindRecent_QueryError(t *testing.T) {
mock, err := pgxmock.NewPool()
require.NoError(t, err)
defer mock.Close()
mock.ExpectQuery("SELECT source, symbol, interval").
WithArgs("BTCUSDT", "1h", 100).
WillReturnError(errors.New("timeout"))
r := &KlineRepo{pool: mock}
_, err = r.FindRecent(context.Background(), "BTCUSDT", "1h", 100)
require.Error(t, err)
require.Contains(t, err.Error(), "query")
require.NoError(t, mock.ExpectationsWereMet())
}