19
1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds Tekniska Högskola [email protected], [email protected] 28 februari 2012 Abstract Denna djupstudie om avancerad använding av JUnit går närmare in på fyra avancerade användingsområden av JUnit, Mock Objects, exception testning, theories och parameterized testning. Dessa områden valdes efter granskring av flertalet artiklar som behandlade olika avancerade användningsområden för JUnit. Målet var att hitta bra avancerade användningsområden och verktyg för dessa samt att utvärdera dess användbarhet för oss själva och programmeringsprojekt i universitetskurser. Verktyg för alla områden utom Mock Objects och Theory Explorer i theories finns inbyggda i JUnit även om dokumentationen för vissa av dem var mycket bristfällig. För att generera Mock Objects användes verktyget Mockito[3]. Resultatet av studien blev fyra verktyg som kommer vara till nytta i framtiden för oss och eventuellt även för programmeringsprojekt i universitetskurser. 1 Introduktion 1.1 Bakgrund Denna rapport behandlar en djupstudie utförd i kursen Coaching för programvaruteam vid Lunds tekniska högskola (LTH) under läsperioderna 2-3 2011/2012. Syftet med djupstudien är att utforska olika avancerade användningsområden för JUnit och undersöka dess relevans. JUnit används för att underlätta testning av Javaprogram. Genom ett ramverk av klasser för att skriva testfall gör JUnit att utvecklaren kan skriva testfall på ett bekvämare sätt och sedan köra alla tester på

Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

  • Upload
    others

  • View
    8

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

1 av 19

Avancerad användning av JUnit

David Meyer, Adam Wamai Egesa

D07, Lunds Tekniska Högskola

[email protected], [email protected]

28 februari 2012

Abstract Denna djupstudie om avancerad använding av JUnit går närmare in på fyra

avancerade användingsområden av JUnit, Mock Objects, exception testning, theories

och parameterized testning. Dessa områden valdes efter granskring av flertalet

artiklar som behandlade olika avancerade användningsområden för JUnit. Målet var

att hitta bra avancerade användningsområden och verktyg för dessa samt att

utvärdera dess användbarhet för oss själva och programmeringsprojekt i

universitetskurser. Verktyg för alla områden utom Mock Objects och Theory Explorer i

theories finns inbyggda i JUnit även om dokumentationen för vissa av dem var mycket

bristfällig. För att generera Mock Objects användes verktyget Mockito[3].

Resultatet av studien blev fyra verktyg som kommer vara till nytta i framtiden för oss

och eventuellt även för programmeringsprojekt i universitetskurser.

1 Introduktion

1.1 Bakgrund Denna rapport behandlar en djupstudie utförd i kursen Coaching för programvaruteam vid Lunds

tekniska högskola (LTH) under läsperioderna 2-3 2011/2012. Syftet med djupstudien är att utforska olika

avancerade användningsområden för JUnit och undersöka dess relevans.

JUnit används för att underlätta testning av Javaprogram. Genom ett ramverk av klasser för att skriva

testfall gör JUnit att utvecklaren kan skriva testfall på ett bekvämare sätt och sedan köra alla tester på

Page 2: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

2 av 19

en gång. Det finns en del i JUnit som heter Testrunner. Denna del tolkar testerna och utför själva

testningen. Återkoppling för vilka tester som går igenom och vilka som fallerar ges sedan på ett bekvämt

vis direkt i din IDE eller terminal. När ett testfall inte går igenom skrivs ett Failure Trace ut vilket ger

detaljerad information om vad som gått fel och vart felet inträffat.

1.2 Syfte Valet av ämne beror på intresset att ta reda på om det finns avancerade testtekniker för JUnit som kan

vara användbara i framtida projekt, både ute i arbetslivet och under studietid. I kursen ingår även ett

projekt där vi agerar coacher för ett programvaruteam som skall ta fram programvara för ett

tidtagningssystem. Teamet arbetar med Extreme Programming[17] där Test Driven Development (TDD)

är ett viktigt inslag. Extreme programming är en agil utvecklingsmetod som kännetecknas av bla pair

programming, continuous integration och Test Driven Development (TDD)[15]. Test driven development

innebär att utvecklarna skriver testfallen före själva implementationen av programmet. Detta gör att

själva designen av koden blir mer genomtänkt eftersom utvecklaren tvingas tänka igenom vad det

egentligen är man vill uppnå. Det hade därför varit mycket tillfredsställande att finna verktyg och

tekniker som kan hjälpa teamet i sitt arbete med TDD. Detta är ett ypperligt tillfälle att experimentera

lite med avancerad användning av JUnit. Teamet som coachas består av programmerare som

antagligen har mycket lite erfarenhet av testning. Därför är det viktigt att de verktyg som testas med

teamet inte blir för avancerade och förstör istället för att hjälpa till.

1.3 Metod Metoden som används i denna djupstudie är att olika avancerade användningsområden för JUnit

identifieras. Dessa användningsområdens relevans kommer sedan bedömas, främst genom en

utvärdering av hur användbart vi själva bedömer att en viss mekanism är men vi kommer även att kolla

på hur utbredd själva användningen är. Om det visar sig att ett verktyg eller en metod inte är något som

används idag så betyder det antagligen att de inte är bra. För de användningsområden och verktyg som

är relevanta kommer en referenssökning genomföras för att hitta säker information. Vissa mindre

verktyg kanske inte behandlas i några vetenskapliga artiklar och tidskrifter. I sådana fall kommer

information från utvecklare och community att användas. Efter att olika områden identifierats som

relevanta kommer tester genom praktisk användning att genomföras. Detta är huvudsakligen tänkt att

ske genom en enkel implementering. Därefter kommer en slutgiltig utvärdering att ske. Utvärderingen

kommer gå in på relevans för användning i teamets arbete samt generell användning.

2 Användning av JUnit’s standardmetoder Detta stycke syftar till att ge en snabb introduktion av JUnit för de läsare som inte är bekanta med verktyget. Om läsaren sedan tidigare använt JUnit kan denna del av uppsatsen hoppas över. För att kunna skriva testfall i JUnit behövs en import av “org.junit.Test;”. Vanlig praxis är att göra en separat testklass för att testa en klass. En testmetod inleds med notationen @Test, tex “@Test public

Page 3: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

3 av 19

void testBankAccount()”. I testmetoden skrivs sedan kod för att testa en viss del av originalkoden. För att ett testfall skall gå igenom eller misslyckas och för att TestRunner skall kunna ge en tydlig feedback används så kallade assert-metoder. Dessa metoder används för att definera hur utgången av ett lyckat testfall skall se ut. Den enklaste assert-metoden är “assertTrue(boolean villkor)”. Om villkoret är sant kommer testet gå igenom, annars kastas ett AssertionError och ett felmeddelande skrivs ut med information om vilket assertion som misslyckts. Förutom assertTrue finns det också assert-metoder som “assertNull(Object obj)” och “assertArrayEquals(int[] expecteds, int[]

actuals)”. Ibland kan det vara användbart att anropa metoden “fail(String felmeddelande)”, denna metod gör att ett AssertionError generars vilket leder till att testet direkt avbryts. Ett felmeddelande kan anges om så önskas. Enhetstester inleds vanligtvis med initiering av de objekt som används i det aktuella testfallet(fixtures), därefter görs anrop till assert-metoder för att kontrollera om de beter sig som det är tänkt. Istället för att initiera dessa objekt i varje testmetod kan en initieringsmetod skapas med notationen @Before. Denna metod kommer då att anropas före varje testmetod och objekten kommer alltså initieras före varje test. Då det krävs någon form av städning efter varje testmetod kan även en metod med notationen @After deklareras. Dessa två notationer kräver import av “org.junit.Before” respektive “org.junit.After”.

3 Mock Objects och dess användning Själva idén med Enhetstestning är att testa varje del av koden för sig. Ett problem med att skriva

enhetstester är att den del av koden som skall testas oftast använder objekt som ligger utom ramen för

just den testade enheten. När andra objekt används i ett enhetstest så bygger alltså utgången av

testningen på implementationen av dessa objekt. Om något objekt som används är buggat eller felaktigt

implementerat kan det alltså resultera i att enhetstestet ger felaktigt resultat. Ett annat problem med

beroende av yttre objekt är att testfall inte kan skrivas för än de objekt som skall användas i testet är

implementerade. Detta kan leda till att programmerare som tillämpar Test Driven Development (TDD),

tvingas frångå sina principer och skriva tester i efterhand eller vänta med att implementera just denna

del av koden. I vissa fall är det till och med så att de objekt som behövs inte kan tilldelas de egenskaper

som krävs i testmiljön med de publika metoder som finns.[2]

Dessa problem kan lösas med hjälp av Mock Objects. Metoden går ut på att dummy Objekt skapas som

ersätter de objekt man behöver vid själva enhetstestningen. Mock Objects kan alltså göra enhetstestet

helt oberoende av andra delar av koden[2] vilket alltså kan vara till stor fördel.

Mock Objects har många olika användningsområden där det kan förenkla testningen och lösa problem.

Ett viktigt sådant område är program som använder sig av en speciell infrastruktur som är jobbig att

sätta upp när testning sker. Det kan till exempel handla om en server eller en lokal databas. Ett Mock

Object som härmar beteendet av en sådan infrastruktur kan skapas. Till exempel kan man då skicka

meddelanden till Mock Objectet och få samma svar som om detta meddelande skickats till servern.

Page 4: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

4 av 19

En annan aspekt av Mock Objects användning är att själva testerna tenderar att bli bättre då man med

Mock Objects kan kontrollera värden mer frekvent genom att anropa JUnit metoden fail() då ett fel

upptäcks. Detta gör att fel kan upptäckas snabbare och att ett mer specifikt felmeddelande kan

genereras[2].

3.1 Verktyg och praktisk användning Idag finns det verktyg som automatiskt kan generera MockObject för existerande klasser. Dessa verktyg

är lätta att komma igång med och att använda. Det mest omtalade verktyget i denna kategori om man

söker på internet är EasyMock. Det finns även ett lite nyare verktyg som heter Mockito.[3] Mockito är

ett öppen källkod projekt under MIT License[4] vilket innebär att det är fritt att modifiera och använda.

EasyMock och Mockito har mycket snarlik syntax och basen av mockitoutvecklare kommer

ursprungligen från EasyMock. I denna djupstudie är det Mockito som vi har studerats lite närmare.

För att komma igång med Mockito behövs bara en jar-fil som laddas ner på den officiella hemsidan och

sedan länkas med ett Javaprojekt.[3] När detta är gjort krävs enbart en statisk import av

“org.mockito.Mockito.*;” i klassen där Mock Objects skall användas. Koden nedan visar hur ett Mock

Object av klassen “BankAccount” skapas. ”BankAccounts”-metod, “public boolean insert(int

sum)”, ställs in så att den alltid returnerar värdet true.

@Test public void TestInsert(){

BankAccount mockAccount=mock(BankAccount.class);

when(mockAccount.insert()).thenReturn(true);

assertTrue(mockAccount.insert());

}

Användingen av mockAccount Objektet i koden ovan visar enbart hur enkelt ett verktyg som Mockito är.

Detta exempel visar inte på den styrka som själva användningen av Mock Objects är kapabelt till. Då

Mockito visat sig vara mycket lättanvänt kommer det att testats på det team som coachas i kursen.

4 Testa Exceptions i JUnit Vanligtvis när enhetstester skrivs i JUnit används assertion metoder för att kontrollera om en metod beter sig så som det är tänkt. Till exempel kan ett test av en metod “int sum(int a, int b)” testas med hjälp av “assertEqual(a+b,sum(a,b))”. När assert metoder används ges en tydlig feedback på vad som gått fel, vilka värden som förväntades och vilka värden resultatet gav. Exceptions kan testas genom användning av “assertTrue(boolean b)”. Först defineras en boolean exceptionBoolean och sedan skrivs den kod som skall testas inom en “try catch” sats. I catch blocket sätts exceptionBoolean till true. Därefter körs “assertTrue(exceptionBoolean)” för att kontrollera om ett exception har kastats eller inte. Detta sätt att testa exceptions var vanligt förekommande före jUnit 4 och Kent Beck beskriver denna metod i sin pocket guide för jUnit.[1]

Page 5: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

5 av 19

I JUnit version 4 tillkom ett nytt sätt för att testa exceptions. Detta ger möjligheten att definiera en metod specifikt för att testa en viss typ av exceptions. Notationen är enligt följande exempel:

import org.junit.Test; import static org.junit.Assert.*;

@Test(expected=NumberFormatException.class) public void numberFormatException() {

String s="this is a string"; Integer.parseInt(s);

}

Ett test går igenom om koden avbryts med det Exception som är angivet i “(expected=)”. I detta

exempel är det alltså “NumberFormatException” som förväntas vilket kastas då “parseInt()”

metoden inte lyckas konvertera strängen s till en Integer. Testet kommer alltså gå igenom.

5 Annoteringarna @Theory och @Parameters I avsnitt 2 Användning av JUnits standardmetoder, diskuterades användning av notationen @Test för att

definiera testmetoder vilka i sin tur innehåller själva testkoden. Det finns två andra sätt skriva tester

med JUnit. Dessa två sätt använder sig av annoteringarna @Theory respektive @Parameters istället för

@Test. Båda metoderna kräver en speciell @RunWith-annotationen ovanför klassdeklarationen där det

anges om man vill använda sig av parameterized testning eller theories. Metoderna kan inte användas

samtidigt i en testklass.

För att reda ut exakt vilka begränsningar och funktioner som användning av @Theory och @Parameters kunde innebära så producerades testkod. Denna testkod bearbetades och sammanställdes till exempelkod för denna uppsats. Koden finns under appendixen A.1 Theories exempelkod och A.2 Parameterized exempelkod.

5.1 Theories

Bakgrund Theories är ursprungligen tänkt att användas som någon slags definition eller specifikation av ett givet

beteende för en viss funktionalitet[13]. Detta kan jämföras med hur traditionella testfall vanligtvis

endast testar ett fåtal scenarion [6]. Theories funktionaliteten skulle åstadkommas genom att analysera

specifikationen. Med hjälp av denna analys skulle villkor sättas på den möjliga indatan till testmetoden.

Villkoren skulle användas för att automatisk generera och sedan exekvera testet med alla möjliga

Page 6: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

6 av 19

kombinationer av indata. Till en början var det tänkt att villkorsprogrammering1[16][8] skulle användas

för att uppnå detta [6].

Givet att specifikationen är korrekt definierad så ger denna metod en bättre testning än vid traditionell

testning eftersom programmeraren då riskerar att missa vissa viktiga kombinationer av indata [6]. Att

använda theories är alltså tänkt att innebära användning av villkorsprogrammering för generering av

indata samt att automatiskt kombinera och exekvera alla möjliga kombinationer av indata. Vilket alltså

kan leda till att fler buggar hittas i koden snabbare.

Implementationen i JUnit I JUnit har theories vidareutvecklats och producerat funktionaliteten bakom runnern Theory som

använder @Theory-, @DataPoint- och @DataPoints-annoteringarna. Metoder och variabler deklarerade

under @DataPoints- respektive @DataPoint-annoteringarna definierar den indata som skall användas

medan metoderna som är deklarerade under @Theories-annoteringarna tar in varje kombination som

argument och testar dem. Både @DataPoint- och @DataPoints-annotationen kan endast användas

framför en publikt och statiskt deklarerad variabel eller metod. Skillnaden mellan dem är att

@DataPoint förväntar sig endast en variabel medan @DataPoints istället förväntar sig en array [9][10].

Där det kan noteras att tex list klasser som de skapade i Javas Collection hierarki endast räknas som

objekt och inte som array.

Generering av indata Till skillnad från den ursprungliga formuleringen om theories så ingår ingen villkorsprogrammering för

att generera indata i JUnits implementation av theories. Däremot så är det möjligt att använda tex

biblioteket Jacop för att utföra villkorsprogrammeringen nödvändig för att generera tänkt indata [8][16].

Det enda som krävs är att detta implementeras under en @DataPoint- eller @DataPoints-tag. Det går

även att använda andra testmetodiker för att generera indata såsom Equivalence classes och Boundary

value analysis beskrivet bl.a. i [12].

@Theory-annotationen Implementationen av theories analyserar varje metod med en @Theory-tag och räknar ut alla möjliga

kombinationer som indatan för varje metod kan anta utifrån det antal parametrar deklarerade för varje

metod samt typerna för varje parameter.

Varje möjlig kombination för en given @Theory-annoterad metod exekveras i egna instanser på samma sätt som traditionella @Test-annoterade metoder exekveras. Dvs att även eventuell @Before och @After metod exekveras före respektive efter varje anrop av en kombination för en @Theory-annoterad metod. Dessutom fungerar alla assert*-metoder, (se avsnitt 2 Användning av JUnits standardmetoder) på samma sätt som i @Test metoder.

1 Se avsnitt 6 Villkorsprogrammering för en utförligare förklaring om ämnet.

Page 7: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

7 av 19

Avbrott med assume*-satser En av de främsta sakerna som skiljer exekveringen av en @Theory-annoterad metod från en @Test-

annoterad metod är hur de statiska assume*-metoderna i JUnit biblioteket såsom tex

assumeTrue(boolean b) fungerar. Där de i traditionella testfall fungerar precis som vanliga assert-

metoder medan i @Theory-metoder istället fungerar som ett filter. För det som händer när en assume*-

metod exekveras i en @Theory-annoterad metod är att om en sats rapporteras inte stämma, dvs det

som skulle motsvara ett rött testfall, så terminerar metoden sin exekvering av den givna kombinationen.

Detta gör även att noggrannare analys av @Theory-metoden kan producera villkor från assume*-

metoder och sedan vidare automatiskt generera indata. Detta är dock inte någonting som ingår direkt i

JUnit men det fristående verktyget Theory Explorer gör just detta[7]. I exempelkoden används detta för

att inte utföra assert-satserna för strängar som inte har längden 8, vilket alltså utesluter en av

variablerna som används som indata från att användas.

Metodanrop för varje kombination Theories runnern kombinerar alltså alla möjliga variationer av den indata som definierats i samband

med @DataPoints- och @DataPoint-annoteringarna och gör ett metodanrop för varje sådan

kombination på varje @Theory-annoterad metod. Antalet metodanrop som utförs på en @Theory-

annoterad metod följer alltså som mest nedan beskrivna form:

( )

Då viss uteslutning sker när olika typer på parametrar används så kan dock antal metodanrop vara

betydligt mindre. Detta kan tex observeras i exempelkoden där testSameDayWithTheory endast

exekveras 16 gånger. Medan 125 exekveringar ges ifall alla typer istället anges till Object då

implementeringen i så fall ej kan filtrera bort vissa resultat. Detta bekräftades via strategiskt placerade

System.out.prints. Vidare så exekveras inte hela metoder vid de tillfällen då assume*-satserna inte går

igenom och metoden terminerar tidigt. Det är även värt att nämna att när @DataPoints används direkt

vid deklareringen av en metod så fungerar inte den typ av filtrering av kombinationer innan exekvering

av @Theory-metoden. Då författarna ej kunde få tillgång till dokumentationen så kunde de ej bekräfta

vidare detta var en bugg eller en avsedd inkonsekvens i implementeringen. Som exempelkoden visar så

är det dock möjligt att deklarera en statisk variabel och ta in data från en annan statisk metod och

därmed bibehålla den huvudsakliga funktionaliteten. Försök att använda @DataPoints direkt utan att

ändra deklararationen av parametertypen resulterar annars i ett java.lang.IllegalArgumentException

som kastas av Javas VM.

I appendixet A.1 Theories exempelkod så observeras att den @Test-anoterade metoden

testSameDayWithTest testar motsvarande assert-satser som den @Theory-annoterade metoden

Page 8: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

8 av 19

kommer att köra. Vilket visar på att theories implementationen betydligt förenklar läsbarheten och

reducerar mängden kod som behöver skrivas.

Exempel med @DataPoint(s) Följande deklaration som använder @DataPoint respektive @DataPoints används i exempelkoden som syns

i appendix A.1 Theories exempelkod:

@DataPoints

public static String[] someTimes = someTimes(); public static String[] someTimes() { return new String[] { "12.34.56", "00.00.00", "23.59.59", "-1" }; } @DataPoint public static Integer aDayNbr() { return new Integer(1); }

Som kan observeras så deklareras en String-array statiskt som sedan läser in resultatet från en annan statiskt deklarerad metod. Detta är främst gjort för att poängtera att även om runnern kan skicka in fel typer till @Theory-annoterade metoder (genom att använda @DataPoints med metoder som nämnts ovan) så kan detta undvikas genom att istället deklarera arrayer direkt i koden och sedan delegera värdet genom ett metodanrop, som i koden. Detta resulterar i exakt samma funktionalitet som skulle funnits om det inte funnits en bugg med @DataPoints för metoder. Som även är synligt så returnerar en annan funktion ett Integer värde som alltså används av @DataPoint-annotering för att sedan vidarebefodras till theory runnern som vidare använder alla andra variabler hämtade från @DataPoint- och @DataPoints-annoteringarna för att exekvera metoderna annoterade med @Theory.

@Theory i exempelkoden Följande deklaration som använder @Theory används i exempelkoden som syns i appendix A.1 Theories

exempelkod:

@Theory public void testSameDayWithTheory(Integer day, String timeInput1, String timeInput2) { assumeTrue(timeInput1.length() == 8); assumeTrue(timeInput2.length() == 8); MyTime myTime1 = new MyTime(day, timeInput1); MyTime myTime2 = new MyTime(day, timeInput2); assertTrue(myTime1.isSameDay(myTime2)); }

Givet att ovan angivna @DataPoint(s) har blivit inlästa sker alltså 16 metodanrop. Då det endast finns 1 variabel av typen Integer bland @DataPoint och totalt 4 variabler av typen String kombineras dessa 4 helt enkelt på alla möjliga sätt tillsammans med Integer variabeln. Det kan vidare ses att varje kombination som innehåller variabeln deklarerad som “-1” i String arrayen inte kommer att passera en av assumeTrue-

Page 9: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

9 av 19

satserna då de kräver att båda String variabler har längden 8. Just denna filtrering har ingen egentlig mening utan är bara ett exempel på hur theories kan användas. assertTrue-satsen kommer vidare i detta exemplet alltid att vara sanna för alla variabler då samma dag används för båda objekten som jämförs.

Deklarativa och konstruktiva theories Som även beskrivs i [14] så kan theories främst användas på två olika sätt. Detta är på ett deklarativt

(eng. declarative) respektive konstruktivt (eng. constructive) sätt. Deklarativ användning av theories

innebär att använda theories mer likt hur den ursprungligen tänktes användas, som ett sätt att skriva

specifikationer. Där en @Theory skall hålla för all möjlig indata, förutom den indata som inte håller för

de angivna assume*-satserna. Varvid det främst menas att hela dimensionen av möjlig indata skall

genereras automatiskt. Som nämnts kan det göras allmänt med någon form av villkorsprogrammering

där ett exempel är det verktyg som utvecklats parallellt med theories, Theory Explorer. I vilket det valts

att inte söka igenom alla möjliga lösningar utan istället försöka hitta en mer konsekvent hanterlig mängd

indata som istället täcker en stor del av indatan som kan ge upphov till buggar. Vilket det i [14] vidare

beskrivs ge upphov till att Theory Explorer kan skala bra vid användning i större projekt.

Det andra sättet att använda theories på, det konstruerande sättet, är mer analogt med den

traditionella scenariobaserade testning som annars används i JUnit. Varvid alla DataPoint(s) istället

används som exempel på giltig indata för ett visst påstående. Ovan visad exempelkod är främst skriven

på ett konstruerande sätt då endast en väldigt liten mängd indata används. Exempelkoden är dock

samtidigt ett typexempel för theories som kan omvandlas, eller utvidgas till att skrivas på ett mer

deklarativt sätt. Detta genom att autogenerera indata på tidigare beskrivet vis, via Theory Explorer eller

liknande sätt. Eventuell användning av testmetodiker för att generera indata kan istället igen beskrivas

som en konstruerande användning av theories.

Jämförelse med exempelbaserad testning I [14] så observeras det att många utvecklare ofta tänker i form av specifikationer och inte bara i form av exempel som exempelbaserad testning annars bygger på. Där konstateras att omkring 25% av tester i de undersökta opensource projekten kunde direkt översättas till det mer generella, theories. Medan så mycket som 67% av de testfall som de som utvecklade theories naturligt kunde uttryckas i theories utan nämnvärd ökning i utvecklingskostnader. Det var i stor utsträckning även möjligt att skriva dessa theories på ett konstruktivt sätt. Det var därför vidare möjligt att använda deras verktyg Theory Explorer för att hitta indata som bröt mot den specifikation skrivits och därmed hitta buggar som annars kan förbises i exempelbaserad testning pga den mänskliga faktorn.

5.2 Parameterized

Sammanfattning av funktionalitet Beteckningen parameterized beskriver funktionaliteten bakom @Parameters-annotationen i JUnit.

Funktionaliteten som parameterized tillför är att kunna köra konstruktorn före varje testfall och

eventuell @Before-metod. Vidare så körs konstruktorn och varje testfall en gång för varje Object-array

som returneras av den @Parameters-annoterade metoden. Vilket till viss del förklarar varför alla

Page 10: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

10 av 19

@Parameters-annoterade metoder måste returnera Collection<Object[]>. Där alltså varje Object[]

skickas in till konstrukturn före varje vanlig exekvering av testfallen. Mer specifikt så skickas varje

element i Object-array:en in som argument i den ordning som de är lagda i array:en. Det objekt på index

0 i array:en skickas alltså som det första argumentet och det med index 1 som det andra etc.

@Parameters-annoterade metoder måste även deklareras statiskt och vara publika[5][11].

Utförligare förklaring av funktionalitet Efter alla importer inleds testklassen i exempelkoden som syns i avsnittet A.2 Parameterized exempelkod med följande:

@RunWith(Parameterized.class) public class TestingParameters

Som tidigare nämnt gör detta att funktionaliteten bakom @Parameterized-annotationen används i

klassen. Mer specifikt så medför denna deklaration att klassen “org.junit.runners.Parameterized”

körs istället för JUnits standard runner som annars används i JUnit. Deklarationen av klassen efterföljs av

några få variabeldeklarationer och sedan de metoder som exekveras av parameterized runnern:

@Parameters public static Collection<Object[]> allFormats() { ArrayList<Object[]> allFormats = new ArrayList<Object[]>(); allFormats.addAll(someCorrectFormats()); allFormats.addAll(someIncorrectFormats()); return allFormats; } @Parameters public static Collection<Object[]> someCorrectFormats() { return Arrays.asList(new Object[][] { { true, "12.34.56" }, { true, "00.00.00" }, { true, "23.59.59" } }); } @Parameters public static Collection<Object[]> someIncorrectFormats() { return Arrays.asList(new Object[][] { { false, "-1" }, { false, "24.00.00" }, { false, "00.60.00" } }); }

Utan en inblick av hur runnern fungerar så skulle det lätt gå att tro att den kommer att behandla alla

metoder annoterade med @Parameters på samma vis. Detta är dock inte fallet. Endast den första

metoden med annotationen används senare i programmet. Detta medför alltså att metoderna

”someCorrectFormats” och ”someIncorrectFormats” endast används till ”allFormats”-metoden

när den exekveras av runnern för parameterized och inte annars. Som i exemplet måste en

@Parameters-annoterad metod både vara statisk och publik, samt returnera en Collection av Object-

arrayer.

Page 11: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

11 av 19

Metodanrop Exemplet fortsätter på följande sätt (JavaDoc har exkluderats):

@Before public void setupTest() { System.out.println(" before the @Before method"); } public TestingParameters(boolean isCorrectFormat, String timeInput) { System.out.print("Constructor executed with values: [" + isCorrectFormat + ", " + timeInput + "]"); this.timeInput = timeInput; this.hasCorrectFormat = isCorrectFormat; } @Test public void testFormats() { MyTime myTime = new MyTime(timeInput); assertEquals(myTime.hasCorrectFormat(), hasCorrectFormat); }

Dessa metoddeklarationer medför att runnern kallar alla dessa metoder i ordningen

“TestingParameters(boolean isCorrectFormat, String timeInput)” (konstruktorn), setupTest

(@Before), testFormats (@Test) och slutligen @After om den funnits med. De körs alla i individuella

instanser på samma sätt som @Before, @Test och @After annars körs av standard runnern. Vidare så

exekveras konstruktorn och därmed hela kedja för varje element i den Collection som returneras från

metoden annoterad med @Parameters och förhåller sig alltså på följande sätt:

Då varje element i den Collection som används innehåller en Object-array så tar parameterized runnern

alla elementen i den arrayen och lägger de som argument till varje konstruktoranrop. Där det första

argumentet matchas till det första elementet i varje Object-array, andra argumentet till andra

argumentet i varje Object-array etc. I exempelkoden används denna funktionalitet till att spara

argumenten som privata variabler för att sedan användas i den @Test annoterade metoden.

Utskrifter från exempelkoden Exekvering av testklassen genererar följande utskrifter:

Constructor executed with values: [true, 12.34.56] before the @Before method Constructor executed with values: [true, 00.00.00] before the @Before method Constructor executed with values: [true, 23.59.59] before the @Before method Constructor executed with values: [false, -1] before the @Before method Constructor executed with values: [false, 24.00.00] before the @Before method Constructor executed with values: [false, 00.60.00] before the @Before method

Page 12: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

12 av 19

Endast en @Parameters metod räknas Som nämnts ovan så används endast den första metoden annoterad med @Parameters av

parameterized runnern. Då endast en konstruktor kan deklareras i varje klass med en parameterized

runner så används alltid alla variabler som deklarerats i @Parameters för en given testmetod. För mer

varierande utdata måste därför extra argument som beskriver detta anges. I exempelkoden

exemplifieras detta genom att två variationer på olika utdata förväntas från den givna indatan. Indatan

förväntas mer konkret vara antingen i korrekt respektive inkorrekt format. I exempelkoden separeras

detta i testmetoden genom att ett extra booleskt värde anges som argument som alltså anger om

indatan förväntas vara i korrekt eller inkorrekt format.

För att undvika detta extra argument för att skilja på vilken utdata som förväntas för ett test så är det

också naturligtvis möjligt att göra en egen klass för all indata som genererar en förväntad utdata som

kan testas. Vilket alltså tex hade delat in exempelkoden i två klasser. En som testade indata som

genererar korrekta format och en som testade indata som generar inkorrekta format.

6 Villkorsprogrammering Kortfattat går det att konstatera att villkorsprogrammering (eller engelska constraint programming)

används för att lösa olika kombinatoriska (optimerings) problem. Detta görs genom att modellera

problemen med diverse villkor på variabler och därefter hitta lösningar som uppfyller dessa villkor. Att

hitta dessa lösningar görs av en så kallad solver och kan separeras från språket det skrivs i. Ett exempel

är att villkoren både kan skrivas i ett programmeringsspråk kallat MiniZinc och i java, där det i det senare

fallet kan göras m.h.a. biblioteket som finns till Jacop[8][16]. Program som är skrivna med villkor kan

sedan båda använda solvern Jacop[8][16] för att hitta lösningar som uppfyller ett givet kombinatoriskt

problem.

7 Framåtblickar och diskussion Under denna djupstudie valde vi att gå närmare in på fyra områden. Mock Objects, Exception testning, theories och parameterized testning. Det finns direkt stöd i JUnit för alla dessa metoder förutom Mock Objects. För att autogenerera Mock Objects användes tredjeparts verktyget Mockito som visade sig vara mycket enkelt att komma igång med. Det fanns bra dokumentation på sidan för utveckling av Mockito. Värre var det med de inbyggda testfunktionerna theories och parameterized. Dokumentationen var mycket bristfällig och de kodexempel som hittades var inte så enkla att tolka direkt för att förstå exakt vilka begränsningar de led av. Detta berodde bland annat på saker som att asumeThat() har en annorlunda funktion när theories används än vid traditionell testning. Att testa exceptions visade sig vara trivialt eftersom det finns ett inbyggt stöd för just detta sedan JUnit version 4. Till en början verkade theories och parameterized testning lite onödigt på grund av att det var lite svårare att sätta sig in i. Med facit i hand kan man dock konstatera att det är användbart för vissa tillämpningar. Parameterized är som bäst när ett värde på utdata håller för en godtyckligt stor mängd

Page 13: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

13 av 19

indata. Och då varierad utdata, vid användning av parameterized, kräver ett extra argument för konstruktorn och därmed ett mer element för varje array som skickas in. Det kan i sådana fall genereras renare kod än traditionell testning utan att ta mera tid att utveckla, då det är mindre att skriva än traditionell testning. Konstruerande theories kan användas som exempelbaserad testning och kan analogt med

parameterized då också producera enklare kod att läsa utan att ökade utvecklingskostnader. En

konstruktiv theory kan emellertid användas som specifikationer på beteende i programmet. Det kan

istället vara ett bra komplement för utvecklare de gånger det kan vara ännu naturligare att skriva test

som specifikationer. Med verktyg som Theory Explorer är det sedan möjligt att automatiskt generera

indata som skall täckas och utan extra utvecklingskostnader på det sättet hitta flera buggar. Då

mänskliga faktorer kan leda till missade scenarion som inte testas i vanlig exempelbaserad testning.

Anledningen till att vi valde just detta ämne var intresset av att hitta funktioner och verktyg för testning som vi senare skulle kunna använda i framtida skolprojekt och i arbetslivet. De verktyg som vi tittade närmare på verkar lovande för dessa ändamål och vi kommer med största sannolikhet att tillämpa det vi lärt oss. Ett annat mål med djupstudien var att undersöka om det fanns några avancerade användningsområden i JUnit som skulle kunna användas av det team vi coachar. Vi anser att det är lämpligt ta upp resultatet av denna studie med teamet och låta dem avgöra om det är något som känns användbart. För att kommunicera inom gruppen används en wiki sida. Denna sida skulle kunna fungera som ett lämpligt medium för att förmedla delar av djupstudien till gruppen, tex genom kodexempel och lite kort förklarande text. I vårt sökande efter bra avancerade användningsområden i JUnit stötte vi på många verktyg som inte

länge uppdaterades eller verkade användas aktivt. Det finns dock några verktyg som ser lovande ut vars

användbarhet inte hunnit undersökas inom tidsramen för detta projekt. Tre saker vi hade velat kollat

ännu närmare på är Hamcrest, Categories och JUnitMax. Det hade även varit intressant att fördjupa sig

ännu mer på theories området genom att testa och utvärdera Theory Explorer mer grundligt. Ytterligare

experimenterande med kod hade också kunnat vara nyttigt då det är det bästa sättet att utvärdera hur

användbart ett verktyg egentligen är.

7 Referenser [1] Kent Beck. JUnit Pocket Guide. O’Reilly Media, Gravenstein Heigh North, Sebastopol, 2004 [2] Mackinnon, T., Freeman, S., Craig, P. Endotesting: Unit Testing with Mock Objects. In Extreme

Programming Examined, G. Succi and M. Marchesi, Eds. The XP Series, pages 287-301. Addison-Wesley

Longman Publishing Co., Boston, MA, 2001.

[3] Szczepan Faber and friends. http://code.google.com/p/mockito/. Hämtad senast 26 februari 2012. [4] http://www.opensource.org/licenses/mit-license.php. Hämtad senast 26 februari 2012. [5] Isa Goksu. http://isagoksu.com/2009/development/agile-development/test-driven-development/ using-junit-parameterized-annotation/. Hämtad senast 26 februari 2012. [6] D. Saff. Theory-infected: or how I learned to stop worrying and love universal quantification. In OOPSLA ’07: Companion to the 22nd ACM SIGPLAN conference on Object oriented programming systems and applications companion, pages 846– 847, New York, NY, USA, 2007. ACM.

Page 14: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

14 av 19

[7] David Saff. From Developer’s Head to Developer Tests Characterization, Theories, and Preventing One

More Bug. In OOPSLA ’07: Companion to the 22nd ACM SIGPLAN conference on Object oriented

programming systems and applications companion, pages 811-812, New York, NY, USA, 2007. ACM.

[8] Krzysztof Kuchcinski, Radosław Szymanek. www.jacop.eu. Hämtad senast 26 februari 2012. [9] http://junit.sourceforge.net/doc/ReleaseNotes4.4.html#theories. Hämtad senast 26 februari 2012. [10]Jacob Childress. http://blogs.oracle.com/jacobc/entry/junit_theories. Hämtad senast 26 februari 2012 [11] http://junit.sourceforge.net/javadoc/index.html. Hämtad senast 26 februari 2012 [12] http://fileadmin.cs.lth.se/cs/Education/ETS200/lectures/L2_BlackBox_Usability.pdf. Hämtad senast 26 februari 2012 [13] David Saff, Marat Boshernitsan. The Practice of Theories: Adding “For-all” Statements to “There-Exists” Tests. http://shareandenjoy.saff.net/tdd-specifications.pdf(vitbok), December 7, 2006. Hämtad senast 26 februari 2012. [14] Saff, D., Boshernitsan, M., Ernst, M.D. Theories in practice: Easy-to-write specifications that catch

bugs. Technical Report MIT-CSAIL-TR-2008-002, MIT Computer Science and Artificial Intelligence

Laboratory, Cambridge, MA, January 14, 2008.

[15] Kent Beck. Test-Driven Development: By Example. Addison-Wesley, Boston, 2002.

[16] Krzysztof Kuchcinski. Constraints-Driven Scheduling and Resource Assignment. ACM Transactions on

Design Automation of Electronic Systems, Vol. 8, No. 3, pages 355–383, July 2003.

[17]K. Beck. Extreme Programming Explained: Embrace Change, Addison-Wesley, Boston, September

2004.

Page 15: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

15 av 19

A Exempelkod

A.1 Theories exempelkod

TestingTheories.java import static org.junit.Assert.*; import static org.junit.Assume.*; import org.junit.Test; import org.junit.experimental.theories.DataPoint; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; import java.util.ArrayList; @RunWith(Theories.class) public class TestingTheories { /** * Times to be used by @Theory-tagged method. * * @return Times to be used by @Theory-tagged method. */ @DataPoints public static String[] someTimes = someTimes(); /** * Method to statically determine the input times to be used. * * @return The input to be used by all @Theory-tagged methods. */ public static String[] someTimes() { return new String[] { "12.34.56", "00.00.00", "23.59.59", "-1" }; } /** * Variable to be used by @Theory-tagged method. */ @DataPoint public static Integer aDayNbr() { return new Integer(1); } /** * Asserts that two time objects with same day are considered to be on the * same day. *

Page 16: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

16 av 19

* @param day * The day to be used by both time objects. Should be sent in as * Integer. Declared as an Object parameter to avoid type * mismatch error from conversion from String to Integer. * @param timeInput1 * Time to be used by first time object. * @param timeInput2 * Time to be used by second time object. */ @Theory public void testSameDayWithTheory(Integer day, String timeInput1, String timeInput2) { assumeTrue(timeInput1.length() == 8); assumeTrue(timeInput2.length() == 8); MyTime myTime1 = new MyTime(day, timeInput1); MyTime myTime2 = new MyTime(day, timeInput2); assertTrue(myTime1.isSameDay(myTime2)); } /** * Asserts that two time objects with the same day are considered to be on * the same day. Does the same thing as the @Theory-tagged method * testSameDayWithTheory. */ @Test public void testSameDayWithTest() { ArrayList<Object> inputs = new ArrayList<Object>(); inputs.add(aDayNbr()); for (int i = 0; i < someTimes.length; i++) { inputs.add(someTimes[i]); } MyTime myTime1, myTime2; String timeInput1, timeInput2; Integer dayNbr; for (int i = 0; i < inputs.size(); i++) { for (int j = 0; j < inputs.size(); j++) { for (int k = 0; k < inputs.size(); k++) { timeInput1 = inputs.get(j).toString(); timeInput2 = inputs.get(k).toString(); if (!(timeInput1.length() == 8) || !(timeInput2.length() == 8) || !(inputs.get(i) instanceof Integer)) { continue; } dayNbr = (Integer) inputs.get(i); myTime1 = new MyTime(dayNbr, timeInput1); myTime2 = new MyTime(dayNbr, timeInput2); assertTrue(myTime1.isSameDay(myTime2)); } } } }

Page 17: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

17 av 19

}

MyTime.java import java.util.regex.Matcher; import java.util.regex.Pattern; public class MyTime { protected int dayNbr; protected String timeInput; protected Pattern timePattern; protected static final String REGEX_FORMAT = "(([0-1]?[0-9])|([2][0-3])).[0-5]?[0-9].[0-5]?[0-9]"; public MyTime(int dayNbr, String timeInput) { this.dayNbr = dayNbr; this.timeInput = timeInput; this.timePattern = Pattern.compile(REGEX_FORMAT); } public MyTime(String timeInput) { this.dayNbr = 0; this.timeInput = timeInput; this.timePattern = Pattern.compile(REGEX_FORMAT); } /** * Checks if the timeInput value stored in this object has a correct time * format. A correct time format has the form "hh.mm.ss", clock starts at * "00.00.00" and ends at "23.59.59". * * @return Returns true if the timeInput value has the the correct time * format. */ public boolean hasCorrectFormat() { Matcher matcher = timePattern.matcher(timeInput); return matcher.matches(); } /** * Compares another time object's day with own day, returns true if equal. * * @param myTime2 * Another time object. * @return boolean value from comparison between this object dayNbr with * value from other time object. */ public boolean isSameDay(MyTime myTime2) { return dayNbr == myTime2.dayNbr; } }

Page 18: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

18 av 19

A.2 Parameterized exempelkod import static org.junit.Assert.*;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.Collection;

import org.junit.Before;

import org.junit.Test;

import org.junit.runners.Parameterized;

import org.junit.runners.Parameterized.Parameters;

import org.junit.runner.RunWith;

@RunWith(Parameterized.class)

public class TestingParameters {

protected String timeInput;

protected boolean hasCorrectFormat;

/**

* List of Object arrays that are individually given as arguments to the

* constructor before execution of the @Before and @Test method.

*

* @return The list of Object arrays to be used as arguments.

*/

@Parameters

public static Collection<Object[]> allFormats() {

ArrayList<Object[]> allFormats = new ArrayList<Object[]>();

allFormats.addAll(someCorrectFormats());

allFormats.addAll(someIncorrectFormats());

return allFormats;

}

@Parameters

public static Collection<Object[]> someCorrectFormats() {

return Arrays.asList(new Object[][] { { true, "12.34.56" },

{ true, "00.00.00" }, { true, "23.59.59" } });

}

@Parameters

public static Collection<Object[]> someIncorrectFormats() {

return Arrays.asList(new Object[][] { { false, "-1" },

{ false, "24.00.00" }, { false, "00.60.00" } });

}

@Before

public void setupTest() {

System.out.println(" before the @Before method");

Page 19: Avancerad användning av JUnitfileadmin.cs.lth.se/cs/Personal/Lars_Bendix/Teaching/Lund/Coaching... · 1 av 19 Avancerad användning av JUnit David Meyer, Adam Wamai Egesa D07, Lunds

19 av 19

}

/**

* The constructor is executed for each @Test-tagged method for each Object

* array returned by the @Parameters-tagged method. If there is a

*

* The constructor will be executed before the @Before-tagged method.

*

* @param isCorrectFormat

* Index 0 of the Object array given by the @Parameters method.

* @param timeInput

* Index 1 of the Object array given by the @Parameters method.

*/

public TestingParameters(boolean isCorrectFormat, String timeInput) {

System.out.print("Constructor executed with values: ["

+ isCorrectFormat + ", " + timeInput + "]");

this.timeInput = timeInput;

this.hasCorrectFormat = isCorrectFormat;

}

/**

* Asserts that the the private String variable timeInput creates a MyTime

* object with the correct format if the private boolean variable is true.

* If false it is asserted that the MyTime object has an incorrect format.

*/

@Test

public void testFormats() {

MyTime myTime = new MyTime(timeInput);

assertEquals(myTime.hasCorrectFormat(), hasCorrectFormat);

}

}