atomic-test-patterns-go.md 2.3 KB

Паттерны тестирования Go: моки и интерфейсы

Контекст: Проект имел 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(...) }
        })
    }
}

Базовая структура тестов для нового сервиса:

  1. Создать mock репозитория
  2. Инициализировать сервис с моком
  3. Вызвать метод
  4. Проверить результат и ошибку

Связанные заметки: [[decision-test-strategy]], [[atomic-dependency-inversion-go]] Источник: Реализация тестов services/auth_test.go, services/places_test.go

#testing #golang #best-practice #clean-architecture