chore: initial plan
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
# Mocking and Test Fixtures
|
||||
|
||||
## Mocks with testify/mock
|
||||
|
||||
Create interfaces for your dependencies, then mock them.
|
||||
|
||||
> For the full testify/mock API (argument matchers, call modifiers, verification), see the `samber/cc-skills-golang@golang-stretchr-testify` skill.
|
||||
|
||||
```go
|
||||
// Define the interface
|
||||
type Database interface {
|
||||
GetUser(id string) (*User, error)
|
||||
CreateUser(user *User) error
|
||||
}
|
||||
|
||||
// Mock implementation
|
||||
type MockDatabase struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockDatabase) GetUser(id string) (*User, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockDatabase) CreateUser(user *User) error {
|
||||
args := m.Called(user)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Usage in tests
|
||||
func TestService_GetUser(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
mockDB := new(MockDatabase)
|
||||
service := NewService(mockDB)
|
||||
|
||||
expectedUser := &User{ID: "1", Name: "John"}
|
||||
mockDB.On("GetUser", "1").Return(expectedUser, nil)
|
||||
|
||||
user, err := service.GetUser("1")
|
||||
|
||||
is.NoError(err)
|
||||
is.Equal(expectedUser, user)
|
||||
mockDB.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestService_GetUser_NotFound(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
mockDB := new(MockDatabase)
|
||||
service := NewService(mockDB)
|
||||
|
||||
mockDB.On("GetUser", "999").Return(nil, ErrNotFound)
|
||||
|
||||
user, err := service.GetUser("999")
|
||||
|
||||
is.Error(err)
|
||||
is.ErrorIs(err, ErrNotFound)
|
||||
is.Nil(user)
|
||||
mockDB.AssertExpectations(t)
|
||||
}
|
||||
```
|
||||
|
||||
## Mock Organization
|
||||
|
||||
For larger codebases, organize mocks alongside the code they mock:
|
||||
|
||||
```go
|
||||
// user_service.go
|
||||
type UserService struct {
|
||||
db Database
|
||||
email EmailService
|
||||
}
|
||||
type Database interface {
|
||||
GetUser(id string) (*User, error)
|
||||
CreateUser(user *User) error
|
||||
}
|
||||
type EmailService interface {
|
||||
SendWelcomeEmail(to string) error
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// user_service_test.go
|
||||
package mypackage_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"path/to/mypackage"
|
||||
)
|
||||
|
||||
// MockDatabase implements mypackage.Database
|
||||
type MockDatabase struct {
|
||||
mock.Mock
|
||||
}
|
||||
func (m *MockDatabase) GetUser(id string) (*mypackage.User, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil { return nil, args.Error(1) }
|
||||
return args.Get(0).(*mypackage.User), args.Error(1)
|
||||
}
|
||||
func (m *MockDatabase) CreateUser(user *mypackage.User) error {
|
||||
return m.Called(user).Error(0)
|
||||
}
|
||||
|
||||
// MockEmailService implements mypackage.EmailService
|
||||
type MockEmailService struct {
|
||||
mock.Mock
|
||||
}
|
||||
func (m *MockEmailService) SendWelcomeEmail(to string) error {
|
||||
return m.Called(to).Error(0)
|
||||
}
|
||||
|
||||
func TestUserService_CreateUser(t *testing.T) {
|
||||
mockDB := new(MockDatabase)
|
||||
mockEmail := new(MockEmailService)
|
||||
service := mypackage.NewUserService(mockDB, mockEmail)
|
||||
|
||||
user := &mypackage.User{Name: "Test", Email: "test@example.com"}
|
||||
mockDB.On("CreateUser", user).Return(nil)
|
||||
mockEmail.On("SendWelcomeEmail", "test@example.com").Return(nil)
|
||||
|
||||
err := service.CreateUser(user)
|
||||
|
||||
assert.NoError(t, err)
|
||||
mockDB.AssertExpectations(t)
|
||||
mockEmail.AssertExpectations(t)
|
||||
}
|
||||
```
|
||||
|
||||
## Test Fixtures
|
||||
|
||||
Create reusable test data in a separate package or file:
|
||||
|
||||
```go
|
||||
package fixtures
|
||||
|
||||
import "time"
|
||||
|
||||
var (
|
||||
DefaultUser = &User{
|
||||
ID: "user-123",
|
||||
Name: "Jane Doe",
|
||||
Email: "jane@example.com",
|
||||
CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
AdminUser = &User{
|
||||
ID: "admin-1",
|
||||
Name: "Admin User",
|
||||
Email: "admin@example.com",
|
||||
Role: "admin",
|
||||
CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
)
|
||||
|
||||
func NewUser(name, email string) *User {
|
||||
return &User{
|
||||
ID: "user-" + uuid.New().String(),
|
||||
Name: name,
|
||||
Email: email,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Time Mocking
|
||||
|
||||
Use `clockwork` to test time-dependent code without `time.Sleep()`:
|
||||
|
||||
```go
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScheduler_AddJob(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
fakeClock := clockwork.NewFakeClock()
|
||||
scheduler := NewScheduler(fakeClock)
|
||||
|
||||
job := &Job{ID: "1", RunAt: time.Now().Add(1 * time.Hour)}
|
||||
scheduler.AddJob(job)
|
||||
|
||||
is.Equal(1, scheduler.PendingCount())
|
||||
|
||||
// Advance fake time
|
||||
fakeClock.Advance(2 * time.Hour)
|
||||
|
||||
is.Equal(0, scheduler.PendingCount())
|
||||
}
|
||||
```
|
||||
|
||||
Install clockwork:
|
||||
|
||||
```bash
|
||||
go get github.com/jonboulle/clockwork
|
||||
```
|
||||
Reference in New Issue
Block a user