Gå til beste praksis - Testing

De første dagene av min programmeringskarriere så jeg ikke virkelig verdien og tenkte hovedsakelig at det dupliserte arbeidet. Nå sikter jeg imidlertid vanligvis på 90-100% testdekning på alt jeg skriver. Og jeg tror generelt at testing på hvert lag er en god praksis (vi kommer tilbake til dette).

Når jeg ser på kodebasene jeg har foran meg daglig, er de som jeg frykter mest å endre, de som har minst testdekning. Og det reduserer til slutt min produktivitet og leveranser. Så for meg er det ganske tydelig at høy testdekning både tilsvarer høyere kvalitet og høyere produktivitet.

Testing på hvert lag

Vi vil dykke ned i et eksempel med en gang. Anta at du har en app med følgende struktur.

Søknadsmodell

Det er noen delte komponenter, for eksempel modeller og håndtere. Da har du et par forskjellige måter å samhandle med denne applikasjonen, f.eks. CLI, HTTP API eller Thrift RPCer. Jeg har funnet det som en god praksis å sørge for at du tester ikke bare modellene eller bare håndtererne, men alle dem. Selv for den samme funksjonen. Fordi det er nødvendigvis sant at hvis du har implementert støtte for Feature X i behandleren, at den faktisk er tilgjengelig via HTTP og Thrift grensesnitt for eksempel.

Dette var du vil være mer trygg på å gjøre endringer i logikken din, selv innerst inne i kjernen av applikasjonen.

Tabellbaserte tester

I nesten alle tilfeller når du tester en metode, vil du teste et par scenarier på funksjonen. Vanligvis med forskjellige inndataparametere eller forskjellige mock-svar. Jeg liker å gruppere alle disse testene i en Test * -funksjon og deretter ha en sløyfe som går gjennom alle testtilfellene. Her er et grunnleggende eksempel:

func TestDivision (t * testing.T) {
    tester: = [] struktur {
        x float64
        du flyter64
        resultat float64
        feilfeil
    } {
        {x: 1,0, y: 2,0, resultat: 0,5, feil: null},
        {x: -1,0, y: 2,0, resultat: -0,5, feil: null},
        {x: 1.0, y: 0.0, resultat: 0.0, err: ErrZeroDivision},
    }
    for _, test: = rekkevidde tester {
        resultat, feil: = dele (test.x, test.y)
        påstå.IsType (t, test.err, err)
        påstå.Effal (t, test.resultat, resultat)
    }
}

Testene ovenfor dekker ikke alt, men tjener som et eksempel for hvordan du kan teste for forventede resultater og feil. Ovenstående kode bruker også den store vitnemålspakken for påstander.

En forbedring, tabellbaserte tester med navngitte testtilfeller

Hvis du har mange tester eller ofte nye utviklere som ikke er kjent med kodebasen, kan det være nyttig å navngi testene dine. Her er et kort eksempel på hvordan det vil se ut

tester: = kart [streng] struktur {
    nummer int
    smsErr feil
    feilfeil
} {
    "vellykket": {0132423444, nil, nul},
    "propagates error": {0132423444, sampleErr, sampleErr},
}

Legg merke til at det her er forskjell mellom å ha et kart og en skive. Kartet garanterer ikke orden, mens skiven gjør det.

Spott ved hjelp av hån

Grensesnitt er naturlig nok super gode integrasjonspunkter for tester, siden implementeringen av et grensesnitt lett kan erstattes av en hånlig implementering. Å skrive spotter kan imidlertid være ganske kjedelig og kjedelig. For å gjøre livet enklere bruker jeg hån for å generere mockene mine basert på et gitt grensesnitt.

La oss se på hvordan vi kan jobbe med det. Anta at vi har følgende grensesnitt.

skriv inn SMS-grensesnitt {
    Send (nummer int, tekststreng) feil
}

Her er en dummy-implementering som bruker dette grensesnittet:

// Messager er en struktur som håndterer meldinger av forskjellige typer.
type Messager struct {
    sms sms
}
// SendHelloWorld sender en Hello world SMS.
func (m * Messager) SendHelloWorld (nummer int) feil {
    feil: = m.sms.Send (nummer, "Hei, verden!")
    hvis feil! = null {
        retur feil
    }
    tilbake null
}

Vi kan nå bruke Mockery til å generere en hån for SMS-grensesnittet. Slik ser det ut (dette eksemplet bruker -inpkg-flagget som setter spottet i samme pakke som grensesnittet).

// MockSMS er en autogenert mock-type for SMS-typen
type MockSMS struct {
    mock.Mock
}
// Send gir en hånsfunksjon med gitte felt: nummer, tekst
func (_m * MockSMS) Send (nummer int, tekststreng) feil {
    ret: = _m.Called (nummer, tekst)
    var r0 feil
    hvis rf, ok: = ret.Get (0). (func (int, streng) feil); ok {
        r0 = rf (antall, tekst)
    } annet {
        r0 = ret.Error (0)
    }
    retur r0
}
var _ SMS = (* MockSMS) (null)

SMS strukturen arver fra vitne mock.Mock, som gir oss noen interessante alternativer når vi skriver testsakene. Så nå er det på tide å skrive testen vår for SendHelloWorld-metoden ved å bruke mocken fra Mockery.

func TestSendHelloWorld (t * testing.T) {
    sampleErr: = error.New ("noen feil")
    tester: = kart [streng] struktur {
        nummer int
        smsErr feil
        feilfeil
    } {
        "vellykket": {0132423444, nil, nul},
        "propagates error": {0132423444, sampleErr, sampleErr},
    }
    for _, test: = rekkevidde tester {
        sms: = & MockSMS {}
        sms.On ("Send", testnummer, "Hallo, verden!"). Retur (test.smsErr). En gang ()
        m: = & Messager {
            sms: sms,
        }
   
        feil: = m.SendHelloWorld (testnummer)
        påstå.Kvalitet (t, test.err, err)
        sms.AssertExpectations (t)
    }
}

Det er et par punkter som er verdt å nevne i eksemplet ovenfor. I testen vil du legge merke til at jeg direkte MockSMS og deretter bruker. På () kan jeg diktere hva som skal skje (.Return ()) når bestemte parametere sendes til mocken.

Til slutt bruker jeg sms.AssertExpectations for å sikre at SMS-grensesnittet har blitt kalt det forventede antall ganger. I dette tilfellet En gang ().

Alle filene ovenfor finner du i denne essensen.

Golden file-tester

I noen tilfeller har jeg funnet det nyttig å bare kunne hevde at en stor responsklump forblir den samme. Kan for eksempel være data som returneres fra en JSON-data fra en API. For det tilfellet lærte jeg fra Michell Hashimoto om bruken av gyldne filer kombinert med en smart var å avsløre kommandolinjeflagg for å prøve.

Den grunnleggende ideen er at du vil skrive riktig svarorgan til en fil (den gyldne filen). Når du kjører testene, gjør du en bytesammenligning mellom den gyldne filen og testresponsen.

For å gjøre det enklere har jeg laget goldie-pakken, som håndterer innstillingen for kommandolinjeflagg og skriving og sammenligning av gyldne filer på en transparent måte.

Her er et eksempel på hvordan du bruker goldie til denne typen testing:

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

    req, err: = http.NewRequest ("GET", "/ eksempel", null)
    hevde.Nil (t, feil)

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

    goldie.Assert (t, "eksempel", opptaker.Body.Bytes ())
}

Når du trenger å oppdatere den gyldne filen din, ville du kjøre følgende:

gå test-oppdatert. / ...

Og når du bare vil kjøre testene, vil du gjøre det som vanlig:

gå test. / ...

Ha det!

Takk for at du holdt deg til slutten! Håper du har funnet noe nyttig i artikkelen.