Контекст: Проект имел 0% тестового покрытия. Для production-сервиса это критично. После внедрения [[atomic-dependency-inversion-go]] стало возможным тестировать сервисы с моками.
Суть: Используем table-driven тесты с интерфейсными моками.
type mockPlaceRepo struct {
places []*models.Place
}
func (m *mockPlaceRepo) GetByID(ctx context.Context, id string) (*models.Place, error) {
for _, p := range m.places {
if p.ID == id { return p, nil }
}
return nil, nil
}
func TestPlaceService_Update_NotOwner(t *testing.T) {
repo := &mockPlaceRepo{}
svc := NewPlaceService(repo)
repo.places = append(repo.places, &models.Place{
ID: "place-1", OwnerID: "owner-1",
})
_, err := svc.Update(ctx, UpdatePlaceInput{
ID: "place-1", OwnerID: "owner-2",
}, false)
if !errors.Is(err, ErrNotYourPlace) {
t.Errorf("expected ErrNotYourPlace, got %v", err)
}
}
Table-driven test pattern:
func TestAuthService_Login(t *testing.T) {
tests := []struct {
name string
email string
password string
wantErr error
}{
{"valid credentials", "user@test.com", "pass", nil},
{"wrong password", "user@test.com", "wrong", ErrInvalidCreds},
{"banned user", "banned@test.com", "pass", ErrUserBanned},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _, err := svc.Login(ctx, LoginInput{tt.email, tt.password})
if !errors.Is(err, tt.wantErr) { t.Error(...) }
})
}
}
Базовая структура тестов для нового сервиса:
Связанные заметки: [[decision-test-strategy]], [[atomic-dependency-inversion-go]]
Источник: Реализация тестов services/auth_test.go, services/places_test.go
#testing #golang #best-practice #clean-architecture