unit-testinggotestingtestify

Strange error when mocking using testfy in golang


I have the following method:

func (u *UserService) Create(createUserDto *CreateUserDto) (*User, error) {
    userExists, err := u.userRepository.FetchByEmail(createUserDto.Email)
    if err != nil {
        return nil, err
    }

    if userExists != nil {
        return nil, &HTTPError{
            Code:    409,
            Message: "Email already exists",
        }
    }

    passwordHash, err := bcrypt.GenerateFromPassword([]byte(createUserDto.Password), bcrypt.DefaultCost)
    if err != nil {
        return nil, err
    }

    activationCode := uuid.New().String()
    activationCodeExpires := time.Now().Add(30 * time.Minute)

    user := &User{
        Name:                  createUserDto.Name,
        Email:                 createUserDto.Email,
        Password:              string(passwordHash),
        Phone:                 createUserDto.Phone,
        Photo:                 createUserDto.Photo,
        IsActive:              false,
        ActivationCode:        &activationCode,
        ActivationCodeExpires: &activationCodeExpires,
    }

    return u.userRepository.Save(user)
}

And I have the following test:

t.Run("Should return a new user", func(t *testing.T) {
    createUserDtoMock := &CreateUserDto{
        Name:     "John Doe",
        Email:    "x6WdM@example.com",
        Password: "password",
        Phone:    nil,
        Photo:    nil,
    }

    var idMock *int64 = new(int64)
    *idMock = 1
    userResponseMock := &User{
        Id:       idMock,
        Name:     "John Doe",
        Email:    "x6WdM@example.com",
        Password: "$2a$10$MmWsAi5AJHnWaUG5vvcl7OUpar9kIhzOMbwp1WBB0dZoFeYYebfKC",
        Phone:    nil,
        Photo:    nil,
    }

    userInputMock := &User{
        Id:       nil,
        Name:     "John Doe",
        Email:    "x6WdM@example.com",
        Password: "password",
        Phone:    nil,
        Photo:    nil,
    }

    repository := &UserRepositoryMock{}
    service := NewUserService(repository)

    repository.On("FetchByEmail", createUserDtoMock.Email).Return((*User)(nil), nil)
    repository.On("Save", userInputMock).Return(userResponseMock, nil)

    user, err := service.Create(createUserDtoMock)

    assert.NoError(t, err)
    assert.IsType(t, &User{}, user)
    assert.Equal(t, *userResponseMock, *user)

    repository.AssertExpectations(t)
})

However, when I run it, I am getting an error that I simply cannot understand:

Running tool: /snap/go/current/bin/go test -timeout 30s -run ^TestUserService$/^Should_return_a_new_user$ github.com/theabner/pallet/internal/core/service

--- FAIL: TestUserService (0.09s)
    --- FAIL: TestUserService/Should_return_a_new_user (0.09s)
panic: 
    
    mock: Unexpected Method Call
    -----------------------------
    
    Save(*entity.User)
            0: &entity.User{Id:(*int64)(nil), Name:"John Doe", Email:"x6WdM@example.com", Password:"$2a$10$3Z7PAQ0F01Xwb/wAzEsQVuEC1O8adELBCUqeMPnxCYEbZ1mSJDk4S", Phone:(*string)(nil), Photo:(*string)(nil), IsActive:false, ActivationCode:(*string)(0xc000114b40), ActivationCodeExpires:time.Date(2024, time.November, 28, 22, 55, 20, 331423224, time.Local), ResetPasswordToken:(*string)(nil), ResetPasswordTokenExpires:<nil>, CreatedAt:<nil>, UpdatedAt:<nil>}
    
    The closest call I have is: 
    
    Save(*entity.User)
            0: &entity.User{Id:(*int64)(nil), Name:"John Doe", Email:"x6WdM@example.com", Password:"password", Phone:(*string)(nil), Photo:(*string)(nil), IsActive:false, ActivationCode:(*string)(nil), ActivationCodeExpires:<nil>, ResetPasswordToken:(*string)(nil), ResetPasswordTokenExpires:<nil>, CreatedAt:<nil>, UpdatedAt:<nil>}
    
    Difference found in argument 0:
    
    --- Expected
    +++ Actual
    @@ -4,3 +4,3 @@
      Email: (string) (len=17) "x6WdM@example.com",
    - Password: (string) (len=8) "password",
    + Password: (string) (len=60) "$2a$10$3Z7PAQ0F01Xwb/wAzEsQVuEC1O8adELBCUqeMPnxCYEbZ1mSJDk4S",
      Phone: (*string)(<nil>),
    @@ -8,4 +8,4 @@
      IsActive: (bool) false,
    - ActivationCode: (*string)(<nil>),
    - ActivationCodeExpires: (*time.Time)(<nil>),
    + ActivationCode: (*string)((len=36) "d33db295-c9c8-411b-a6fc-3818510adc59"),
    + ActivationCodeExpires: (*time.Time)(2024-11-28 22:55:20.331423224 -0300 -03 m=+1800.089611904),
      ResetPasswordToken: (*string)(<nil>),
    
    Diff: 0: FAIL:  (*entity.User=&{<nil> John Doe x6WdM@example.com $2a$10$3Z7PAQ0F01Xwb/wAzEsQVuEC1O8adELBCUqeMPnxCYEbZ1mSJDk4S <nil> <nil> false 0xc000114b40 2024-11-28 22:55:20.331423224 -0300 -03 m=+1800.089611904 <nil> <nil> <nil> <nil>}) != (*entity.User=&{<nil> John Doe x6WdM@example.com password <nil> <nil> false <nil> <nil> <nil> <nil> <nil> <nil>})
    at: [/home/theabnermatheus/Workspace/personal_projects/pallet/internal/shared/mocks/repository/user_repository_mock.go:13 /home/theabnermatheus/Workspace/personal_projects/pallet/internal/core/service/user_service.go:55 /home/theabnermatheus/Workspace/personal_projects/pallet/internal/core/service/user_service_test.go:105]
     [recovered]
    panic: 
    
    mock: Unexpected Method Call
    -----------------------------
    
    Save(*entity.User)
            0: &entity.User{Id:(*int64)(nil), Name:"John Doe", Email:"x6WdM@example.com", Password:"$2a$10$3Z7PAQ0F01Xwb/wAzEsQVuEC1O8adELBCUqeMPnxCYEbZ1mSJDk4S", Phone:(*string)(nil), Photo:(*string)(nil), IsActive:false, ActivationCode:(*string)(0xc000114b40), ActivationCodeExpires:time.Date(2024, time.November, 28, 22, 55, 20, 331423224, time.Local), ResetPasswordToken:(*string)(nil), ResetPasswordTokenExpires:<nil>, CreatedAt:<nil>, UpdatedAt:<nil>}
    
    The closest call I have is: 
    
    Save(*entity.User)
            0: &entity.User{Id:(*int64)(nil), Name:"John Doe", Email:"x6WdM@example.com", Password:"password", Phone:(*string)(nil), Photo:(*string)(nil), IsActive:false, ActivationCode:(*string)(nil), ActivationCodeExpires:<nil>, ResetPasswordToken:(*string)(nil), ResetPasswordTokenExpires:<nil>, CreatedAt:<nil>, UpdatedAt:<nil>}
    
    Difference found in argument 0:
    
    --- Expected
    +++ Actual
    @@ -4,3 +4,3 @@
      Email: (string) (len=17) "x6WdM@example.com",
    - Password: (string) (len=8) "password",
    + Password: (string) (len=60) "$2a$10$3Z7PAQ0F01Xwb/wAzEsQVuEC1O8adELBCUqeMPnxCYEbZ1mSJDk4S",
      Phone: (*string)(<nil>),
    @@ -8,4 +8,4 @@
      IsActive: (bool) false,
    - ActivationCode: (*string)(<nil>),
    - ActivationCodeExpires: (*time.Time)(<nil>),
    + ActivationCode: (*string)((len=36) "d33db295-c9c8-411b-a6fc-3818510adc59"),
    + ActivationCodeExpires: (*time.Time)(2024-11-28 22:55:20.331423224 -0300 -03 m=+1800.089611904),
      ResetPasswordToken: (*string)(<nil>),
    
    Diff: 0: FAIL:  (*entity.User=&{<nil> John Doe x6WdM@example.com $2a$10$3Z7PAQ0F01Xwb/wAzEsQVuEC1O8adELBCUqeMPnxCYEbZ1mSJDk4S <nil> <nil> false 0xc000114b40 2024-11-28 22:55:20.331423224 -0300 -03 m=+1800.089611904 <nil> <nil> <nil> <nil>}) != (*entity.User=&{<nil> John Doe x6WdM@example.com password <nil> <nil> false <nil> <nil> <nil> <nil> <nil> <nil>})
    at: [/home/theabnermatheus/Workspace/personal_projects/pallet/internal/shared/mocks/repository/user_repository_mock.go:13 /home/theabnermatheus/Workspace/personal_projects/pallet/internal/core/service/user_service.go:55 /home/theabnermatheus/Workspace/personal_projects/pallet/internal/core/service/user_service_test.go:105]
    

goroutine 23 [running]:
testing.tRunner.func1.2({0x616fe0, 0xc000115060})
    /snap/go/10743/src/testing/testing.go:1632 +0x230
testing.tRunner.func1()
    /snap/go/10743/src/testing/testing.go:1635 +0x35e
panic({0x616fe0?, 0xc000115060?})
    /snap/go/10743/src/runtime/panic.go:785 +0x132
github.com/stretchr/testify/mock.(*Mock).fail(0xc0001389b0, {0x675778?, 0x4?}, {0xc000138a00?, 0x1?, 0x1?})
    /home/theabnermatheus/go/pkg/mod/github.com/stretchr/testify@v1.10.0/mock/mock.go:349 +0x12d
github.com/stretchr/testify/mock.(*Mock).MethodCalled(0xc0001389b0, {0x714c61, 0x4}, {0xc000114b50, 0x1, 0x1})
    /home/theabnermatheus/go/pkg/mod/github.com/stretchr/testify@v1.10.0/mock/mock.go:509 +0x5d7
github.com/stretchr/testify/mock.(*Mock).Called(0xc0001389b0, {0xc000114b50, 0x1, 0x1})
    /home/theabnermatheus/go/pkg/mod/github.com/stretchr/testify@v1.10.0/mock/mock.go:481 +0x125
github.com/theabner/pallet/internal/shared/mocks/repository.(*UserRepositoryMock).Save(0xc0001389b0, 0xc00017cd80)
    /home/theabnermatheus/Workspace/personal_projects/pallet/internal/shared/mocks/repository/user_repository_mock.go:13 +0x7d
github.com/theabner/pallet/internal/core/service.(*UserService).Create(0xc0001a3f10, 0xc0001a3f20)
    /home/theabnermatheus/Workspace/personal_projects/pallet/internal/core/service/user_service.go:55 +0x2e2
github.com/theabner/pallet/internal/core/service.TestUserService.func5(0xc000174d00)
    /home/theabnermatheus/Workspace/personal_projects/pallet/internal/core/service/user_service_test.go:105 +0x325
testing.tRunner(0xc000174d00, 0x693bc0)
    /snap/go/10743/src/testing/testing.go:1690 +0xf4
created by testing.(*T).Run in goroutine 22
    /snap/go/10743/src/testing/testing.go:1743 +0x390
FAIL    github.com/theabner/pallet/internal/core/service    0.099s
FAIL

Help me, I have tried everything.

All this code needs to do is check if there is already a user with the registered email and if there isn't, hash the password, create an activation hash with an expiration time and send it to the repository which is an interface.


Solution

  • You tell testify to expect a specific call to your Save function.

    Let me demonstrate this with a trivial example:

    type Adder interface {
        Add(x, y int) int
    }
    
    type MyAdder struct{ adder Adder }
    
    func (m MyAdder) Add(x, y int) int {
        return m.adder.Add(x, y)
    }
    

    We have an Adder interface and a MyAdder which just delegates to an Adder.

    When we ‘test’ it like:

    func TestMyAdder_Add(t *testing.T) {
        type args struct {
            x, y int
        }
        tests := []struct {
            name string
            args args
            want int
        }{
            {"zero", args{0, 0}, 0},
            {"onetwothree", args{1, 2}, 3},
            // {"twoonethree", args{2, 1}, 3},
        }
    
        mock := NewMockAdder(t)
        mock.On("Add", 0, 0).Return(0)
        mock.On("Add", 1, 2).Return(3)
    
        m := MyAdder{mock}
        for _, tt := range tests {
            t.Run(tt.name, func(t *testing.T) {
                if got := m.Add(tt.args.x, tt.args.y); got != tt.want {
                    t.Errorf("MyAdder.Add() = %v, want %v", got, tt.want)
                }
            })
        }
    }
    

    everything is fine, although it is debatable what this test is worth. We assert the test will call the embedded adder twice, once with 0, 0 and once with 1, 2.

    When we now add 2, 1 by uncommenting the line above reading {"twoonethree", args{2, 1}, 3}, we get an error:

    --- FAIL: TestMyAdder_Add (0.00s)
        mock.go:351: 
            
            mock: Unexpected Method Call
            -----------------------------
            
            Add(int,int)
                    0: 2
                    1: 1
            
            The closest call I have is: 
            
            Add(int,int)
                    0: 0
                    1: 0
            
            
            Diff: 0: FAIL:  (int=2) != (int=0)
                1: FAIL:  (int=1) != (int=0)
            at: [.../mock_Adder.go:14 .../adder.go:12 .../adder_test.go:26]
        --- FAIL: TestMyAdder_Add/twoonethree (0.00s)
    

    because the mock can only respond to the two specified inputs.

    Since you calculate the non-deterministic arguments passwordHash, activationCode and activationCodeExpires, you can't use a fixed argument set. What would work is

            repository.On("Save", mock.IsType(&User{})).Return(
                func(user *User) (*User, error) {
                    user.Id = new(int64)
                    return user, nil
                })
    

    but the reply would obviously non-deterministic as well, so your assertions won't work.

    Also, as a side note, you need not assert the user type assert.IsType(t, &User{}, user).