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

Gomock测试报错:主方法仅首个子方法被调用,其余缺失调用

Go单元测试修复:SendUserOTP方法子调用missing call问题

问题背景

我有一个负责向用户发送OTP消息的Go方法SendUserOTP,内部依次调用RemoveOTP、GetOneTimePassCode、SaveOTP和SendOTP四个子方法。编写单元测试用例时,执行测试后仅RemoveOTP的调用符合预期,其余三个方法均报missing call(s)错误。我不想使用AnyTimes()这类方案,因为确定所有子方法都会被调用一次,求理想修复方案。

业务代码(user_usecase.go)

func (u *UserUseCase) SendUserOTP(ctx context.Context, user *domain.User, mobileNumber string) error {
    var err error
    var OTPNumber string
    var expiration time.Time
    //Remove previous OTP if any in DB
    if err = u.Repo.RemoveOTP(ctx, user.ID, mobileNumber); err != nil {
        return err
    }

    //Generate OTP
    OTPNumber, expiration, err = u.TokenService.GetOneTimePassCode()
    if err != nil {
        return err
    }

    //Save Code in Database and then send
    otpDataInput := domain.UserMobileOtps{
        UserID:       user.ID,
        MobileNumber: mobileNumber,
        Email:        user.Email,
        OtpCode:      OTPNumber,
        ExpiredAt:    expiration,
    }
    if err = u.Repo.SaveOTP(ctx, otpDataInput); err != nil {
        return err
    }

    if err = u.SMSService.SendOTP(ctx, mobileNumber, OTPNumber); err != nil {
        return err
    }
    return nil
}

测试代码(user_usecase_test.go)

// setup initializes common variables and returns them.
func setup(t *testing.T) (context.Context,
    *gomock.Controller,
    *mocks.MockUserRepository,
    usecase.UserUseCase,
    domain.User,
    *mocks.MockTokenService,
    *mocks.MockSMSService,
) {
    ctx := context.Background()
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockUserRepository(ctrl)
    mockTokenService := mocks.NewMockTokenService(ctrl)
    mockSmsService := mocks.NewMockSMSService(ctrl)
    uc := usecase.UserUseCase{Repo: mockRepo, TokenService: mockTokenService, SMSService: mockSmsService}
    user := domain.User{ID: 1}

    return ctx, ctrl, mockRepo, uc, user, mockTokenService, mockSmsService
}


func TestSendUserOTP_Error(t *testing.T) {
    ctx, ctrl, mockRepo, uc, user, mockTokenService, mockSmsService := setup(t)
    otpExpiration := time.Now().Add(time.Minute * 5)
    defer ctrl.Finish()

    someError := errors.New("some issue")
    mockRepo.EXPECT().RemoveOTP(ctx, user.ID, "XXXXXYYYYY").Return(someError).Times(1)
    mockTokenService.EXPECT().GetOneTimePassCode().Return("XXXYYY", otpExpiration, someError).Times(1)
    mockRepo.EXPECT().SaveOTP(ctx, domain.UserMobileOtps{UserID: user.ID, MobileNumber: "XXXXXYYYYY", Email: user.Email, OtpCode: "XXXYYY", ExpiredAt: otpExpiration}).Return(someError).Times(1)
    mockSmsService.EXPECT().SendOTP(ctx, "XXXXXYYYYY", "XXXYYY").Return(someError).Times(1)

    err := uc.SendVerificationOTP(ctx, &user, "XXXXXYYYYY")
    assert.Error(t, err)
    assert.Equal(t, someError, err)
}

测试错误信息

TestSendUserOTP_Error
    controller.go:310: missing call(s) to *mocks.MockTokenService.GetOneTimePassCode() /project_path/user/usecase/user_usecase_test.go
    
    controller.go:310: missing call(s) to *mocks.MockUserRepository.SaveOTP(is equal to context.Background (*context.emptyCtx), is equal to {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 0 4 XXXXXYYYYY  XXXYYY 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {2023-10-06 10:34:25.951558924 +0530 IST m=+600.001514851 true} {0001-01-01 00:00:00 +0000 UTC false}} /project_path/user/usecase/user_usecase_test.go
  
    controller.go:310: missing call(s) to *mocks.MockSMSService.SendOTP(is equal to context.Background (*context.emptyCtx), is equal to XXXXXYYYYY (string), is equal to XXXYYY (string)) /project_path/user/usecase/user_usecase_test.go
    
    controller.go:310: aborting test due to missing call(s)
--- FAIL: TestSendUserOTP_Error (0.00s)

问题根源

  1. 方法调用名不匹配:测试代码里调用的是uc.SendVerificationOTP,但业务代码里的目标方法是SendUserOTP,导致实际执行的不是预期方法,直接引发后续调用不匹配。
  2. 错误场景流程误解:当前测试让RemoveOTP返回错误,此时业务方法会直接return err终止流程,后续的GetOneTimePassCode、SaveOTP、SendOTP根本不会执行,测试里却期望这三个方法被调用,自然触发missing call。

修复方案

1. 修正方法调用名

把测试里的uc.SendVerificationOTP(ctx, &user, "XXXXXYYYYY")改为uc.SendUserOTP(ctx, &user, "XXXXXYYYYY"),确保调用的是目标业务方法。

2. 按错误场景拆分测试用例

业务方法的错误分支是串行终止的,每个步骤出错都会中断后续流程,因此需要为每个错误场景单独编写测试,不能在一个测试里期望所有方法都被调用:

场景1:RemoveOTP执行失败

func TestSendUserOTP_RemoveOTPFails(t *testing.T) {
    ctx, ctrl, mockRepo, uc, user, _, _ := setup(t)
    defer ctrl.Finish()

    someError := errors.New("failed to remove existing OTP")
    mockRepo.EXPECT().RemoveOTP(ctx, user.ID, "XXXXXYYYYY").Return(someError).Times(1)

    err := uc.SendUserOTP(ctx, &user, "XXXXXYYYYY")
    assert.Error(t, err)
    assert.Equal(t, someError, err)
}

场景2:OTP生成失败

func TestSendUserOTP_GenerateOTPFails(t *testing.T) {
    ctx, ctrl, mockRepo, uc, user, mockTokenService, _ := setup(t)
    defer ctrl.Finish()

    otpErr := errors.New("failed to generate OTP")
    mockRepo.EXPECT().RemoveOTP(ctx, user.ID, "XXXXXYYYYY").Return(nil).Times(1)
    mockTokenService.EXPECT().GetOneTimePassCode().Return("", time.Time{}, otpErr).Times(1)

    err := uc.SendUserOTP(ctx, &user, "XXXXXYYYYY")
    assert.Error(t, err)
    assert.Equal(t, otpErr, err)
}

场景3:SaveOTP执行失败

func TestSendUserOTP_SaveOTPFails(t *testing.T) {
    ctx, ctrl, mockRepo, uc, user, mockTokenService, _ := setup(t)
    otpExpiration := time.Now().Add(5 * time.Minute)
    testOTP := "123456"
    defer ctrl.Finish()

    saveErr := errors.New("failed to save OTP")
    mockRepo.EXPECT().RemoveOTP(ctx, user.ID, "XXXXXYYYYY").Return(nil).Times(1)
    mockTokenService.EXPECT().GetOneTimePassCode().Return(testOTP, otpExpiration, nil).Times(1)
    mockRepo.EXPECT().SaveOTP(ctx, domain.UserMobileOtps{
        UserID:       user.ID,
        MobileNumber: "XXXXXYYYYY",
        Email:        user.Email,
        OtpCode:      testOTP,
        ExpiredAt:    otpExpiration,
    }).Return(saveErr).Times(1)

    err := uc.SendUserOTP(ctx, &user, "XXXXXYYYYY")
    assert.Error(t, err)
    assert.Equal(t, saveErr, err)
}

场景4:SendOTP执行失败

func TestSendUserOTP_SendOTPFails(t *testing.T) {
    ctx, ctrl, mockRepo, uc, user, mockTokenService, mockSmsService := setup(t)
    otpExpiration := time.Now().Add(5 * time.Minute)
    testOTP := "123456"
    defer ctrl.Finish()

    sendErr := errors.New("failed to send OTP")
    mockRepo.EXPECT().RemoveOTP(ctx, user.ID, "XXXXXYYYYY").Return(nil).Times(1)
    mockTokenService.EXPECT().GetOneTimePassCode().Return(testOTP, otpExpiration, nil).Times(1)
    mockRepo.EXPECT().SaveOTP(ctx, domain.UserMobileOtps{
        UserID:       user.ID,
        MobileNumber: "XXXXXYYYYY",
        Email:        user.Email,
        OtpCode:      testOTP,
        ExpiredAt:    otpExpiration,
    }).Return(nil).Times(1)
    mockSmsService.EXPECT().SendOTP(ctx, "XXXXXYYYYY", testOTP).Return(sendErr).Times(1)

    err := uc.SendUserOTP(ctx, &user, "XXXXXYYYYY")
    assert.Error(t, err)
    assert.Equal(t, sendErr, err)
}

场景5:所有步骤执行成功

func TestSendUserOTP_Success(t *testing.T) {
    ctx, ctrl, mockRepo, uc, user, mockTokenService, mockSmsService := setup(t)
    otpExpiration := time.Now().Add(5 * time.Minute)
    testOTP := "123456"
    defer ctrl.Finish()

    mockRepo.EXPECT().RemoveOTP(ctx, user.ID, "XXXXXYYYYY").Return(nil).Times(1)
    mockTokenService.EXPECT().GetOneTimePassCode().Return(testOTP, otpExpiration, nil).Times(1)
    mockRepo.EXPECT().SaveOTP(ctx, domain.UserMobileOtps{
        UserID:       user.ID,
        MobileNumber: "XXXXXYYYYY",
        Email:        user.Email,
        OtpCode:      testOTP,
        ExpiredAt:    otpExpiration,
    }).Return(nil).Times(1)
    mockSmsService.EXPECT().SendOTP(ctx, "XXXXXYYYYY", testOTP).Return(nil).Times(1)

    err := uc.SendUserOTP(ctx, &user, "XXXXXYYYYY")
    assert.NoError(t, err)
}

总结

通过拆分测试用例,每个测试仅覆盖一个错误分支或成功分支,确保测试中的方法调用期望完全匹配业务代码的实际执行流程,同时修正方法调用名的错误,就能解决missing call问题,且无需使用AnyTimes()这类模糊匹配规则。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 14:12:02