lørdag den 15. september 2007

God unittest?

Det er ikke så frygtelig lang tid siden, at alle talte om unittest, men ingen lavede dem. Nu er det som om, at der ikke rigtigt er nogen som taler om dem længere, men alle laver dem. Er det udtryk for, at unittests generelt har nået en modenhed, som gør, at der ikke er mere at sige om dem? Nok desværre ikke. Men hvad er det så for diskussioner som er blevet væk?

Det første gode spørgsmål, som man kan stille sig selv er, hvor stor en unit til en unittest? Der kan der siges meget klogt om, men svaret på det må være, at det er så meget som det giver mening at teste selvstændigt.

Hvor meget?

Det er oplagt, at kompleksiteten af de små enheder, er væsentligt mindre end større helheder, og at kompleksiteten stiger voldsomt selv ved små forøgelser at størrelsen. Det er derfor en god ide at starte i det små, og verificere de enkelte dele inden man går videre og verificere at man kan koble dem sammen i større og større strukturer og systemer. Ved at benytte sig af, at man kan antage, at delene allerede fungerer, så er det muligt at hæve abstraktionsniveauet på testen af helet, så man ikke så hurtigt bliver indfanget af den stigende kompleksitet. Alligevel er det oplagt, at der hurtigt bliver tale om størrelser, som det ikke giver mening at teste selvstændigt - programmer afhænger simpelthen er for mange faktorer som kun dårligt lader sig kontrollere.

Med andre ord vil det give mening at have test på integrationsniveau som unittests - hvis bare man holder for øje, at det skal kunne testes selvstændigt.

Men hvor små enheder skal man så starte med at teste? Hvis man har valgt en objekt-orienteret tilgang til at strukturere ens program, så giver svaret næsten sig selv - det er objektet og dets metoder.

Min kode kan ikke unittestes...

Er de objekter, som vi opbygger vores programmer så tilstrækkeligt uafhængige til at vi kan teste dem selvstændigt? Desværre kun alt for sjældent og det skyldes som oftest at programmet trods alle gode intentioner er endt med at have en uklar ansvarsfordeling og for funktionaliteten efterhånden er blevet godt og grundigt sammenfiltret. Og som om det ikke er rigeligt med udfordringer, så er nogle programmer er nok opbygget af klasser med metoder, men uden at være spor objekt-orienterede i deres struktur - der er passive data i nogle klasser og tilstandsløs funktionalitet i andre .

Er man endda så uheldig at man står med en større mængde eksisterende kode, som aldrig rigtigt at blevet unittestet for alvor, så vil man ofte opdage at det er opbygget på en måde, som er direkte fjentlig overfor lade sig teste selvstændigt (inden man giver op, så er der dog god hjælp at hente f.eks. i Michael Feathers bog Working Efficiently with Legacy Code - her er modsvar til alle de undskyldninger som jeg i tidens løb har set for at man ikke kan unitteste, samt en lang række undskyldninger som jeg ikke havde fantasi til... læs den inden du igen siger, at du har kode som ikke kan unittestestes!).

Hvis vi vil unitteste, så tvinger det os med andre ord til at skrive kode, som kan fungere selvstændigt. Vi bliver altså nødt til at overveje ansvarsområder, interfaces, afkobling af afhængigheder (f.eks. vha. dependency injection). Og vi bliver nødt til at læse programkoden for at kunne skrive testen. Og det er faktisk her, at mange folk mener at den egentlige værdi af en unittest ligger: At den tvinger os dels til at skrive god koden, for kode der ikke kan testes er som oftest dårlig kode - og til at reviewe vores egen kode, når vi skriver testen.

Selve testen

Unittest stiller ikke kun krav til den kode, som skal testes - en god test opstår ikke uden videre (som mange har lært på hårde måde, og som alt for mange desværre stadig har til gode at indse).

Det første og helt indlysende krav, som man kan stille til en unittest er såmænd, at den tester noget. Jeg har set eksempler på unittest som godt nok huskede at kalde noget programkode, men derefter glemte at antage noget om effekten (bevares, helt uden værdi er det ikke, for man har da i det mindste fået testet, at koden kan formå at returnere normalt efter at være blevet kaldt...).

Et andet, og måske knapt så indlysende, krav er, at testen alene skal kunne svare på om der er en fejl eller ej. Hvis der er en fejl af den type, som man tester for, så skal testen kunne fejle. Og hvis den ikke er der, så må den ikke fejle. Uanset hvor mange gange man kalder den. Jeg har set unittest, som testede webservices ved at kalde en anden server - sjovt nok, så fejlede den hver gang at der var netværksproblemer, hvilket hver gang satte grå hår i hovedet på den stakkel som ikke kunne se, hvordan hans rettelse kunne have påvirket webservicen. Jeg har også set unittest, som baserede sig på at gemme hardkodede værdier i en fælles database - med den effekt, at man kun kunne køre en instans af testen - den stakkel som kom som nummer 2 ville se testen fejle uforklarligt.

Oplevelser som mine har sandsynligvis været ophav til flg. tommelfingerregler om unittests fra Micheal Feathers hånd: En unittest
  • må ikke bruge en database
  • må ikke bruge netværk
  • må ikke bruge filsystemet
  • skal kunne afvikles samtidigt
  • må ikke krave manuel konfiguration
Et yderligere krav (som i øvrigt bliver nemmere at opfylde, hvis man overholder ovenstående) er, at unittest skal kunne afvikles hurtigt. Her tænkes på, at testens afviklingstid ikke skal måles i sekunder, men derimod i millisekunder - jeg arbejder for tiden på et projekt med kun omkring 45% coverage og der er alligevel over 2500 tests. Det kommer hurtigt til at løbe op, hvis ikke man passer på.

Ingen af kravene er dog endegyldige - jeg har haft stor værdi af unittests, som kaldte de udtræk fra databasen, som programmet brugte - databaseschemaer er ikke en statisk størrelse over tid, og de er ofte ganske løst koblet til programmet. At opdage at tæppet er blevet trukket væk under en af en databaseændring, er mere værd end det overhead, som det introducerer i kørselstid hhv. den usikkerhed om resultatet, som det introducerer at være afhængig af at databaserserveren er altid kørende og velfungerende.

Pointen om at undgå databaseadgang er dog god nok. I stedet for at inline databasekode i forretningslogikken, hvorved man ikke kan teste sin kode uden at stable et større fixture på benene i databasen, så bør man afkoble sin forretningslogik fra databasetilgangen og teste de to ting adskilt.

Et eksempel: I stedet for at have en hel postnummertabel i databasen som en del af forudsætningen for en test af validering af adresselister, så lav en metode til at finde postnumre i databasen postnummertabel, test denne - og stub den så iøvrigt af i logikken til validering af adresser (ved f.eks. at lade stubben have et memory baseret map...). Programkoden er blevet nemmere at læse og forstå, det bliver nemmere at finde og rette evt. fejl, og testen kører en størrelsesorden eller to hurtigere! Der er kun fordele...

Kvalitetssikring af test

Er der så hjælp til at få gode unittest. Ja, da.

Et af det mest oplagte - og heldigvis også mest udbredte - værktøjer, er coveragemålinger. I stedet for at tro, at man rammer de tiltænkte dele af programkoden, så kan man prøve det, og efterfølgende se at man rent faktisk gør det (eller måske mere realistisk - at man ikke rammer det man troede). De simple udgaver måler linecoverage, og det er da også et godt udgangspunkt. Men det kan være misvisende, og derfor er et bedre mål, det som ofte kaldes branchcoverage. Branchcoverage måler ikke bare, at man rammer alle linjer, men også at man får afprøvet alle mulige veje igennem programkoden - eller med andre ord, at man kommer forbi alle beslutningspunkter.

Aktivt at bruge coveragemålinger når man udvikler en unittest, er de første gange en ganske lærerig oplevelse. Man mærker hurtigt, at man som udvikler kun er vant til at tænke i positive normaltilfælde (ja, som konstruktør i videste forstand, for jeg vil godt tage analytikere og designere med i båden, selvom de ikke skriver så mange unittest som os andre...) - man mangler næsten altid at tænke på fejlhåndtering og undtagelser.

Man kan ikke snakke om coveragemålinger uden også at snakke om coverage procenter. Hvor stor en coverage skal man have for at det er godt nok? 50%? 90%? 100%? Tja, som udgangspunkt er mere bedre, men der er et par solide forbehold:
  • Der er tale om en stigende omkostning og et faldende udbytte (der er en faldende grænseværdi). Man skal med andre ord gøre sig klart, hvor meget man vil betale for de ekstra procenter (og man laver dette for at finde eller undgå fejl - hvor sandsynlig er det at der gemmer sig en ekstra fejl i det sidste procentpoint?). Eller sat på spidsen: Kan det virkelig betale sig at jagte den sidste toString(), som kun bruges til debugning?
  • Ikke alle procentpoint er lige meget værd. En tommelfingerregel siger, at 20/80 reglen også gælder for fejl i programkode, og derfor vil jeg hellere have høj dækning på de 20% af koden som har stort fejlpotentiale end høj dækning i resten. Hvis 40% coverage dækker over 100% dækning på de farlige 20% af kodebasen og 25% dækning af resten, så er det langt bedre end 0% dækning af den kritiske del og 50% dækning af det trivielle.
  • Det er ikke nok, at ramme en kodelinje - man skal også bruge det til noget (se ovenfor).
  • Generel coverage har - specielt med dårlig opdelt og afkoblet kode - en tendens til at overvurdere effekten, da der også bliver talt "collaterals" med. Eller sagt på en anden måde, det var langt fra alle mine point i dart som kan tilskrives evne og omtanke - mange point er bare heldige forbiere!
Jager man coverage uden at tage stilling til risiko og effekt, så risikerer man at få testet det trivielle grundigt, og slet ikke det vanskelige. Hånden på hjertet, så vil der nok være en tendens til at bruge tid på de lavhængende frugter, hvis der ikke er anden motivation for at prioritere anderledes.

Synes man stadig ikke, at coverage er høj nok, så bør man kaste et blik på Guantanamo: Al kode er skyldig indtil testet uskyldig - og skyldig kode slettes! Så kan man hurtigt få coverage op. Når man nærlæser værktøjet beskrivelse, så opdager man, at det trods alt ikke er så slemt - al utestet kode antages at være kandidat til at blive slettet, så der genereres et parallelt sourcetræ, som kun indeholder testet kode - så kan man selv vurdere, om man tør erstatte sin "rigtige" programkode, og i hvilket omfang.

Folkene bag Guantanamo har faktisk en anden interessant tanke: Instrumenter produktionskoden, og slet efterfølgende de dele af programmet som ingen bruger... det vil nok kræve nogle rimelig lightweight instrumenteringer - men hvis måleperioden er lang nok, så kan man nøjes med en sandsynlighedsbaseret måling. Specielt hvis det kun skal bruges som inspirationsgrundlag (mit aktuelle projekt har "mørk kode" - kode, som kun skal bruges, hvis uheldet er ude - sådan en kode vil (forhåbentlig) aldrig blive kaldt i praksis, men er stadig yderst relevant at teste og at have med i kodebasen).

Et tilsvarende ekstremt værktøj er AshCroft: Der er stadig i høj grad tale om work-in-progress, men den nuværende version bruger en Java Security manager til at sikre, at man ikke kalder kode i mere end den ene klasse, som man ønsker at teste (plus de nødvendige stubbe). Så kan man lære det!

I en anden boldgade ligger værktøjer Jester. Tanken bag Jester er, at forsøge at teste om unittesten rent faktisk tester noget. Jester vil derfor mutere koden som testes, så der med vilje introduceres fejl. F.eks. vil den ændre if-sætninger (ved f.eks. at skrive not foran betingelsen) i den forventning, at en god unittest vil fange det som en fejl - fanger unittesten det ikke, så rapporterer Jester det som en fejl i unittesten!

Bliver vi klogere?

De fleste lærer heldigvis af deres fejl - men der er en hurtigere (og efter min mening mere behagelig) måde, og det er at lære af andres erfaringer.

Jeg har ovenfor henvist til Michael Feather bog Working Efficiently with Legacy Code - den kommer med mine varmeste anbefalinger. Han adresserer på glimrende vis alle de udfordringerne der kan være at få unittest op og stå i forbindelse med eksisterende kode, både egen og andres.

Står man som Moses foran det Røde hav, og ikke aner, hvad man skal gøre, så er en god introduktion bogen Pragmatic Unittesting - det er som alle bøger i Pragmatic Programmer serien nærmest at sammenligne med Pixie-bøger, men en gang imellem er det meget rart at få en nemt læst bog imellem hænderne, som klart tager stilling til, hvad der er vigtigt og hvad der kan ignoreres i første omgang. Har man prøvet at skrive mere end en håndfuld unittests, så er udbyttet af bogen nok temmeligt ringe (men på den anden side, den er, som tidligere sagt, meget nemt læst...).

Har man brug for noget med mere kød på (og der står jeg selv nu), så vil jeg tro at bogen xUnit Test Patterns: Refactoring Test Code ville være værd at kigge på. Det står højt på listen over bøger, som jeg skal have kigget nærmere på.

Ingen kommentarer: