Какой подход к покрытию кода тестами вы считаете оптимальным? Стоит ли гнаться за 100% покрытием?

Ответ

Гнаться за 100% покрытием кода тестами — часто неэффективно и экономически нецелесообразно. Оптимальный подход — это риск-ориентированное тестирование, где усилия концентрируются на наиболее важных частях системы.

Мой подход:

  1. Критически важная логика (70-90% покрытия):

    • Бизнес-логика, обработка платежей, алгоритмы, работа с аутентификацией и авторизацией.
    • Здесь высокая цена ошибки, поэтому требуется максимальное покрытие всех возможных сценариев, включая пограничные случаи.
  2. API-контракты и интеграции (60-80%):

    • Тестирование эндпоинтов: проверка кодов ответа, структуры JSON, обработки некорректных входных данных.
    • Важно гарантировать, что ваш сервис корректно взаимодействует с другими системами.
  3. Вспомогательный код и утилиты (30-50%):

    • Простые функции-форматтеры, хелперы, которые легко проверить визуально и риск ошибки в которых минимален.

Пример структуры тестов в Go:

Для проверки логики сервиса мы используем табличные тесты, которые позволяют легко покрыть множество сценариев.

// user_service.go
package users

// IsAdult проверяет, является ли пользователь совершеннолетним
func (s *Service) IsAdult(userID int) (bool, error) {
    user, err := s.repo.GetUserByID(userID)
    if err != nil {
        return false, err // Например, ErrUserNotFound
    }
    return user.Age >= 18, nil
}

// user_service_test.go
package users

func TestIsAdult(t *testing.T) {
    mockRepo := &MockUserRepository{}

    testCases := []struct {
        name          string
        userID        int
        mockAge       int
        mockErr       error
        expected      bool
        expectedErr   bool
    }{
        {"Совершеннолетний", 1, 25, nil, true, false},
        {"Несовершеннолетний", 2, 17, nil, false, false},
        {"Граничный случай (18 лет)", 3, 18, nil, true, false},
        {"Пользователь не найден", 4, 0, ErrUserNotFound, false, true},
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            // Настраиваем мок для каждого кейса
            mockRepo.On("GetUserByID", tc.userID).Return(User{Age: tc.mockAge}, tc.mockErr).Once()

            service := NewService(mockRepo)
            isAdult, err := service.IsAdult(tc.userID)

            assert.Equal(t, tc.expected, isAdult)
            if tc.expectedErr {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
            }
        })
    }
    mockRepo.AssertExpectations(t)
}

Вывод: Покрытие тестами — это инструмент для повышения уверенности в коде, а не самоцель. Важнее покрытия — качество самих тестов, хороший code review и использование статических анализаторов.