Testdekningsmetrikker er djevelens påfunn 


 

La oss begynne med en eksempeltest på given-when-then format: 

Gitt at hunden er et stykke unna 
Når jeg roper på den 
Så kommer hunden. 

Dette er en god test – den tester observerbar oppførsel og er enkel og vedlikeholdbar. Vi kan forestille oss at objektet ‘hund’ har flere slike API endepunkter, som ‘sitt’, ‘dekk’ etc. La oss videre anta at hunden er implementert i form av en rekke detaljer – slik som forbein, bakbein, hale osv. 

Objektet ‘katt’ er som kjent betydelig enklere både å teste og implementere fordi alle endepunkter kaller samme funksjon: ignore() 

La oss nå anta at vi er i et prosjekt som er så dysfunksjonelt at vi har metrikker for “testdekning på linjenivå” og et insentiv om at denne skal være så høy som mulig. 

Vi trenger ikke gå så langt at vi forestiller oss et prosjekt direkte fra helvete der vi har ‘krav’ om ‘100% testdekning’ uten videre spesifikasjoner — noe som ikke bare er dysfunksjonelt, men også en matematisk umulighet. (et emne som eventuelt er en annen artikkel) 

Nå er plutselig ikke testen vår så ‘god’ lenger, så vi skriver den om til følgende: 

Gitt at hunden er et stykke unna 
Når jeg roper på den 
Så : 

  • Logrer halen 
  • Venstre forbein beveger seg 
  • Høyre bakbein beveger seg 
  • … 
  • … 

Dette er en aldeles forferdelig dårlig test som gir en “bedre” testdekning målt på kodelinjenivå. 

Men hvorfor er dette en dårlig test? 

Fordi den ikke tester observerbare resultater men implementasjonsdetaljer. Hvis jeg personlig kunne bestemme så hadde alle utviklere, prosjektledere og testere stått med kritt ved tavlen og skrevet: 

Jeg skal ikke automatisere testing av implementasjonsdetaljer 
Jeg skal ikke automatisere testing av implementasjonsdetaljer 
Jeg skal ikke automatisere testing av implementasjonsdetaljer 

Hvor mange ganger? 10? 100? 1000? Så mange ganger at vi faktisk kunne fått en slutt på automatisert testing av implementasjonsdetaljer! 

Men hvorfor er det så galt å automatisere testing av implementasjonsdetaljer da? 

La oss se på hva som kjennetegner en god automatisert test: 

  • Den er god til å oppdage regresjonsfeil – regresjonsfeil er i dette tilfellet all utilsiktet endring av observerbare resultater, ikke bare feil som dukker opp om-igjen 
  • Den er robust mot refaktorering – refaktorering er definert som bevisst forbedring av implementasjon uten å endre observerbare resultater 
  • Den gir rask og presis tilbakemelding 
  • Den krever lite vedlikehold 

Testen vår med “forbedret” testdekning feiler på alle punkter: 

  • Den er dårlig til å oppdage regresjonsfeil – hvis vi bytter ut funksjonen for ‘kom’ med implementasjonen for ‘sitt’ kan det hende at testen fremdeles passerer fordi både bein og hale beveger seg i dette tilfellet også. 
  • Den takler ikke refaktorering i det hele tatt – hvis vi endrer rekkefølge på hale og bein eller endog fjerner delfunksjonen ‘logre’ helt fra implementasjonen så må vi endre alle testene våre selv om vi ikke har endret faktisk funksjonalitet – dette er oppskriften på et prosjekt som går fullstendig i stå fordi ingenting kan endres. Vi kunne like gjerne skrevet koden på steintavler. 
  • Det kan godt hende den gir tilbakemeldinger raskt, men de kommer med overpresisjon – som å bruke PI med 10000 desimaler – ikke hjelpsomt i realistiske tilfelle. 
  • Vedlikeholdet blir fort ganske enormt av samme grunn som problemet med refaktorering over. 

Det høres jo ille ut, kan det bli enda verre tro? 

Joda, det er nemlig ikke alltid det er mulig å gjøre testing av alle implementasjonsdetaljer selv om man skriver dårlige tester, så da kan man fort ende opp med testsett som heter “increase test coverage” og “increase test coverage some more” med falske “tester” som bare mosjonerer koden uten å faktisk gjøre en sjekk på noe som helst — slik at verktøyet som sjekker at linjene har blitt besøkt rapporterer en godkjent andel. 

Men hvilke retningslinjer kan man ha for testautomatiering som gjør at det blir en nytte og ikke til hinder? 

  • Først, kast alle dekningsmetrikker – de vil aldri kunne gi mening 
  • Skriv gjerne tester under utvikling – jo flere jo bedre, og under utvikling er det også lov å teste implementasjonsdetaljer for å se om man har skrevet det man hadde planlagt. Test Driven Development kan også være din venn. 
  • Når man har skrevet en test, vurder om den får en god score på de 4 kvalitetskritereiene over 
  • Hvis testen scorer godt kan den beholdes, hvis den scorer dårlig så må den enten slettes eller forbedres til den blir en god test 
  • Hvis det dukker opp regresjonsfeil enten i produksjon eller under annen test så er det antakelig en funksjon som er en god kandidat for en god automatisert test 
  • Lag et minimumssett med automatiserte tester som kjører ende-til-ende og garanterer at man ikke har glemt noe fundamentalt før man iverksetter annen testing 
  • Lag tester som tester observerbar oppførsel før refaktorering slik at de fungerer som et sikkerhetsnett mot regresjon. 

Etter hvert som prosjektet skrider fremad og man under utvikling og test finner regresjonsfeil. vil man på denne måten automatisk få en riktig testdekning – en test som aldri oppdager regresjonsfeil er bortkastet kode og også en økt vedlikeholdsbelastning. 

Det er også gunstig med en plan for sanering av gamle tester – kode som er stabil genererer ikke spontane feil av seg selv og trenger ikke nødvendigvis tester som kjører gang på gang uten virkning. Det kan være en god plan å fjerne slike tester og heller lage nye ‘ferske’ tester dersom koden skal refaktoreres ved en senere anledning. Da får man også spissede tester som omhandler det man faktisk skal gjøre endringer på. 

Denne artikkelen er skrevet av Richard Rostad, han jobber i Promis Qualify.

, ,