Найкращі практики - тестування

Я в перші дні моєї кар’єри програмування не дуже бачив цінності, і головним чином вважав, що це дублює роботу. Тепер я, як правило, прагну 90-100% покриття тесту на все, що пишу. Я, як правило, вважаю, що тестування кожного шару - це хороша практика (ми повернемося до цього).

Насправді, дивлячись на кодові бази, які я маю перед собою щодня, ті, кого я боюся найбільше змінити, - це ті, що мають найменше охоплення тестом. І це, в кінцевому рахунку, знижує мою продуктивність та наші результати. Тож мені цілком зрозуміло, що високий рівень тестового покриття дорівнює як більш високій якості, так і більш високій продуктивності.

Тестування на кожному шарі

Ми відразу зануримось у приклад. Припустимо, у вас є додаток із такою структурою.

Модель програми

Є деякі спільні компоненти, наприклад, моделі та обробники. Тоді у вас є кілька різних способів взаємодії з цим додатком, наприклад CLC, HTTP API або Thrift RPC. Я вважаю гарною практикою переконатися, що ви протестуєте не тільки моделі чи лише оброблювачі, але й усі. Навіть за тією ж особливістю. Оскільки, але обов'язково вірно, що якщо ви впровадили підтримку Feature X в обробнику, вона фактично доступна, наприклад, через інтерфейси HTTP та Thrift.

Це ви будете більш впевнено вносити зміни до своєї логіки, навіть глибоко в глибину ядра програми.

Табличні тести

Майже у всіх випадках, коли ви тестуєте метод, ви хочете перевірити пару сценаріїв функції. Зазвичай з різними вхідними параметрами або різними макетними відповідями. Мені подобається групувати всі ці тести в одну функцію Test *, а потім мати цикл, який проходить через усі тестові випадки. Ось основний приклад:

func TestDivision (t * testing.T) {
    тести: = [] структура {
        x float64
        y float64
        результат float64
        помилка помилки
    } {
        {x: 1.0, y: 2.0, результат: 0.5, помилка: nil},
        {x: -1.0, y: 2.0, результат: -0.5, помилка: nil},
        {x: 1.0, y: 0.0, результат: 0.0, помилка: ErrZeroDivision},
    }
    для _, тест: = тести діапазону {
        результат, помилка: = ділити (test.x, test.y)
        assert.IsType (t, test.err, помилка)
        assert.Equal (t, тест.результат, результат)
    }
}

Наведені вище тести охоплюють не все, але слугують прикладом для тестування на очікувані результати та помилки. Вищевказаний код також використовує великий пакет свідчень для тверджень.

Розширення, тестові тести з названими тестовими кейсами

Якщо у вас є багато тестів або часто нові розробники, які не знайомі з кодовою базою, вам може бути корисно назвати ваші тести. Ось короткий приклад того, як це могло б виглядати

тести: = map [string] struct {
    число int
    Помилка smsErr
    помилка помилки
} {
    "успішний": {0132423444, nil, nil},
    "поширює помилку": {0132423444, sampleErr, sampleErr},
}

Зауважте, що тут є різниця між наявністю карти та фрагмента. Карта не гарантує замовлення, в той час як фрагмент це робить.

Знущання за допомогою насмішок

Інтерфейси, природно, дуже хороші точки інтеграції для тестів, оскільки реалізацію інтерфейсу можна легко замінити макетною реалізацією. Однак писати макети може бути досить нудно і нудно. Щоб полегшити життя, я використовую глузування, щоб генерувати свої знущання на основі заданого інтерфейсу.

Давайте подивимось, як з цим працювати. Припустимо, у нас є наступний інтерфейс.

введіть SMS-інтерфейс {
    Помилка надсилання (число int, текстовий рядок)
}

Ось фіктивна реалізація за допомогою цього інтерфейсу:

// Messager - це структура обробки обміну повідомленнями різних типів.
введіть структуру Messager {
    sms SMS
}
// SendHelloWorld надсилає SMS Hello Hello.
func (m * Messager) SendHelloWorld (число int) помилка {
    помилка: = m.sms.Send (число, "Привіт, світ!")
    якщо помилка! = нуль {
        помилка повернення
    }
    повернути нуль
}

Тепер ми можемо використовувати Mockery для створення макету для інтерфейсу SMS. Ось як це могло б виглядати (у цьому прикладі використовується прапор -inpkg, який ставить макет у той самий пакет, що й інтерфейс).

// MockSMS - це автогенерований тип макету для типу SMS
введіть структуру MockSMS {
    глузувати
}
// Надіслати надає функцію макету із заданими полями: число, текст
func (_m * MockSMS) Помилка надсилання (число int, текстовий рядок) {
    ret: = _m.Called (число, текст)
    помилка var r0
    якщо rf, ok: = ret.Get (0). (func (int, string) помилка); добре {
        r0 = rf (число, текст)
    } else {
        r0 = ret.Error (0)
    }
    повернути r0
}
var _ SMS = (* MockSMS) (нуль)

Структура SMS успадковує від свідчення mock.Mock, який дає нам кілька цікавих варіантів при написанні тестових випадків. Отже, зараз саме час написати наш тест на метод SendHelloWorld за допомогою макету з насмішки.

func TestSendHelloWorld (t * testing.T) {
    sampleErr: = errors.New ("деяка помилка")
    тести: = map [string] struct {
        число int
        Помилка smsErr
        помилка помилки
    } {
        "успішний": {0132423444, nil, nil},
        "поширює помилку": {0132423444, sampleErr, sampleErr},
    }
    для _, тест: = тести діапазону {
        sms: = & MockSMS {}
        sms.On ("Надіслати", test.number, "Привіт, світ!"). Повернення (test.smsErr) .Once ()
        m: = & Messager {
            sms: sms,
        }
   
        помилка: = m.SendHelloWorld (тестовий номер)
        assert.Equal (t, test.err, помилка)
        sms.AssertExpeptions (t)
    }
}

У наведеному вище прикладі коду варто згадати кілька моментів. У тесті ви помітите, що я створюю інстанцію MockSMS, а потім використовую.

Нарешті, я використовую sms.AssertExpeptions, щоб переконатися, що інтерфейс SMS був викликаний очікуваною кількість разів. У цьому випадку один раз ().

Усі файли вище можна знайти в цьому суті.

Золоті тести на файли

У деяких випадках мені здається корисним лише твердити, що велика крапля у відповідь залишається такою ж. Можуть, наприклад, бути дані, повернуті з даних JSON з API. У такому випадку я дізнався від Мішелла Хашимото про використання золотих файлів у поєднанні зі смарт-викриттям прапорів командного рядка для проходження тестування.

Основна ідея полягає в тому, щоб ви написали правильний орган відповіді у файл (золотий файл). Потім під час запуску тестів ви виконуєте порівняння байтів між золотим файлом та тестовою відповіддю.

Для того, щоб було простіше, я створив пакет goldie, який прозоро обробляє налаштування прапора командного рядка та золоте написання та порівняння файлів.

Ось приклад того, як використовувати goldie для цього виду тестування:

func TestExample (t * testing.T) {
    рекордер: = httptest.NewRecorder ()

    req, err: = http.NewRequest ("GET", "/ example", nil)
    assert.Nil (t, помилка)

    обробник: = http.HandlerFunc (ExampleHandler)
    обробник.ServeHTTP ()

    goldie.Assert (t, "example", Recorder.Body.Bytes ())
}

Коли вам потрібно оновити золотий файл, виконайте такі дії:

пройти тест - оновлення. / ...

І коли ви просто хочете запустити тести, зробіть це як завжди:

пройти тест. / ...

Бувай!

Дякуємо, що дотримувались кінця! Сподіваємось, ви знайшли щось корисне у статті.