Какой у вас подход к написанию Unit-тестов в Go?

Ответ

Мой подход к unit-тестированию основан на том, что тесты — это неотъемлемая часть кода, обеспечивающая его надежность, поддерживаемость и безопасный рефакторинг.

Основные принципы:

  1. Инструменты: Я использую стандартный пакет testing как основу. Для более удобных и читаемых проверок (assertions) предпочитаю библиотеку testify/assert или testify/require.

  2. Table-Driven Tests (табличные тесты): Это идиоматичный для Go способ организации тестов. Он позволяет легко описывать множество тестовых случаев, включая пограничные значения, в компактном и читаемом виде.

    import (
        "testing"
        "github.com/stretchr/testify/assert"
    )
    
    func TestCalculate(t *testing.T) {
        testCases := []struct {
            name    string // Имя теста
            a, b    int
            want    int
            wantErr bool   // Ожидаем ли ошибку
        }{
            {"positive numbers", 2, 3, 5, false},
            {"zero values", 0, 0, 0, false},
            {"negative number", -5, 3, -2, false},
            // Можно добавить кейс с ошибкой, если функция ее возвращает
        }
    
        for _, tc := range testCases {
            t.Run(tc.name, func(t *testing.T) {
                got, err := Calculate(tc.a, tc.b)
    
                if tc.wantErr {
                    assert.Error(t, err)
                } else {
                    assert.NoError(t, err)
                    assert.Equal(t, tc.want, got)
                }
            })
        }
    }
  3. Что я тестирую в первую очередь:

    • Критическая бизнес-логика: Функции, которые реализуют ключевые правила приложения.
    • Сложные алгоритмы: Функции с нетривиальной логикой.
    • Пограничные случаи: Нулевые значения, пустые строки, отрицательные числа, максимальные значения.
    • Обработка ошибок: Проверяю, что функция корректно возвращает и обрабатывает ошибки.
  4. Мокинг (Mocking) зависимостей: Для изоляции тестируемого кода от внешних зависимостей (базы данных, внешние API) я использую интерфейсы. В тестах я подменяю реальную реализацию на мок (mock-объект), который имитирует нужное поведение. Это можно делать вручную или с помощью библиотек вроде gomock или testify/mock.

  5. Покрытие кода (Code Coverage): Я использую go test -cover для анализа покрытия. Я стремлюсь к показателю в 70-80% для важной логики, но отношусь к нему как к инструменту, а не цели. 100% покрытие тривиальных геттеров/сеттеров бесполезно, в то время как 80% покрытие критического модуля — это отличный результат. Главное — уверенность в корректности кода.