如何不依赖库模拟数据库调用?附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
相关产品推荐
相关产品推荐

