Отидете най-добри практики - тестване

В първите дни на моята програмна кариера наистина не виждах стойността и главно смятах, че тя дублира работата. Сега обаче обикновено се стремя към 90-100% тестово покритие на всичко, което пиша. И като цяло вярвам, че тестването на всеки слой е добра практика (ще се върнем към това).

Всъщност, когато разглеждам кодовите бази, които имам пред себе си всеки ден, тези, от които се страхувам най-много да се променят, са тези с най-малко покритие на теста. И това в крайна сметка намалява моята производителност и нашите резултати. Така че за мен е доста ясно, че високото покритие на тестовете е равно на по-високо качество и по-висока производителност.

Тестване на всеки слой

Веднага ще се потопим в пример. Да предположим, че имате приложение със следната структура.

Модел на приложение

Има някои споделени компоненти, като моделите и манипулаторите. След това имате няколко различни начина за взаимодействие с това приложение, напр. CLI, 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 World.
func (m * Messager) Грешка в SendHelloWorld (число int) {
    грешка: = m.sms.Send (номер, "Здравей, свят!")
    ако греши! = нула {
        грешка при връщане
    }
    връщане нула
}

Вече можем да използваме Mockery за генериране на макет за SMS интерфейса. Ето как би изглеждало това (в този пример се използва флагът -inpkg, който поставя макета в същия пакет като интерфейса).

// MockSMS е автогенериран тип макет за типа SMS
тип структура MockSMS {
    mock.Mock
}
// Send изпраща макетна функция с дадени полета: номер, текст
func (_m * MockSMS) Грешка при изпращане (номер int, текстов низ) {
    ret: = _m.Повикан (номер, текст)
    грешка вар r0
    ако rf, ok: = ret.Get (0). (func (int, string) грешка); Добре {
        r0 = rf (число, текст)
    } else {
        r0 = ret.Error (0)
    }
    връщане r0
}
var _ SMS = (* MockSMS) (нула)

Структурата на SMS наследява от mock.Mock, което ни дава няколко интересни опции при писането на тестовите случаи. И така, сега е време да напишем нашия тест за метода SendHelloWorld, използвайки макета от Mockery.

func TestSendHelloWorld (t * testing.T) {
    sampleErr: = грешки.Нови ("някаква грешка")
    тестове: = map [string] struct {
        номер int
        smsErr грешка
        грешка грешка
    } {
        "успешен": {0132423444, nil, nil},
        "разпространява грешка": {0132423444, sampleErr, sampleErr},
    }
    за _, тест: = тестове за обхват {
        sms: = & MockSMS {}
        sms.On ("Изпращане", тест. номер, "Здравей, свят!"). Връщане (test.smsErr) .Once ()
        m: = & Messager {
            sms: sms,
        }
   
        грешка: = m.SendHelloWorld (тест.брой)
        assert.Equal (t, test.err, грешка)
        sms.AssertExpectations (т)
    }
}

Има няколко точки, които си струва да се споменат в горния примерен код. В теста ще забележите, че създавам MockSMS и след това използвам .On () мога да диктувам какво трябва да се случи (.Return ()), когато определени параметри са изпратени към макета.

Най-накрая използвам 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)
    handler.ServeHTTP ()

    goldie.Assert (t, "пример", рекордер.Body.Bytes ())
}

Когато трябва да актуализирате своя златен файл, изпълнете следното:

отидете на тест-актуализация. / ...

И когато просто искате да стартирате тестовете, правите това както обикновено:

отидете на тест. / ...

Чао чао!

Благодаря, че се придържате към края! Надявам се, че сте намерили нещо полезно в статията.