Testdriven utveckling (TDD ) är en mjukvaruutvecklingsteknik som bygger på att upprepa mycket korta utvecklingscykler: först skrivs ett test som täcker den önskade förändringen, sedan skrivs kod som gör att testet går igenom, och slutligen skrivs omfaktorisering. genomfört ny kod enligt relevanta standarder. Kent Beck , som anses vara teknikens uppfinnare, hävdade 2003 att testdriven utveckling uppmuntrar enkel design och inger förtroende [ 1 ] .
1999, när det dök upp, var testdriven utveckling nära besläktad med test-först-konceptet som användes vid extrem programmering [2] , men senare dök det upp som en oberoende metodik. [3] .
Ett test är en procedur som låter dig antingen bekräfta eller motbevisa kodens funktionalitet. När en programmerare kontrollerar funktionen hos koden han har utvecklat utför han manuell testning.
Testdriven utveckling kräver att utvecklaren skapar automatiserade enhetstester som definierar krav för koden precis innan den faktiska koden skriver. Ett test innehåller tillståndstester som antingen kan uppfyllas eller inte. När de är avrättade sägs provet ha godkänts. Att klara testet bekräftar beteendet av programmeraren. Utvecklare använder ofta testramverk för att skapa och automatisera lanseringen av testsviter . I praktiken täcker enhetstester kritiska och icke-triviala delar av koden. Det kan vara kod som ändras ofta, kod som får mycket annan kod att fungera eller kod som har många beroenden.
Utvecklingsmiljön måste reagera snabbt på små kodändringar. Programmets arkitektur bör baseras på användningen av många komponenter med hög grad av intern kohesion, som är löst kopplade till varandra, vilket gör det lättare att testa koden.
TDD innebär inte bara kontroll av korrektheten, utan påverkar också programmets design. Baserat på tester kan utvecklare snabbt föreställa sig vilken funktionalitet användaren behöver. Således visas detaljerna i gränssnittet långt innan den slutliga implementeringen av lösningen.
Naturligtvis gäller samma krav för kodningsstandarder för tester som för huvudkoden.
Detta arbetsflöde är baserat på boken Test Driven Development: By Example av Kent Beck . [ett]
När du utvecklar genom testning börjar varje ny funktionalitet ( eng. feature ) i programmet med att skriva ett test. Oundvikligen kommer detta test att misslyckas eftersom motsvarande kod ännu inte har skrivits. (Om det skriftliga testet blir godkänt finns antingen den föreslagna "nya" funktionen redan, eller så har testet brister.) För att skriva ett test måste en utvecklare tydligt förstå kraven för den nya funktionen. För detta övervägs möjliga användningsfall och användarberättelser. Nya krav kan också ändra befintliga tester. Detta skiljer testdriven utveckling från tekniker där tester skrivs efter att koden redan har skrivits: det tvingar utvecklaren att fokusera på kraven innan koden skriver – en subtil men viktig skillnad.
I detta skede kontrolleras att de nyss skrivna proven inte blir godkända. Detta skede kontrollerar också själva proven: det skriftliga provet kan alltid godkännas och därför vara värdelöst. Nya test bör misslyckas av uppenbara skäl. Detta kommer att öka förtroendet (även om det inte garanterar helt) att testet faktiskt testar vad det var designat för att göra.
I det här skedet skrivs ny kod så att testet ska klara. Den här koden behöver inte vara perfekt. Det är acceptabelt att den klarar testet på något oelegant sätt. Detta är acceptabelt eftersom efterföljande steg kommer att förbättra och polera den.
Det är viktigt att skriva kod som utformats specifikt för att klara testet. Du bör inte lägga till onödig och följaktligen oprövad funktionalitet.
Om alla tester blir godkända kan programmeraren vara säker på att koden uppfyller alla krav som testas. Efter det kan du fortsätta till det sista steget av cykeln.
När den önskade funktionaliteten har uppnåtts kan koden rensas upp i detta skede. Refaktorering är processen att ändra den interna strukturen i ett program utan att påverka dess externa beteende och med syftet att göra det lättare att förstå dess arbete, eliminera kodduplicering och göra det lättare att göra ändringar inom en snar framtid.
Den beskrivna cykeln upprepas och implementerar mer och mer ny funktionalitet. Stegen ska vara små, mellan 1 och 10 förändringar mellan testkörningarna. Om den nya koden misslyckas med de nya testerna, eller om de gamla testerna slutar klara, måste programmeraren återgå till felsökning . När du använder tredjepartsbibliotek bör du inte göra ändringar så små att de bokstavligen testar själva tredjepartsbiblioteket [3] , och inte koden som använder det, om det inte finns misstanke om att biblioteket innehåller fel.
Testdriven utveckling är nära besläktad med sådana principer som " håll det enkelt, dumt, KISS " och " du behöver det inte, YAGNI " . Designen kan bli renare och tydligare genom att bara skriva den kod som behövs för att klara testet. [1] Kent Beck föreslår också principen " fake it till you make it " . Tester bör skrivas för den funktionalitet som testas. Detta anses ha två fördelar. Detta hjälper till att säkerställa att applikationen är testbar, eftersom utvecklaren måste tänka på hur applikationen ska testas redan från början. Det hjälper också till att säkerställa att all funktionalitet täcks av tester. När en funktion skrivs före testning tenderar utvecklare och organisationer att gå vidare till nästa funktion utan att testa den befintliga.
Tanken på att kontrollera att ett nyskrivet test misslyckas hjälper till att säkerställa att testet faktiskt testar något. Först efter denna kontroll bör du börja implementera den nya funktionen. Denna teknik, känd som "röd/grön/refaktorering", kallas det "testdrivna utvecklingsmantrat". Här betyder rött de som inte klarat proven och grönt de som har godkänts.
Etablerade testdrivna utvecklingsmetoder har lett till skapandet av tekniken Acceptance Test-driven Development (ATDD ), där de kriterier som kunden beskrivit automatiseras till acceptanstest, som sedan används i den vanliga utvecklingsprocessen genom enhetstestning ( sv .unit testdriven development, UTDD ). [4] Denna process säkerställer att ansökan uppfyller de angivna kraven. Vid utveckling genom acceptanstestning är utvecklingsteamet fokuserat på ett tydligt mål: att tillgodose acceptanstest som återspeglar relevanta användarkrav.
Acceptans (funktionella) tester ( engelska kundtester, acceptanstest ) - tester som kontrollerar applikationens funktionalitet för överensstämmelse med kundkrav. Acceptanstest utförs på kundens sida. Detta hjälper honom att vara säker på att han kommer att få all nödvändig funktionalitet.
En studie från 2005 visade att att använda testdriven utveckling innebär att skriva fler tester, och att programmerare som skriver fler tester tenderar att vara mer produktiva. [5] Hypoteser som kopplar kodkvalitet till TDD har varit ofullständiga. [6]
Programmerare som använder TDD i nya projekt rapporterar att de är mindre benägna att känna behovet av att använda en debugger. Om några av testerna plötsligt misslyckas kan det vara mer produktivt att rulla tillbaka till den senaste versionen som klarar alla tester än att felsöka. [7]
Testdriven utveckling erbjuder mer än bara validering, den påverkar också programdesign. Genom att initialt fokusera på tester är det lättare att föreställa sig vilken funktionalitet användaren behöver. Således tänker utvecklaren igenom detaljerna i gränssnittet innan implementeringen. Tester tvingar dig att göra din kod mer testbar. Till exempel, överge globala variabler, singletons, gör klasser mindre kopplade och lättare att använda. Högkopplad kod, eller kod som kräver komplex initiering, kommer att vara betydligt svårare att testa. Enhetstestning bidrar till bildandet av tydliga och små gränssnitt. Varje klass kommer att ha en specifik roll, vanligtvis en liten. Som en konsekvens kommer engagemanget mellan klasserna att minska och anslutningen kommer att öka. Kontraktsprogrammering ( eng. design by contract ) kompletterar testning och bildar de nödvändiga kraven genom uttalanden ( eng. assertions ).
Även om testdriven utveckling kräver mer kod för att skrivas, är den totala utvecklingstiden vanligtvis mindre. Tester skyddar mot fel. Därför minskar tiden som läggs på felsökning många gånger om. [8] Ett stort antal tester hjälper till att minska antalet buggar i koden. Att åtgärda defekter tidigare i utvecklingen förhindrar kroniska och kostsamma buggar som leder till lång och tråkig felsökning senare.
Tester låter dig refaktorera kod utan risk att förstöra den. När du gör ändringar i väl testad kod är risken att introducera nya buggar mycket lägre. Om den nya funktionaliteten leder till fel kommer tester, om de finns, naturligtvis omedelbart att visa detta. När man arbetar med kod som det inte finns några tester för kan ett fel upptäckas efter en lång tid, då det blir mycket svårare att arbeta med koden. Väl testad kod tolererar lätt refaktorering. Förtroende för att ändringar inte kommer att bryta befintlig funktionalitet ger utvecklare förtroende och ökar deras effektivitet. Om befintlig kod är väl täckt med tester kommer utvecklare att känna sig mycket mer fria att fatta arkitektoniska beslut som förbättrar designen av koden.
Testdriven utveckling uppmuntrar mer modulär, flexibel och utbyggbar kod. Detta beror på att med denna metodik behöver utvecklaren tänka på programmet som många små moduler som är skrivna och testade oberoende och först då kopplade samman. Detta resulterar i mindre, mer specialiserade klasser, mindre koppling och renare gränssnitt. Användningen av mockar bidrar också till kodmodularisering, eftersom det kräver en enkel mekanism för att växla mellan mock och vanliga klasser.
Eftersom endast koden som behövs för att klara testet skrivs täcker automatiserade tester alla exekveringsvägar. Till exempel, innan du lägger till ett nytt villkorligt uttalande, måste utvecklaren skriva ett test som motiverar tillägget av detta villkorliga uttalande. Som ett resultat är testerna som är resultatet av testdriven utveckling ganska kompletta: de upptäcker alla oavsiktliga förändringar i kodens beteende.
Tester kan användas som dokumentation. Bra kod kommer att berätta hur det fungerar bättre än någon annan dokumentation. Dokumentation och kommentarer i koden kan vara inaktuella. Detta kan vara förvirrande för utvecklare som tittar på koden. Och eftersom dokumentation, till skillnad från tester, inte kan säga att den är föråldrad, är situationer där dokumentation inte är sann inte ovanliga.
Testsviten måste ha tillgång till koden som testas. Å andra sidan bör principerna för inkapsling och datadöljning inte kränkas. Därför skrivs enhetstester vanligtvis i samma enhet eller projekt som koden som testas.
Det kanske inte finns någon tillgång till privata fält och metoder från testkoden . Därför kan enhetstestning kräva ytterligare arbete. I Java kan en utvecklare använda reflektion för att referera till fält markerade som privata . [10] Enhetstester kan implementeras i inre klasser så att de har tillgång till medlemmar i den yttre klassen. I .NET Framework kan partiella klasser användas för att komma åt privata fält och metoder från ett test.
Det är viktigt att kodavsnitt avsedda enbart för teständamål inte finns kvar i den släppta koden. I C kan villkorliga sammanställningsdirektiv användas för detta. Detta kommer dock att innebära att den släppta koden inte exakt matchar den testade koden. Genom att systematiskt köra integrationstester på en släppt version kan du säkerställa att ingen kod finns kvar som implicit förlitar sig på olika aspekter av enhetstester.
Det finns ingen konsensus bland programmerare som använder testdriven utveckling om hur meningsfullt det är att testa privata, skyddade metoder såväl som data . Vissa är övertygade om att det räcker att testa vilken klass som helst genom dess offentliga gränssnitt, eftersom privata variabler bara är en implementeringsdetalj som kan ändras, och dess förändringar bör inte återspeglas i testsviten. Andra hävdar att viktiga aspekter av funktionalitet kan implementeras i privata metoder, och att testa dem implicit genom ett offentligt gränssnitt kommer bara att komplicera saker: enhetstestning innebär att testa minsta möjliga enheter av funktionalitet. [11] [12]
Enhetstester testar varje enhet individuellt. Det spelar ingen roll om modulen innehåller hundratals tester eller bara fem. Tester som används i testdriven utveckling bör inte korsa processgränser, använda nätverksanslutningar. Annars kommer det ta lång tid att klara tester och utvecklare kommer att vara mindre benägna att köra hela testsviten. Genom att införa ett beroende av externa moduler eller data förvandlas också enhetstester till integrationstester. Samtidigt, om en modul i kedjan beter sig felaktigt, kanske det inte är direkt klart vilken.
När koden som utvecklas använder databaser, webbtjänster eller andra externa processer är det vettigt att lyfta fram den del som testas. Detta görs i två steg:
Att använda falska och skenbara objekt för att representera omvärlden resulterar i att den verkliga databasen och annan extern kod inte testas som ett resultat av den testdrivna utvecklingsprocessen. För att undvika fel är tester av verkliga implementeringar av gränssnitten som beskrivs ovan nödvändiga. Dessa tester kan separeras från resten av enhetstesten och är egentligen integrationstester. De behöver färre än modulära, och de kan lanseras mer sällan. Men oftast implementeras de med samma testramverk som enhetstester .
Integrationstest som modifierar data i databasen bör återställa databasen till det tillstånd den var i innan testet kördes, även om testet misslyckas. Följande tekniker används ofta för detta:
Det finns bibliotek Moq, jMock, NMock, EasyMock, Typemock, jMockit, Unitils, Mockito, Mockachino, PowerMock eller Rhino Mocks, såväl som sinon för JavaScript, designade för att förenkla processen att skapa skenobjekt.