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)
问题根源
- 方法调用名不匹配:测试代码里调用的是
uc.SendVerificationOTP,但业务代码里的目标方法是SendUserOTP,导致实际执行的不是预期方法,直接引发后续调用不匹配。 - 错误场景流程误解:当前测试让
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
相关产品推荐
相关产品推荐

