Enhetstesting Succinctly Proving Correctness

Dette er et utdrag fra Unit Testing Succinctly eBook, av Marc Clifton, gitt vennlig av Syncfusion.

Uttrykket "å bevise korrekthet" brukes normalt i sammenheng med sannheten til en beregning, men med hensyn til enhetstesting har det vist seg å ha korrekt trekk i tre brede kategorier, hvorav den andre gjelder beregninger selv:

  • Verifisere at innganger til en beregning er korrekte (metodekontrakt).
  • Verifiserer at en metallsamtale resulterer i det ønskede beregningsresultatet (kalt beregningsaspektet), fordelt på fire typiske prosesser:
    • Datatransformasjon
    • Data reduksjon
    • Statens endring
    • Statlig korrekthet
  • Ekstern feilhåndtering og gjenoppretting.

Det er mange aspekter ved en applikasjon der enhetstesting vanligvis ikke kan brukes til å bevise korrekthet. Disse inkluderer de fleste brukergrensesnittfunksjoner som layout og brukervennlighet. Enhetstesting er i mange tilfeller ikke den riktige teknologien for testing av krav og applikasjonsadferd med hensyn til ytelse, last og så videre.


Hvordan Enhetstest viser korrekthet

Proving correctness involverer:

  • Verifiserer kontrakten.
  • Verifiserer beregningsresultater.
  • Verifiserer datatransformasjonsresultater.
  • Verifisering av eksterne feil håndteres riktig.

La oss se på noen eksempler på hver av disse kategoriene, deres styrker, svakheter og problemer som vi kan støte på med vår kode.

Bevislig kontrakt er implementert

Den mest grunnleggende form for enhetstesting er å verifisere at utvikleren har skrevet en metode som klart angir "kontrakten" mellom den som ringer og metoden som kalles. Dette tar vanligvis form av å verifisere at dårlige innganger til en metode resulterer i at et unntak kastes. For eksempel kan en "divide by" -metode kaste en ArgumentOutOfRangeException hvis nevneren er 0:

offentlig statisk int Divide (int teller, int nevner) hvis (nevner == 0) kaste ny ArgumentOutOfRangeException ("Neminator kan ikke være 0.");  retur teller / nevner;  [TestMethod] [ExpectedException (typeof (ArgumentOutOfRangeException)) offentlig tomgang BadParameterTest () Divide (5, 0); 

Men å verifisere at en metode utfører kontraktsforsøk er en av de svakeste enhetstester man kan skrive.

Bevis beregningsresultater

En sterkere enhetstest innebærer å verifisere at beregningen er riktig. Det er nyttig å kategorisere metodene dine i en av de tre beregningsformene:

  • Data reduksjon
  • Datatransformasjon
  • Statens endring

Disse bestemmer hvilke typer enhetstester du kanskje vil skrive for en bestemt metode.

Data Reduksjon

De Dele opp Metoden i den foregående prøven kan betraktes som en form for datareduksjon. Det tar to verdier og returnerer en verdi. Å illustrere:

[TestMethod] public void VerifyDivisionTest () Assert.IsTrue (Divide (6, 2) == 3, "6/2 skal være lik 3!"); 

Dette illustrerer å teste en metode som reduserer inngangene, vanligvis til en resulterende utgang. Dette er den enkleste form for nyttig enhetstesting.

Datatransformasjon

Datatransformasjonsenhetstester har en tendens til å fungere på verdisett. For eksempel er følgende en test for en metode som konverterer kartesiske koordinater til polarkoordinater.

statisk statisk dobbelt [] ConvertToPolarCoordinates (double x, double y) double dist = Math.Sqrt (x * x + y * y); dobbel vinkel = Math.Atan2 (y, x); returner ny dobbel [] dist, vinkel;  [TestMethod] public void ConvertToPolarCoordinatesTest () double [] pcoord = ConvertToPolarCoordinates (3, 4); Assert.IsTrue (pcoord [0] == 5, "Forventet avstand til lik 5"); Assert.IsTrue (pcoord [1] == 0,92729521800161219, "Forventet vinkel til 53.130 grader"); 

Denne testen verifiserer korrektheten av den matematiske transformasjonen.

Liste Transformasjoner

Liste transformasjoner skal skilles i to tester:

  • Kontroller at kjernetransformasjonen er riktig.
  • Verifiser at listen operasjonen er riktig.

For eksempel, fra ethetsprøvingens perspektiv, er følgende prøve dårlig skrevet fordi den inkorporerer både datareduksjonen og datatransformasjonen:

public struct Navn public string FirstName get; sett;  offentlig streng LastName get; sett;  offentlig liste ConcatNames (Liste navn) Liste concatenatedNames = ny liste(); foreach (Navn på navn i navn) concatenatedNames.Add (name.LastName + "," + name.FirstName);  returnere concatenatedNames;  [TestMethod] public void NameConcatenationTest () Liste navn = ny liste() nytt navn () FirstName = "John", LastName = "Travolta", nytt navn () FirstName = "Allen", LastName = "Nancy"; Liste newNames = ConcatNames (navn); Assert.IsTrue (newNames [0] == "Travolta, John"); Assert.IsTrue (newNames [1] == "Nancy, Allen"); 

Denne koden er bedre enhetstestet ved å skille datareduksjonen fra datatransformasjonen:

offentlig streng Concat (Navn) Return Name.LastName + "," + name.FirstName;  [TestMethod] public void ContactNameTest () Navnnavn = nytt Navn () FirstName = "John", LastName = "Travolta"; streng concatenatedName = Concat (navn); Assert.IsTrue (concatenatedName == "Travolta, John"); 

Lambda Expressions og Unit Tests

LINQ-syntaksen (Language Integrated Query) er nært koblet til lambda-uttrykk, noe som resulterer i en lettlest syntax som gjør livet vanskelig for enhetstesting. For eksempel, denne koden:

offentlig liste ConcatNamesWithLinq (Liste navn) return names.Select (t => t.LastName + "," + t.FirstName) .ToList (); 

er betydelig mer elegant enn de foregående eksemplene, men det gir seg ikke godt til enhetstesting av den faktiske "enheten", det vil si datareduksjonen fra en navnestruktur til en enkelt kommaseparert streng uttrykt i lambda-funksjonen t => t.LastName + "," + t.FirstName. For å skille enheten fra listen operasjonen krever:

offentlig liste ConcatNamesWithLinq (Liste navn) return names.Select (t => Concat (t)). ToList (); 

Vi kan se at enhetstesting ofte kan kreve refactoring av koden for å skille enhetene fra andre transformasjoner.

Statendring

De fleste språk er "stateful", og klasser klarer ofte tilstand. Statens klasse, representert ved sine egenskaper, er ofte en nyttig ting å teste. Vurder denne klassen som representerer konseptet for en forbindelse:

offentlig klasse AlreadyConnectedToServiceException: ApplicationException public AlreadyConnectedToServiceException (strengmelding): base (msg)  offentlig klasse ServiceConnection public bool Connected get; beskyttet sett;  offentlig ugyldig Koble () hvis (Tilkoblet) Kast nytt AlreadyConnectedToServiceException ("Bare en tilkobling om gangen er tillatt.");  // Koble til tjenesten. Connected = true;  Offentlig tomgang Koble fra () // Koble fra tjenesten. Connected = false; 

Vi kan skrive enhetstester for å verifisere de ulike tillatte og uopprettede tilstandene til objektet:

[TestClass] offentlig klasse ServiceConnectionFixture [TestMethod] offentlig tomgang TestInitialState () ServiceConnection conn = new ServiceConnection (); Assert.IsFalse (conn.Connected);  [TestMethod] public void TestConnectedState () ServiceConnection conn = ny ServiceConnection (); conn.Connect (); Assert.IsTrue (conn.Connected);  [TestMethod] public void TestDisconnectedState () ServiceConnection conn = ny ServiceConnection (); conn.Connect (); conn.Disconnect (); Assert.IsFalse (conn.Connected);  [TestMethod] [ExpectedException (typeof (AlreadyConnectedToServiceException)) offentlig tomgang TestAlreadyConnectedException () ServiceConnection conn = new ServiceConnection (); conn.Connect (); conn.Connect (); 

Her verifiserer hver test korrektheten av objektets tilstand:

  • Når den er initialisert.
  • Når du blir bedt om å koble til tjenesten.
  • Når du blir bedt om å koble fra tjenesten.
  • Når flere enn en samtidig tilkobling er forsøkt.

Statlig bekreftelse avslører ofte feil i statlig ledelse. Se også følgende "Mocking Classes" for ytterligere forbedringer av foregående eksempelkoden.

Bevis en metode håndterer en ekstern unntak

Ekstern feilhåndtering og gjenoppretting er ofte viktigere enn å teste om din egen kode genererer unntak på de riktige tidspunktene. Det er flere grunner til dette:

  • Du har ingen kontroll over en fysisk separat avhengighet, enten det er en webtjeneste, en database eller en annen separat server.
  • Du har ikke bevis på korrektheten til andres kode, vanligvis et tredjepartsbibliotek.
  • Tredjeparts tjenester og programvare kan kaste et unntak på grunn av et problem som koden din oppretter, men ikke oppdager (og vil ikke nødvendigvis være lett å oppdage). Et eksempel på dette er at når du sletter poster i en database, kaster databasen et unntak på grunn av poster i andre tabeller som refererer til postene programmet ditt sletter, og dermed krenker en utenlandsk nøkkelbegrensning.

Disse unntakene er vanskelige å teste fordi de krever å skape minst en feil som vanligvis ville genereres av tjenesten du ikke kontrollerer. En måte å gjøre dette på er å "tåle" tjenesten; Dette er imidlertid bare mulig hvis det eksterne objektet er implementert med et grensesnitt, en abstrakt klasse eller virtuelle metoder.

Mocking Classes

For eksempel er den tidligere koden for tjenesten "ServiceConnection" ikke mockable. Hvis du vil teste sin statlige ledelse, må du fysisk opprette en forbindelse til tjenesten (hva som helst) som kanskje eller ikke er tilgjengelig når du kjører enhetstester. En bedre implementering kan se slik ut:

offentlig klasse MockableServiceConnection public bool Connected get; beskyttet sett;  beskyttet virtuelt void ConnectToService () // Koble til tjenesten.  beskyttet virtuelt tomrom DisconnectFromService () // Koble fra tjenesten.  offentlig ugyldig Koble () hvis (Tilkoblet) Kast nytt AlreadyConnectedToServiceException ("Bare en tilkobling om gangen er tillatt.");  ConnectToService (); Connected = true;  offentlig tomgang Koble fra () DisconnectFromService (); Connected = false; 

Legg merke til hvordan denne mindre refactoring nå lar deg skrive en mock klasse:

offentlig klasse ServiceConnectionMock: MockableServiceConnection protected override void ConnectToService () // Gjør ingenting.  beskyttet overstyring ugyldig DisconnectFromService () // Ikke gjør noe. 

som lar deg skrive en enhetstest som tester statlig ledelse uavhengig av tilgjengeligheten av tjenesten. Som dette illustrerer, kan selv enkle arkitektoniske eller implementasjonsendringer i stor grad forbedre testbarheten til en klasse.

Bevis en feil er re-creatable

Din første forsvarslinje for å bevise at problemet er blitt korrigert, viser ironisk nok at problemet eksisterer. Tidligere så vi et eksempel på å skrive en test som viste at Divide-metoden sjekker for en nevnerverdi av 0. La oss si at en feilrapport er arkivert fordi en bruker krasjer programmet når du skriver inn 0 for nevnerverdien.

Negativ testing

Den første rekkefølgen er å lage en test som utøver denne tilstanden:

[TestMethod] [ExpectedException (typeof (DivideByZeroException)) offentlig tomrom BadParameterTest () Divide (5, 0); 

Denne testen passerer fordi vi beviser at feilen eksisterer ved å verifisere at når nevnen er 0, en DivideByZeroException er hevet. Disse typer tester betraktes som "negative tester" som de passere når det oppstår en feil. Negativ testing er like viktig som positiv testing (diskutert neste) fordi den bekrefter eksistensen av et problem før det er korrigert.

Bevis en feil er fast

Tydeligvis vil vi bevise at en feil har blitt løst. Dette er en "positiv" test.

Positiv testing

Vi kan nå introdusere en ny test, en som vil teste at koden selv oppdager feilen ved å kaste en ArgumentOutOfRangeException.

[TestMethod] [ExpectedException (typeof (ArgumentOutOfRangeException)) offentlig tomgang BadParameterTest () Divide (5, 0); 

Hvis vi kan skrive denne testen før Å fikse problemet, vil vi se at testen mislykkes. Til slutt, etter å fikse problemet, går vår positive test, og den negative testen feiler nå.

Selv om dette er et trivielt eksempel, demonstrerer det to begreper:

  • Negative tester som viser at noe gjentatte ganger ikke virker, er viktig for å forstå problemet og løsningen.
  • Positive tester, som viser at problemet er løst, er viktig, ikke bare for å verifisere løsningen, men også for å gjenta testen når en endring er gjort. Enhetstesting spiller en viktig rolle når det gjelder regresjonstesting.

Til slutt er det ikke alltid lett å bevise at det finnes en feil. Som en generell tommelfingerregel er enhetstester som krever for mye oppsett og mocking en indikator på at koden som blir testet ikke er isolert nok fra eksterne avhengigheter, og kan være en kandidat for refactoring.

Bevis ingenting ødelagt når du endrer kode

Det bør være klart at regresjonstesting er et målbart nyttig utfall av enhetstesting. Som kode gjennomgår endringer, vil det bli introdusert feil som vil bli avslørt dersom du har god kode dekning i enhetstester. Dette sparer betydelig tid i feilsøking, og enda viktigere, sparer tid og penger når programmereren oppdager feilen i stedet for brukeren.

Bevisbehov er oppfylt

Applikasjonsutvikling starter vanligvis med et krav på høyt nivå, vanligvis orientert rundt brukergrensesnitt, arbeidsflyt og beregninger. Ideelt reduserer teamet synlig sett av krav ned til et sett med programmatiske krav, som er usynlig til brukeren, av sin natur.

Forskjellen manifesterer seg i hvordan programmet testes. Integrasjonstesting er vanligvis på synlig nivå, mens enhetstesting er på det finere kornet av usynlig, programmatisk korrekthetstesting. Det er viktig å huske på at enhetstester ikke er ment å erstatte integreringstesting; Likevel, som på høyt nivå på søknadskrav, er det lavt programmatiske krav som kan defineres. På grunn av disse programmatiske kravene er det viktig å skrive enhetstester.

La oss ta en rund metode. .NET Math.Round-metoden vil runde opp et tall hvis brøkdelskomponent er større enn 0,5, men vil runde ned når brøkkomponenten er 0,5 eller mindre. La oss si at det ikke er oppførselen vi ønsker (uansett grunn), og vi vil rulle opp når den delte delen er 0,5 eller høyere. Dette er et beregningsbehov som skal kunne utledes av et høyere krav til integrering, noe som resulterer i følgende metode og test:

offentlig statisk int RoundUpHalf (double n) if (n < 0) throw new ArgumentOutOfRangeException("Value must be >= 0); int ret = (int) n; dobbeltfraksjon = n - rett; hvis (fraksjon> = 0,5) ++ ret; Retur; [TestMethod] offentlig tomgang RoundUpTest () int result1 = RoundUpHalf (1.5); int result2 = RoundUpHalf (1.499999); Assert.IsTrue (result1 == 2, "Forventet 2."); Assert.IsTrue (result2 == 1, "Forventet 1.");

En separat test for unntaket skal også skrives.

Å ta krav på applikasjonsnivå som er verifisert med integreringstesting og redusere dem til lavere krav til beregning er en viktig del av den samlede enhetsteststrategien, da den definerer klare beregningskrav som søknaden må oppfylle. Hvis det oppstår problemer med denne prosessen, kan du prøve å konvertere programkravene til en av de tre beregningskategoriene: datareduksjon, datatransformasjon og tilstandsendring.