如何为外部包导入的方法创建单元测试预期?Go Testify场景
问题描述
我用Testify框架给Go函数写单元测试时遇到了空指针问题。目标函数CalculateTransaction依赖外部包github.com/jmoiron/sqlx的*sqlx.Tx对象,调用其Commit()和Rollback()方法时触发空指针panic。
目标函数代码(calculate.go)
import ( "context" "errors" "github.com/jmoiron/sqlx" ) func (u usecase) CalculateTransaction(ctx context.Context, ID int64) (err error) { // tx 的类型为 `*sqlx.Tx`,来自上述导入的外部包 tx, err := u.sqlRepo.BeginTX() if err != nil { return err } defer func() { if tx != nil { err = tx.Rollback() if err != nil { return } } }() err = tx.Commit() if err != nil { return err } return nil }
我的测试代码(初始版本)
func TestCalculate(t *testing.T) { var tx *sqlx.Tx var expectedError string mockSQL = new(mocks.SQLRepository) mockSQL.On("BeginTX").Return(tx, nil) // 此处需要 mock tx.Commit() // 此处需要 mock tx.Rollback() usecase := &calculationUsecase{ sqlRepo: mockSQL } actualError := usecase.CalculateTransaction(context.Background(), 1) require.EqualError(t, actualError, expectedError) }
测试运行时触发invalid memory address or nil pointer dereference错误,因为tx是nil,调用Commit()/Rollback()时崩溃。我尝试结合sqlmock,但依然无效:
func TestCalculate(t *testing.T) { var tx *sqlx.Tx var expectedError string dbMock, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected", err) } defer dbMock.Close() mockSQL = new(mocks.SQLRepository) mockSQL.On("BeginTX").Return(tx, nil) mock.ExpectCommit() mock.ExpectRollback() usecase := &calculationUsecase{ sqlRepo: mockSQL } actualError := usecase.CalculateTransaction(context.Background(), 1) require.EqualError(t, actualError, expectedError) }
请问如何正确设置测试预期,解决空指针问题?
解决方案
核心问题
你返回的tx是nil指针,导致后续调用方法时触发空指针panic。要解决这个问题,需要给BeginTX()返回一个可mock的*sqlx.Tx对象,而非nil。
方法一:为sqlx.Tx抽象本地接口(推荐)
通过抽象接口隔离外部依赖,让测试更灵活:
- 在业务包中定义包含所需方法的本地接口:
// 定义本地TX接口,匹配sqlx.Tx的核心方法 type TX interface { Commit() error Rollback() error } // 修改SQLRepository接口,让BeginTX返回本地TX类型 type SQLRepository interface { BeginTX() (TX, error) }
- 业务代码无需改动(
sqlx.Tx会隐式实现该接口),测试时mock本地接口:
func TestCalculate(t *testing.T) { // 创建mock的TX对象 mockTx := new(mocks.TX) // 设置Commit预期:返回nil(模拟提交成功) mockTx.On("Commit").Return(nil) // 设置Rollback预期:返回sql.ErrTxDone(提交成功后回滚会触发该错误) mockTx.On("Rollback").Return(sql.ErrTxDone) // 设置SQLRepository的BeginTX返回mockTx mockSQL := new(mocks.SQLRepository) mockSQL.On("BeginTX").Return(mockTx, nil) usecase := &calculationUsecase{sqlRepo: mockSQL} actualErr := usecase.CalculateTransaction(context.Background(), 1) require.NoError(t, actualErr) // 验证所有mock预期都被执行 mockSQL.AssertExpectations(t) mockTx.AssertExpectations(t) }
方法二:用sqlmock生成真实的*sqlx.Tx对象
如果不想抽象接口,可以用sqlmock创建绑定mock的数据库连接,生成有效*sqlx.Tx:
func TestCalculate(t *testing.T) { dbMock, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer dbMock.Close() // 将sqlmock的DB转换为sqlx.DB sqlxDB := sqlx.NewDb(dbMock, "sqlmock") // 开启mock事务,得到有效*sqlx.Tx tx, err := sqlxDB.BeginTxx(context.Background(), nil) if err != nil { t.Fatal(err) } // 设置BeginTX返回这个mock事务 mockSQL := new(mocks.SQLRepository) mockSQL.On("BeginTX").Return(tx, nil) // 设置sqlmock的预期:Commit成功,Rollback返回ErrTxDone mock.ExpectCommit() mock.ExpectRollback().WillReturnError(sql.ErrTxDone) usecase := &calculationUsecase{sqlRepo: mockSQL} actualErr := usecase.CalculateTransaction(context.Background(), 1) require.NoError(t, actualErr) // 验证sqlmock的所有预期都被满足 if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }
补充说明
- 原业务代码中defer函数存在语法错误(缺少
()),已在上述代码中修正。 - 当
Commit()成功后,Rollback()会返回sql.ErrTxDone,测试时需正确设置该预期,否则会触发断言失败。
内容的提问来源于stack exchange,提问作者new line
相关产品推荐
相关产品推荐

