Blogg

Test Driven Development

den 26 april 2018

Test Driven Development går ut på att låta enhetstester driva fram utvecklingen av en applikation. Det är ett roligt, utmanande och extremt effektivt arbetssätt för att skriva långsiktig och stabil kod. 

Först och främst, vad är ett enhetstest?

Ett enhetstest är ett par rader kod som kör en funktion och testar så att det förväntade resultatet stämmer med det faktiska resultatet. Om så inte är fallet så kommer testet att lysa rött i utvecklarens gränssnitt och ge en indikation om att någonting i applikationen inte fungerar som det ska. 
Kort och gott: ett enhetstest testar så att din kod gör vad du faktiskt vill att den ska göra.

Ett exempel på ett enkelt enhetstest, som testar så att kalkylatorn summerar två givna värden.

Ett exempel på ett enkelt enhetstest, som testar så att kalkylatorn summerar två givna värden.

En applikation kan innehålla hundratals, ibland tusentals små enhetstester, och dessa körs hela tiden med målet att alla enhetstester ska lysa grönt efter att du utvecklat varje ny funktion. Dessa tester ger en enorm känsla av trygghet då du som utvecklare direkt ser om du råkat förstöra någonting eller introducerat buggar i en helt annan ände av applikationen som du annars inte hade upptäckt förrän det vore för sent.

Exempel på hur det kan se ut i gränssnittet när ett enhetstest fallerar. 

Inom traditionell utveckling ser arbetet oftast ut så att först skrivs kod för att utföra en funktion, sedan skrivs ett enhetstest för den funktionen. Men det arbetssätt som jag tänkte slå ett slag för och som jag blivit mycket förtjust i är att man vänder helt och hållet på steken, skriver enhetstesterna först och låter därmed testerna driva fram utvecklingen.

Vad är TDD?

TDD (Test Driven Development) är ett arbetssätt som går ut på att man alltid börjar med att skriva ett felande enhetstest, för att därefter skriva så lite kod som möjligt för att få det testet att bli godkänt. Därefter refaktoreras koden och sedan upprepas hela processen med nästa funktion. Denna processen sammanfattas ofta i form av tre strikta lagar som varje utvecklare måste förhålla sig till för att kunna påstå sig arbeta enligt TDD.

The Three Laws of TDD:

  • You are not allowed to write any production code unless it is to make a failing unit test pass.
  • You are not allowed to write any more of a unit test than is sufficient to fail.
  • You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

 

Så här fungerar det i praktiken:

 

1. Make it fail

Ingen implementeringskod får skrivas förrän det först finns ett felande enhetstest. Det kan tyckas konstigt och lite bakvänt att börja med att skriva ett test för något som man vet inte kommer att fungera, men det finns flera goda anledningar till detta. Dels så är det mycket enklare att skriva testerna först. Anledningen till att många utvecklare tycker att det är svårt att skriva enhetstester, är för att dom försöker göra det efter att ha skrivit funktionen, och det är mycket krångligare att testa en funktion som inte är skriven för att testas. Det kan i värsta fall vara så att den funktion du försöker testa är omöjligt att testa utan att skrivas om, och då slösar du bara bort din tid med att stirra dig blind på att få testerna att fungera.

En annan god anledning till att skriva testerna först är att du som utvecklare får ett facit på den funktion du tänkt bygga, innan den byggs. Det blir väldigt enkelt att veta när man är färdig med en funktion: När alla tester är godkända och matchar alla acceptanskriterier. Dessa tester fungerar sedan som en form av levande dokumentation för dig och dina kollegor, som man alltid kan komma tillbaka till för att ta reda på hur en viss funktion är tänkt att fungera. 

Men den absolut största anledningen, i mitt tycke, till att skriva enhetstesterna först är att testerna då faktiskt blir skrivna. Eftersom du inte får skriva någon kod utan att först ha skrivit ett felande test, så blir all din kod därmed testad. Om du ger dig själv friheten att skriva testerna vid ett senare tillfälle så finns alltid risken att det glöms bort eller prioriteras bort på grund av tidsbrist.

2. Make it work

Efter att ha skrivit ett felande test, skriv därefter så lite kod som möjligt för att få just det felande testet att bli godkänt. Tanken med detta är att på ett så snabbt och enkelt sätt, och utan att krångla till det, på bara ett par få rader kod få vårat felande test att bli godkänt. Detta tvingar oss som utvecklare att tänka i enklare banor och undvika att överkomplicera saker och ting för tidigt i implementerings stadiet.

3. Make it work, well!

Sedan kommer vi till den bästa biten, refaktorering! Vid refaktoreringen så är det dags att städa den funktion vi nyss byggt, ersätta hårdkodade variabler med dynamisk data, bryta ut redundant logik och lyfta ut återanvändbara delar så att det blir tillgängligt i hela applikationen. I detta läget har vi alltså ett godkänt test redan innan vi börjar, och vi refaktorerar tills vi känner oss nöjda med funktionen. Testet ska såklart fortsatt vara godkänt efter refaktoreringen. 

4. Repeat

När alla tester åter är gröna och godkända så går vi vidare till nästa funktion och upprepar hela processen.

En långsiktig investering

Som för mycket här i livet så är tid även TDD:s värsta fiende. Tid är pengar som vi alla vet och att arbeta enligt TDD tar oftast lite längre tid. Speciellt om man inte skrivit mycket enhetstester innan. Det absolut största argumentet många har mot att jobba enligt TDD är att det tar för lång tid, men tar det egentligen längre tid? Det beror på om man ser det kortsiktigt eller långsiktigt. 

I en studie som gjordes på fyra olika case hos både IBM och Microsoft, där olika team fick jobba med samma lösning, med samma projektstyrning men där vissa av teamen anammade TDD, visade det sig att de utvecklingsteam som jobbade enligt TDD tappade omkring 15-35% i produktivitet, vilket ju kan låta skrämmande till en början. 35% på 12 månader är 4 månader. Och ingen vill väl tappa 4 månader? Men samma studie visade samtidigt att de team som jobbade enligt TDD minskade antalet defekter/buggar med 40% till 90%! Och vem vill inte ha 90% mindre buggar? 

Detta gör faktiskt TDD till ett effektivare arbetssätt både kvalitetsmässigt och tidsmässigt i det långa loppet, eftersom vi alla vet att det är underhåll och buggar som är kostsamma och tidsödande för alla projekt. Det är även buggar som kan skapa en tråkig relation mellan kund och leverantör, vilket i värsta fall kan leda till att man förlorar den kunden. Så även om TDD initialt kan kännas långsamt och tar lite extra tid, så får man se detta som en långsiktig investering.

“TDD seems to be applicable in various domains and can significantly reduce the defect density of developed software without significant productivity reduction of the development team.”

Källa Microsoft: https://www.microsoft.com/en-us/research/wp-content/uploads/2009/10/Realizing-Quality-Improvement-Through-Test-Driven-Development-Results-and-Experiences-of-Four-Industrial-Teams-nagappan_tdd.pdf

Avslutningsvis

Jag hoppas att jag förklarat TDD på ett så enkelt och lockande sätt som möjligt och att du som inte jobbar enligt detta arbetssätt åtminstone ger det ett försök i ditt nästa projekt. Det kommer kännas lite struligt i början och du kommer att känna dig långsam, men om du härdar ut och ger det några dagar så kommer du ganska snart märka att det är ett väldigt roligt sätt att arbeta på och att inställningen till enhetstester kommer gå från ett nödvändigt ont till givande och positivt utmanande.

Man brukar ju säga att allt det bästa har någon annan redan sagt, därför vill jag avsluta med mitt absoluta favoritcitat av Robert C. “Uncle Bob” Martin, som är något av en gud för många utvecklare. Det kommer från boken “The Clean Coder”, som jag för övrigt rekommenderar alla som jobbar inom utveckling att läsa. Den innehåller inte mycket kod utan handlar snarare mer om “Code of conduct” och vad det innebär att utveckla professionellt, och det är relevant för alla oavsett vilken roll man har inom utvecklingsteamet. Tack för att ni tog er tid att läsa mitt inlägg och ta hand om varandra!

“How much of the code should be tested with these automated unit tests? 
Do I really need to answer that question? All of it! All. Of. It. 
Am I suggestion 100% test coverage? No I’m not suggesting it. I’m demanding it.
Every single line of code that you write should be tested. Period.”