Go Best Practices - Testen

Ik zag de waarde van de eerste dagen van mijn programmeercarrière niet echt in en dacht vooral dat het dubbel werk was. Nu streef ik echter meestal naar 90-100% testdekking op alles wat ik schrijf. En ik ben over het algemeen van mening dat testen op elke laag een goede gewoonte is (we komen hier op terug).

Als ik naar de codebases kijk die ik dagelijks voor me heb, zijn degenen die het meest bang zijn om te veranderen, degenen met de minste testdekking. En dat vermindert uiteindelijk mijn productiviteit en onze deliverables. Voor mij is het vrij duidelijk dat een hoge testdekking zowel een hogere kwaliteit als een hogere productiviteit betekent.

Testen op elke laag

We zullen meteen in een voorbeeld duiken. Stel dat u een app hebt met de volgende structuur.

Applicatiemodel

Er zijn enkele gedeelde componenten, zoals de modellen en de handlers. Dan hebt u een aantal verschillende manieren om met deze applicatie te communiceren, b.v. CLI, HTTP API of Thrift RPC's. Ik vond het een goede gewoonte om ervoor te zorgen dat je niet alleen de modellen of alleen de handlers test, maar allemaal. Zelfs voor dezelfde functie. Omdat het maar noodzakelijkerwijs waar is dat als je ondersteuning voor Feature X in de handler hebt geïmplementeerd, het feitelijk beschikbaar is via bijvoorbeeld de HTTP- en Thrift-interfaces.

Dit was dat u meer vertrouwen zult hebben in het wijzigen van uw logica, zelfs diep in de kern van de applicatie.

Op tafel gebaseerde tests

In bijna alle gevallen waarin u een methode test, wilt u een paar scenario's over de functie testen. Meestal met verschillende invoerparameters of verschillende nepreacties. Ik groepeer al deze tests graag in één Test * -functie en laat vervolgens een lus door alle testgevallen lopen. Hier is een basisvoorbeeld:

func TestDivision (t * testing.T) {
    tests: = [] struct {
        x float64
        y float64
        resultaat float64
        fout
    } {
        {x: 1.0, y: 2.0, resultaat: 0,5, fout: nul},
        {x: -1.0, y: 2.0, resultaat: -0.5, err: nil},
        {x: 1.0, y: 0.0, resultaat: 0.0, fout: ErrZeroDivision},
    }
    voor _, test: = bereiktests {
        resultaat, err: = delen (test.x, test.y)
        assert.IsType (t, test.err, err)
        assert.Equal (t, test.resultaat, resultaat)
    }
}

De bovenstaande tests dekken niet alles, maar dienen als een voorbeeld voor het testen op verwachte resultaten en fouten. De bovenstaande code gebruikt ook het geweldige testify-pakket voor beweringen.

Een verbeterde, op tabellen gebaseerde tests met op naam gestelde testgevallen

Als u veel tests hebt of vaak nieuwe ontwikkelaars die niet bekend zijn met de codebasis, kan het handig zijn om uw tests een naam te geven. Hier is een kort voorbeeld van hoe dat eruit zou zien

tests: = map [string] struct {
    nummer int
    smsErr-fout
    fout
} {
    "succesvol": {0132423444, nihil, nihil},
    "propageert fout": {0132423444, sampleErr, sampleErr},
}

Merk op dat er hier een verschil is tussen het hebben van een kaart en een segment. De kaart biedt geen garantie voor de volgorde, terwijl het segment dat wel doet.

Bespotten met spot

Interfaces zijn natuurlijk super goede integratiepunten voor tests, omdat de implementatie van een interface gemakkelijk kan worden vervangen door een nep-implementatie. Het schrijven van mocks kan echter behoorlijk vervelend en saai zijn. Om het leven gemakkelijker te maken, gebruik ik mockery om mijn mocks te genereren op basis van een bepaalde interface.

Laten we eens kijken hoe we daarmee kunnen werken. Stel dat we de volgende interface hebben.

type sms-interface {
    Verzendfout (nummer int, tekstreeks)
}

Hier is een dummy-implementatie met behulp van deze interface:

// Messager is een struct die verschillende soorten berichten afhandelt.
type Messager struct {
    sms sms
}
// SendHelloWorld verzendt een Hallo wereld-sms.
func (m * Messager) SendHelloWorld (number int) fout {
    err: = m.sms.Send (nummer, "Hallo wereld!")
    if err! = nil {
        retour fout
    }
    retour nul
}

We kunnen nu Mockery gebruiken om een ​​mock voor de SMS-interface te genereren. Hier is hoe dat eruit zou zien (dit voorbeeld gebruikt de vlag -inpkg die de mock in hetzelfde pakket plaatst als de interface).

// MockSMS is een automatisch gegenereerd spottype voor het SMS-type
type MockSMS struct {
    mock.Mock
}
// Verzenden biedt een mock-functie met bepaalde velden: nummer, tekst
func (_m * MockSMS) Verzend (nummer int, tekstreeks) fout {
    ret: = _m.Called (nummer, tekst)
    var r0-fout
    if rf, ok: = ret.Get (0). (func (int, string) fout); OK {
        r0 = rf (nummer, tekst)
    } anders {
        r0 = ret.Error (0)
    }
    retourneer r0
}
var _ SMS = (* MockSMS) (nul)

De SMS-structuur is erfelijk van testify mock.Mock, wat ons een aantal interessante opties geeft bij het schrijven van de testcases. Het is nu tijd om onze test voor de SendHelloWorld-methode te schrijven met de mock van Mockery.

func TestSendHelloWorld (t * testing.T) {
    sampleErr: = errors.New ("some error")
    tests: = map [string] struct {
        nummer int
        smsErr-fout
        fout
    } {
        "succesvol": {0132423444, nihil, nihil},
        "propageert fout": {0132423444, sampleErr, sampleErr},
    }
    voor _, test: = bereiktests {
        sms: = & MockSMS {}
        sms.On ("Verzenden", test.number, "Hallo wereld!"). Return (test.smsErr) .Once ()
        m: = & Messager {
            sms: sms,
        }
   
        err: = m.SendHelloWorld (test.number)
        assert.Equal (t, test.err, err)
        sms.AssertExpectations (t)
    }
}

Er zijn een paar punten die het vermelden waard zijn in de bovenstaande voorbeeldcode. In de test merk je dat ik MockSMS instantieer en vervolgens .On () gebruik. Ik kan dicteren wat er moet gebeuren (.Return ()) wanneer bepaalde parameters naar de mock worden verzonden.

Eindelijk gebruik ik sms.AssertExpectations om ervoor te zorgen dat de sms-interface het verwachte aantal keren is genoemd. In dit geval Once ().

Alle bovenstaande bestanden zijn te vinden in deze kern.

Gouden bestandstests

In sommige gevallen vond ik het handig om gewoon te kunnen beweren dat een grote respons-blob hetzelfde blijft. Dit kunnen bijvoorbeeld gegevens zijn die worden geretourneerd door JSON-gegevens vanuit een API. In dat geval leerde ik van Michell Hashimoto over het gebruik van gouden bestanden in combinatie met een smart van het blootstellen van opdrachtregelvlaggen om te gaan testen.

Het basisidee is dat u het juiste antwoordgedeelte naar een bestand schrijft (het gouden bestand). Vervolgens voert u bij het uitvoeren van de tests een bytevergelijking uit tussen het gouden bestand en de testrespons.

Om het u gemakkelijker te maken, heb ik het goldie-pakket gemaakt, dat de opdrachtregelvlaginstelling en het schrijven en vergelijken van gouden bestanden transparant verwerkt.

Hier is een voorbeeld van het gebruik van Goldie voor dit soort testen:

func TestExample (t * testing.T) {
    recorder: = httptest.NewRecorder ()

    req, err: = http.NewRequest ("GET", "/ example", nihil)
    assert.Nil (t, err)

    handler: = http.HandlerFunc (ExampleHandler)
    handler.ServeHTTP ()

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

Wanneer u uw gouden bestand moet bijwerken, voert u het volgende uit:

ga testen -update. / ...

En als u gewoon de tests wilt uitvoeren, doet u dat zoals gewoonlijk:

ga testen. / ...

Tot ziens!

Bedankt voor het blijven hangen! Ik hoop dat je iets nuttigs hebt gevonden in het artikel.