Erfarenheter av enhetstest

Enhetstest har vi sysslat med ganska många år nu och det är idag en självklar del av mjukvaruutvecklingsarbete. De flesta antingen praktiserar det, ljuger om att de praktiserar det och/eller har dåligt samvete för att de ännu inte hunnit sätta igång att praktisera det.

Jag tänker här summera ihop vad jag hittils samlat ihop för erfarenheter från verkligheten i ämnet enhetstest.

Motivation
Den viktigaste positiva effekten av enhetstest är att det styr oss utvecklare att skriva mer modulär och testbar kod. Det underbara med enhetstest är att egenskaperna hos testbar kod har mycket gemensamt med egenskaperna för bra design! Det är tydligare att ställa krav på “testbar kod“ än på “bra design”.

En annan viktig effekt av enhetstest är att möjliggöra refakturering av kod. Enhetstesterna försäkrar oss om att vi kontinuerligt är på banan, även med vår nya partiellt omskrivna kod.

Med kod som inte är testbar menas att klasser och paket hänger ihop i en ej enkelt delbar enhet, och detta går bara att, utan att ändra koden, att testa som integrations- eller systemtest, dvs. att man matar in data i ena änden (t.ex. UI eller en infil) och observerar vad som kom ut i andra änden av modulen/systemet, t.ex. en utfil eller databas.

Enhetstest skall istället testa varje enskild enhet, dvs. enskilda moduler/klasser, genom att systematiskt anropa dess gränssnitt och analysera dess svar och resultat.

Den sammanlagda motivationen till enhetstest är att möjliggöra att man med större precision skall kunna leverera det som utvecklarna avser att leverera och på ett smidigt sätt ha möjlighet att, med bibehållen kvalitet, anpassa och vidareutveckla systemet i takt med att kravbilden förändras. 

Väl enhetstestade system fungerar helt enkelt bättre och är enklare att förändra.

När man passar
Kod där det är svårt och enligt min mening ofta för dyrt att skriva och underhålla enhetstest är användargränssnitt, och där det allra yttersta lagret av kod längst ut mot användargränssnittet. Jag har försökt och sett försök att enhetstesta UI-kod men aldrig noterat någon som lyckats bra (det kanske finns bra motexempel numera? Tipsa mig gärna om detta!). Ett sätt att adressera detta på är att designa UI på så vis att det yttersta lagret är mycket tunt – inte massa logik utan bara den yttre delen av presentationsfunktionaliteten, skippa enhetstester helt för dessa delar och istället fokusera enhetstest på den kod som finns i lagren innanför. Denna strategi sammanfaller med god design av UI också.

Andra exempel när enhetstest lätt kan kosta mer än det smakar är komplexa algoritmer som arbetar med bilder, kartor, ljud eller video. Hur skall ett enhetstest se ut som testar korrektheten i en algoritm ”Lens Distortion” i ett bildbehandlingprogram? Sist jag stod inför liknande problem utelämnade vi dessa enhetstester eftersom vi bedömde det vara dyrare att skriva och underhålla dessa enhetstester än att släppa igenom fler fel i dessa moduler till senare testfaser.

Korrekthet
Enhetstest skall testa korrekthet och inte detektera om något förändras. Detta är värt att poängtera. Ett exempel på ett felaktigt utformat enhetstest jämför med något referensdata som finns på fil eller som klistrats in i koden, som kanske dessutom samlats in under oklara former. När detta test senare fallerar, vet inte någon vad proceduren är eller var för att verifiera om det är ett fel som upptäcks eller om det är en valid förändring i beteendet om det inte är tydligt dokumenterat i koden hur man härleder vad som är ett korrekt resultat.

I enhetstest som innehåller magiska konstanter skall det antingen vara självklart varför man jämför med just dessa konstanter (add(3, 4) → assert result==7) eller så skall det stå dokumenterat hur man verifierat att resultatet skall bli just 42. Det kan vara svårare än man kan tro att inte hamna i den fällan.

Jag brukar helt enkelt rensa bort enhetstester som bara testar förändring i utdata istället för att testa korrekthet eftersom de döljer den riktiga statusen beträffande enhetstester och för att de försvårar vidareutveckling av systemet, istället för precis tvärtom, att förenkla.

Om man verkligen vill detektera förändringar i systemets beteende, det kan finnas goda skäl till detta också, så skall dessa tester förläggas skiljt från enhetstesterna.

Kodtäckning
Kodtäckningssiffran för enhetstester är viktig, men måste användas i ett nyanserat sammanhang. Som grov tumregel brukar jag säga att 70% börjar närma sig anständighetsgränsen. Här brukar jag räkna bort kod som inte behöver/skall enhetstestas (se tidigare). Ju mer komplex programkod desto högre täckningsgrad och bättre verifiering av korrekthet krävs och omvänt desto mer trivialt/grunt/likformigt desto lägre kan täckningsgraden vara. 80% är bra men har man över 90% så tycker jag att man kan fundera på om det är dags att fokusera på något mer angeläget problem.

Men jag vacklar lite här – jag är nyfiken på om 100% kanske i själva verket är en bra idé, åtminstånde som målsättning, och att det kanske leder till ett bättre och billigare system totalt sett i slutänden. Vissa säger att 100% är omöjligt utom möjligen i trivial kod. Det kanske är så. Jag testade nyss att fylla ut luckorna i ett datalager baserat på JPA 2.0 med en hyggligt komplex informationsmodell och kom relativt lätt upp i 97.5%. Resten har jag faktiskt hopp om att addressera eftersom de beror på otydlig felhantering i DAO:ar, som alltså borde kunna designas bort. Men å andra sidan utgör sällan datalager med JPA komplexa hierarkier och komplex kod utan är istället grund och bred till sin natur, så det är inte ett idealt exempel. Andra gånger i mer komplexa moduler när jag försökt öka kodtäckningen har jag varit tvungen att lägga uppseendeväckande mycket tid på att öka från till exempel 80% till 90% trots att koden är riktigt bra. Man tröttnar till slut och undrar om det är värt det. Dessutom blir det en massa testkod att skyffla framför sig i framtida refaktureringar och omdesigner.

Om 100% innebär att en ansenlig del av problemen flyttas upfront, så vi slipper få dem i senare faser när de är dyrare att åtgärda så är 100% en bra idé. 100% tvingar utvecklaren att verkligen fundera igenom designen kring felhanteringen eftersom det brukar ju vara i obskyra catch-satser som man inte vet hur man skall nå som de sista procenten otäck(t) kod hittas. Kan man designa bort denna sällan eller aldrig exekverade kod helt istället så är mycket vunnet. Men 100% förutsätter nog en riktigt bra design precis överallt, och därför kanske förblir en utopi.

Dock säger kodtäckning absolut inte allt. Man kan ha hög kodtäckning på enhetstesterna men ändå ha låg funktionstäckning om man inte testat korrekthet i tillräckligt hög grad.

Ett av de enklaste sätten att få hög kodtäckning men låg funktionstäckning är att implementera enhetstester som anropar alla klasser och metoder med minsta möjliga ansträngning, och som inte bryr sig om resultatet. Då får man hög kodtäckning men man har bara kontrollerat att koden inte smäller. Inte att den gör något och definitivt inte vad den gör.

Därför kan det vara en god idé att när man nått till exempel 80% så kollar man på att öka funktionstäckningen, eller kvaliteten på testkoden, istället för att stirra sig blind på att öka kodtäckningen ytterligare.

Utvecklingsprocess
Enhetstest måste skrivas i samma projektfas som koden skrivs och designas. Enligt TDD skall man skriva enhetstesten först och aldrig skriva mer kod än att enhetstesten passerar. Det är kul de perioder när det fungerar. Ibland fungerar det och passar också problemet. Ibland passar det inte.

I verkligheten finner jag det ibland inte går att designa enligt TDD så eftersom ett tidigt testskrivande stör ett itererande och trevande designarbete, där man provar sig fram med olika designlösningar för att se vilken lösning som faller på plats. Samma situation inträffar när man prövar sig fram i designen under att man försöker förstå någon aspekt på ett API eller ramverk. När sedan en design som man börjar bli nöjd med tagit form (men inte senare!) är det dags att skriva enhetstesterna. Man får emellertid inte ha hunnit så långt med designen och koden att man inte tillåter sig att göra substantiell omdesign när man upptäcker vilka aspekter av designen som inte blev testbara, vilket nästan alltid händer, och skriva om dessa så de blir testbara.

Att lägga till enhetstest som en massiv aktivitet i efterhand har jag ibland sett ske men aldrig sett lyckas utan att man samtidigt tar betydande omtag på koden som skall testas. En annan variant på enhetstestrelaterade aktivieter som heller inte fungerar är att dela upp utvecklingsarbetet så att en utvecklare skriver enhetstestkoden och en annan resten av koden. Detta kan kanske fungera som en variant av parprogrammeringsform där man sitter bredvid varandra och kodar i realtid, men inte annars.

Visst går det att skriva enhetstestkod i efterhand, men om man bara lägger till testkod utan att anpassa den testade koden fyller den inte sitt syfte att öka kvaliteten på koden, minska felen i densamma eller göra den lättare att refakturera.

För att lyckas med att lägga till enhetstest i efterhand måste arbetet vara förenat med refakturering och omdesign, förmodligen substantiell, för att göra koden testbar.

För att lyckas med enhetstest krävs samma egenskaper och parametrar som för att lyckas med mjukvaruutveckling generellt. Det går alltså inte att bota dålig programkod och ett dåligt fungerande projekt bara genom att ställa krav på enhetstest på projektet.

Enhetstest är en aktivitet som rör utvecklare, inte testare. Att ta fram enhetstest är en naturlig del av utvecklingsprocessen. Från tests sida i projektet kan man dock välja att använda sig av informationen hur enhetstesttäckningen ser ut för att rikta in det övriga testarbetet på de delar som har en lägre enhetstesttäckning och som man bedömer har hög testprioritet.

Vinst- och investeringsavvägning
Enligt min erfarenhet är det tveksamt om investeringen i att skriva kompletta enhetstester i första utvecklingssvängen lönar sig. Däremot är det en förutsättning för att kunna vidareutveckla systemet efter första releasen. Eftersom det inte går att lägga till alla enhetstester i efterhand utan att helt eller delvis skriva om systemet så gör man alltså bäst i att lägga till dem på en gång. Men det beror också på vilka tillförlitlighetskrav man har på systemet. Om det är så att buggar kan få mycket dyra konsekvenser är ökat fokus på enhetstest ett av många bra sätt att minska på antalet buggar redan i designfasen.

Man kan nog egentligen bara strunta i att skriva enhetstester om man vet med sig att man gör en prototyp som man skall slänga sedan. Fast kanske inte ens då, verktygen för att stödja enhetstest blir bättre och bättre och ett TDD-inspirerat arbetssätt upplever jag fungerar bra även för prototyper. Prototyper skall också fungera.

Continuous Integration (CI)
Jenkins eller annan CI server behövs inte för att köra enhetstest eller annan automattest, men det är ett bra och bekvämt verktyg därför att det tillför historik, automatik och notifieringar på ett standardiserat sätt. Det är så enkelt att sätta upp idag att det finns få skäl att låta bli.

Att sätta upp en Jenkins-server är i storleksordning 1-3 dagars arbete om man inte tillför några ytterligare sidokrav. Den behöver inte backas upp och behöver ingen reserv. Den utgör bara ett verktyg som bara behöver finnas så länge utvecklingsarbetet pågår. Glöm inte att göra noteringar om installationsproceduren på projekt-wikin, även om det var trivialt. Det lönar sig när man står där nästa år med en kraschad CI-server och det som vanligt är bråttom med något annat.

Det är en bra idé att också installera Sonar på Jenkins-servern. Sonar levererar utmärkta s/w-metrics på ett enkelt och snyggt sätt. Precis som med Jenkins, saknas goda skäl att inte köra Sonar 😉

Att sätta upp Jenkinsjobb kan däremot ta väldigt olika tid beroende på deras beskaffenhet.

Om man har en modul baserad på Java/Maven är det normalt mycket lätt att integrera den med Jenkins: Storleksordning minuter.

Man kan bli tvungen att börja beta av teknisk skuld om man skall börja med Continuous Integration i form av att rätta upp bökiga byggprocedurer, dokumentera utvecklingsmiljösetuper, krångla med manuella patchar och installationsförfaranden som måste utföras på byggmaskiner och/eller målmaskiner, kanske till och med av speciella personer, för att Jenkins skall kunna köra systemet. Därför kan det ta längre tid än en kvart att lägga till en testkörningsjobb av en modul/system i Jenkins. Detta arbete är normalt dock inte dåligt spenderad tid. Normalt får man betala för det senare ändå.

Att introducera Jenkins (eller annan CI-server) när man redan har JUnit tillför ytterligare kvalitet, förutom responsen som en CI-server ger: Man blir tvingad att checka in allt och dokumentera allt och ha procedurer för allt eftersom Jenkinsjobbet skall kunna bygga och köra alla tester från incheckad källkod.

Jenkins och JUnit är sedan utmärkt plattform att lägga ytterligare testautomatisering på: Integrationstest, systemtest och acceptanstest. Den dagen man tillför automatiska tester som kör hela systemet (integration, system, acceptans) tillförs ytterligare robusthet eftersom Jenkins då inte bara behöver kunna bygga hela systemet utan också köra det och ha tillgång till testdata, och dessutom repetitivt.

IT Consultant at CAG Edge. Cloud and Continuous Delivery specialist, software developer and architect, Node.js, Java.

Publicerad i Java, Test
4 comments on “Erfarenheter av enhetstest
  1. ”Andra exempel när enhetstest lätt kan kosta mer än det smakar är komplexa algoritmer som arbetar med bilder, kartor, ljud eller video.”
    Jag skulle tvärtom vilja påstå att det är en problemklass där det är särskilt viktigt att ha bra enhetstester.
    Jag kan medge att det finns vissa hakar. Man vill av praktiska skäl kunna kontrollera resultaten med manuella beräkningar, vilket är svårt med stora datamängder. Min erfarenhet är att det oftast är möjligt att göra enhetstester för mycket mindre datamängder än funktionen normalt arbetar med, så att manuell verifiering är rimlig.
    Sedan måste man förstås ha en tillräckligt bra spec för att kunna säga vad som är ett korrekt resultat, men det gäller ju alltid. Om du inte vet vart du vill komma är det liten chans att du kommer dit.
    Ibland har man bara en intuitiv spec på slogannivå och det är upp till implementatören att välja beteende — jag antar att det är det fallet du fiskar efter med exemplet ”Lens Distortion”. Själv ser jag inte varför det skulle vara särskilt svårt att enhetstesta. Kan du beskriva hur algoritmen fungerar för 8×8 pixlar? Kan du göra motsvarande beräkning i Excel/Matlab eller med kollegieblock och miniräknare? Vad hindrar dig isåfall att göra ett motsvarande enhetstest?

    • Daniel Marell skriver:

      Du har rätt i att ”Lens Distortion” förmodligen går alldeles utmärkt att enhetstesta och att denna klass av funktionalitet dessutom är särskilt viktig att enhetstesta. Suck. Det är alltså inte ett bra exempel på när enhetstest blir för dyrt.

      Det är ett bra retoriskt grepp att exemplifiera tre gånger. Jag var till mitt stora förtret tvungen att stryka ett av mina tre exempel redan innan publicering eftersom det var ett tveksamt exempel, och nu tog du ifrån mig ett till. Om någon nu kommer in med ett bra förslag på hur vi enhetstestar UI blir det ingenting kvar av passmöjligheterna. Undanflykterna att hoppa enhetstester blir färre och färre desto mer jag gräver.

  2. Per Lundolm skriver:

    Vi enhetstestar GUI. 🙂 Vi vill verifiera att ”om man trycker på köp så hamnar produkten i varukorgen”. Då vi kör Wicket så har den ett testramverk inbyggt. Därmed slipper vi tester som är kopplad till layout och sega Selenium servrar. Och vi kan testa enskilda komponenter, widgets.

    • Daniel Marell skriver:

      Jaha. Där satt den sista spiken. Inget arkitekturellt lager är fredad från enhetstest. Och kör man Wicket i sitt projekt får man alltså inte något enhetstestandrum alls.

      Men automatgenererad kod tänker iaf inte jag enhetstesta. Inte heller likformiga adapterklasser. Skrev en sån nyss för en Web Service och tänkte att ”även om den här klassen vore den sista oenhetstestade klassen i världen så…”

Kategorier

WP to LinkedIn Auto Publish Powered By : XYZScripts.com