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

如何为外部包导入的方法创建单元测试预期?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抽象本地接口(推荐)

通过抽象接口隔离外部依赖,让测试更灵活:

  1. 在业务包中定义包含所需方法的本地接口:
// 定义本地TX接口,匹配sqlx.Tx的核心方法
type TX interface {
  Commit() error
  Rollback() error
}

// 修改SQLRepository接口,让BeginTX返回本地TX类型
type SQLRepository interface {
  BeginTX() (TX, error)
}
  1. 业务代码无需改动(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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 00:06:25