Какой подход к покрытию кода тестами вы считаете оптимальным? Стоит ли гнаться за 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 и использование статических анализаторов.

Ответ 18+ 🔞

А, ну вот, опять про эти 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.NoNoError(t, err)
            }
        })
    }
    mockRepo.AssertExpectations(t)
}

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