You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何不依赖库模拟数据库调用?附Go语言场景示例

如何用假数据库实现单元测试?

核心思路其实很简单:基于你定义的Database接口写个Mock(假实现)——因为Go的接口是隐式实现的,只要结构体有Query方法,就能当Database传给PostgresStore,完全不用依赖真实数据库。

不过这里有个小坑:你原来的Database接口返回的是*sql.Rows这个具体类型,这会导致Mock没法替代。得先把接口改得更抽象一点,先解决这个问题,再写Mock。

第一步:重构接口,解耦具体类型

把*sql.Rows换成自定义的Rows接口,这样既兼容真实的sql.Rows,又能让我们的Mock实现这个接口:

// Rows 定义查询结果的核心方法,和sql.Rows的关键方法对齐
type Rows interface {
    Next() bool
    Scan(dest ...interface{}) error
    Close() error
    Err() error
}

// Database 调整后的接口,返回自定义Rows而不是具体的*sql.Rows
type Database interface {
    Query(query string, args ...interface{}) (Rows, error)
}

// PostgresStore 原结构不变,依然依赖Database接口
type PostgresStore struct {
    db Database
}

真实的*sql.DB的Query方法返回*sql.Rows,而*sql.Rows刚好有这四个方法,所以生产代码里传*sql.DB完全没问题,不用改业务逻辑。

第二步:写Mock实现

先写模拟Rows的结构体,用来模拟查询结果的遍历、扫描等行为:

// MockRows 模拟查询结果行
type MockRows struct {
    data        [][]interface{} // 模拟的每行数据
    currentIdx  int             // 当前遍历到第几行
    mockScanErr error           // 模拟Scan时的错误
    mockCloseErr error          // 模拟Close时的错误
    mockErr     error           // 模拟Rows.Err()返回的错误
}

// Next 实现Rows接口的Next方法,控制是否还有下一行
func (m *MockRows) Next() bool {
    m.currentIdx++
    return m.currentIdx <= len(m.data)
}

// Scan 实现Rows接口的Scan方法,把模拟数据赋值给传入的指针
func (m *MockRows) Scan(dest ...interface{}) error {
    if m.mockScanErr != nil {
        return m.mockScanErr
    }
    // 取当前行的数据,逐个赋值给dest对应的指针
    row := m.data[m.currentIdx-1]
    for i, val := range row {
        switch d := dest[i].(type) {
        case *int:
            *d = val.(int)
        case *string:
            *d = val.(string)
        // 这里根据你的Patient结构体字段类型,补充对应的类型转换
        }
    }
    return nil
}

// Close 实现Rows接口的Close方法
func (m *MockRows) Close() error {
    return m.mockCloseErr
}

// Err 实现Rows接口的Err方法
func (m *MockRows) Err() error {
    return m.mockErr
}

再写模拟数据库的结构体:

// MockDB 模拟Database接口
type MockDB struct {
    mockQueryResult Rows   // 预设的查询结果
    mockQueryErr    error  // 预设的Query错误
}

// Query 实现Database接口的Query方法,返回预设的结果或错误
func (m *MockDB) Query(query string, args ...interface{}) (Rows, error) {
    // 还可以在这里判断传入的SQL语句是否符合预期,比如验证SQL是否正确
    return m.mockQueryResult, m.mockQueryErr
}

第三步:写单元测试用例

现在可以针对不同场景写测试了,比如正常返回数据、Query报错、Scan报错等:

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

// 测试正常获取患者列表的场景
func TestPostgresStore_GetPatients_Success(t *testing.T) {
    // 准备模拟数据
    mockRows := &MockRows{
        data: [][]interface{}{
            {1, "John", "Doe", 30, "ABC Insurance"},
            {2, "Jane", "Smith", 25, "XYZ Insurance"},
        },
    }
    mockDB := &MockDB{
        mockQueryResult: mockRows,
    }
    store := &PostgresStore{db: mockDB}

    patients, err := store.GetPatients()

    assert.NoError(t, err)
    assert.Len(t, patients, 2)
    assert.Equal(t, 1, patients[0].ID)
    assert.Equal(t, "John", patients[0].Name)
}

// 测试Query方法报错的场景
func TestPostgresStore_GetPatients_QueryError(t *testing.T) {
    mockDB := &MockDB{
        mockQueryErr: assert.AnError,
    }
    store := &PostgresStore{db: mockDB}

    patients, err := store.GetPatients()

    assert.Error(t, err)
    assert.Nil(t, patients)
}

// 测试Scan方法报错的场景
func TestPostgresStore_GetPatients_ScanError(t *testing.T) {
    mockRows := &MockRows{
        data:        [][]interface{}{{1, "John", "Doe", 30, "ABC Insurance"}},
        mockScanErr: assert.AnError,
    }
    mockDB := &MockDB{
        mockQueryResult: mockRows,
    }
    store := &PostgresStore{db: mockDB}

    patients, err := store.GetPatients()

    assert.Error(t, err)
    assert.Nil(t, patients)
}

偷懒小技巧:用现成的Mock库

如果不想自己写Mock结构体,可以用社区成熟的库,比如go-sqlmock,它能直接模拟sql.DB的行为,不用改原接口:

import (
    "database/sql"
    "testing"
    "github.com/DATA-DOG/go-sqlmock"
    "github.com/stretchr/testify/assert"
)

func TestPostgresStore_GetPatients_WithSQLMock(t *testing.T) {
    // 创建mock的sql.DB和mock控制器
    db, mock, err := sqlmock.New()
    assert.NoError(t, err)
    defer db.Close()

    // 预期的SQL查询语句,以及返回的结果
    mock.ExpectQuery("SELECT id, name, age, insurance FROM patients").
        WillReturnRows(sqlmock.NewRows([]string{"id", "name", "surname", "age", "insurance"}).
            AddRow(1, "John", "Doe", 30, "ABC Insurance").
            AddRow(2, "Jane", "Smith", 25, "XYZ Insurance"))

    store := &PostgresStore{db: db}
    patients, err := store.GetPatients()

    assert.NoError(t, err)
    assert.Len(t, patients, 2)
    // 验证所有预期的数据库调用都已执行
    assert.NoError(t, mock.ExpectationsWereMet())
}

这种方式更适合直接用database/sql的场景,省得自己写一堆Mock代码。


内容的提问来源于stack exchange,提问作者Axel Oehrli

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 21:40:59