Below there are two tests that test the same scenarios, but the first one uses the constructor NewProcessor and the other a manually created instance of Processor, which is the same struct created by the constructor. Note that the tests are explicitly set up incorrectly where there is always an expectation for a Download call.
If you run them, only the 1st test fails. This means it caught an invalid setup where Download is expected even if there’s an invalid arg. Why doesn’t the second test capture this? The reason is the constructor has mock.AssertExpectations(t), which is called at the end of each test/subtest. This means there is an implicit assertion already, so you don’t need to specify it in the test. However, the second test doesn’t use the constructor; it manually creates the struct. For the second test to capture assertions, it must call mock.AssertExpectations(t) manually.
main.go
//go:generate mockery --name=Processor --dir=. --output=./mocks --outpkg=servicemock --case=snake
// ...
type Processor interface {
Download(ctx context.Context, videoID string) ([]byte, error)
}
func DownloadVideo(ctx context.Context, p Processor, videoID string) ([]byte, error) {
if videoID == "" {
return nil, fmt.Errorf("videoID must be non-empty")
}
return p.Download(ctx, videoID)
}
main_test.go
func TestDownloadVideo_WithExpectations(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
videoID string
want []byte
wantErr bool
}{
{name: "happy_path", videoID: "vid-123", want: []byte("raw"), wantErr: false},
{name: "missing_id", videoID: "", want: nil, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockProc := servicemock.NewProcessor(t)
mockProc.On("Download", mock.Anything, tt.videoID).Return(tt.want, nil).Once()
got, err := DownloadVideo(ctx, mockProc, tt.videoID)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, got)
})
}
}
func TestDownloadVideo_WithoutExpectations(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
videoID string
want []byte
wantErr bool
}{
{name: "happy_path", videoID: "vid-123", want: []byte("raw"), wantErr: false},
{name: "missing_id", videoID: "", want: nil, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockProc := &servicemock.Processor{}
mockProc.Mock.Test(t)
mockProc.On("Download", mock.Anything, tt.videoID).Return(tt.want, nil)
got, err := DownloadVideo(ctx, mockProc, tt.videoID)
if tt.wantErr {
require.Error(t, err)
// mockProc.AssertExpectations(t)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, got)
// mockProc.AssertExpectations(t)
})
}
}
Mocked service below:
// Code generated by mockery v2.53.5. DO NOT EDIT.
package servicemock
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// Processor is an autogenerated mock type for the Processor type
type Processor struct {
mock.Mock
}
// Download provides a mock function with given fields: ctx, videoID
func (_m *Processor) Download(ctx context.Context, videoID string) ([]byte, error) {
ret := _m.Called(ctx, videoID)
if len(ret) == 0 {
panic("no return value specified for Download")
}
var r0 []byte
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok {
return rf(ctx, videoID)
}
if rf, ok := ret.Get(0).(func(context.Context, string) []byte); ok {
r0 = rf(ctx, videoID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, videoID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewProcessor creates a new instance of Processor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewProcessor(t interface {
mock.TestingT
Cleanup(func())
}) *Processor {
mock := &Processor{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}