.center.middle # Test-drevet Webutvikling
_.tt[ivar.conradi.osthus@iteraconsulting.no],_
_.tt[kris-mikael.krister@iteraconsulting.no]_ --- # Agenda 1. Testing intro - Hvorfor tester vi? - Flere nivå av testing 2. Play framework - Introduksjon til rammeverket - Praktisk eksempel - Rask presentasjon av Selenium og Selenese - Vise Plays ulike testtyper - Installasjon og oppsett av første prosjekt 3. Test-driven development - Introduksjon til TDD - Kodeeksempel 4. Workshop - Utfordringer med test-drevet webutvikling - Introdusere oppgaven - Teste kontrollere - Teste modeller - Teste perisistering - Teste validering - Teste viewet --- .center.middle # Testing intro --- # Hvorfor tester vi? - **Verifisere at løsningen tilfredsstiller kravene** - Bygger vi det vi skal? - **Sikre at løsningen ikke gjør noe annet enn det vi hadde tenkt** - **Sikre kodekvalitet** - **Beskytte koden ved fremtidige endringer** - **Fungerer som dokumentasjon** --- # Sikre kodekvalitet - Verifisere korrekthet kode - Enklere å teste grenseverdier gjennom tester - Finne feil/bugs tidligere - Spare tid og penger - Gode tester kan indikere et *minimum* av kvalitet - MEN: vi kan *ikke* teste kvalitet direkte --- .center.middle ### Det er billig å finne feil tidlig
--- # Beskytte koden ved fremtidige endringer - Gjør det billigere å legge til mer funksjonalitet siden - Ofte *bieffekter* ved å innføre ny funksjonalitet - Vi har ikke kontroll på fremtiden - Hvilke utviklere som kommer til - Hvilken funskjonalitet som skal endres / legges til - Gode tester gjør det tryggere å refaktorere - En mekanisme for å øke kodekvaliteten - Forbedre / endre system-design --- # Fungerer som dokumentasjon - Hvordan skal koden benyttes? - Hva har utviklerne tenkt? - Hvordan instansierer vi opp et objekt? - Hva er grenseverdiene?
@Test public void shouldRequirePersonToBeOver18ToBuyBeer() { Person p = new Person("ole", 18); assertFalse(p.allowedToBuyBeer()); }
--- # God testpraksis - Automatiserte tester - Raske tester - Små isolerte tester som verifiserer konkrete funksjoner - Testene skal kunne kjøres i vilkårlig rekkefølge - Testene kjøres ved **innsjekk** av ny kode - Typisk av en *continous integration server* (CI)
###Dersom koden er vanskelig å teste er det et tegn på dårlig design...
--- .center # Vi ønsker å unngå feil i
produksjon
--- .center # Testing i flere nivå
--- # Enhetstester
###Validerer at *isolerte enheter* i applikasjonen fungerer slik de skal.
- Tester små biter av koden - Tester *betinget* funksjonalitet - *Isolerte* tester. - Involverer typisk bare en klasse - Faker avhengiger via "*mocks*" - F.eks: fake en database basert på API ### Eksempler i WEB-app - Benyttes til å validere domenemodeller --- #Enhetstester: eksempel
public class FoobarTest{ @Before public void setUp() { // Kode som kjøres før hver test } @Test public void shouldSumCorrectly() { assertEquals(2, 1+2); } }
--- # Funksjonelle tester
###Fokuserer på å teste en spesifikk funksjon i applikasjonen.
- Verifiserer at grensesnitt følger kontrakten. - Kan fortelle oss at "noe i koden" brakk - Tester koden på tilsvarende måte som den blir konsumert. - Innvolverer typisk flere klasser ### Eksempler i WEB-app - Teste at kontrollerne håndterer forespørsler korrekt. - Verifisere at en HTML-form finnes på siden. - Ble brukeren redirigert til riktig side? - Er brukeren autentisert korrekt? --- # Akseptansetester
### Verifiserer at systemet tilfredsstiller kravene som er satt til løsningen.
- Tester systemet fra *brukerens* perspektiv - Høy-nivå tester som verifiserer hele brukerhistorier - Sikre at systemet løser *kravene* - Fokuserer *ikke* på hvordan et krav er løst ### Eksempler i en WEB-applikasjon - Verifisere at en HTML-form finnes på. - Verifisere at det er mulig å legge til en kommentar. - Verifisere at brukeren havner på forsiden etter å lagt til en kommentar. --- .center.middle # Play framework --- - Webrammeverk for java - Lært av Rails - Lettvekt - God testintegrasjon
.label[Fordeler] - Lært fra andres feil - Inkluderer hele webstacken - Automatisk rekompilering - Konvensjon over konfigurasjon
.label[Ulemper] - Begrenset dokumentasjon - Begrenset mengde bibliotek tilgjengelig
.center[.big[Så hvorfor har vi valgt Play i dag?]] --- # Plays arkitektur .center
--- # Demo
.label[Hva skal til for å starte en webapp i Play?] 1. Installer Java 1. Last ned og unzip .tt[play.zip] 1. Fra kommandolinje: .tt[play new minApplikasjon] 1. .tt[play run minApplikasjon] 1. Time to try!
.html ├── app │ ├── controllers │ │ └── Application.java │ ├── models │ └── views │ ├── Application │ │ └── index.html │ ├── errors │ │ ├── 404.html │ │ └── 500.html │ └── main.html ├── ... └── test ├── Application.test.html ├── ApplicationTest.java ├── BasicTest.java └── data.yml --- # Oppsett av play
.label[Win] - Installer .tt[java] hvis du ikke har JDK fra før (ligger på usb) - Kopier mappa .tt[play] til .tt[c:\dev\play] (fra usb) - Startmeny » Run/Kjør » Skriv .tt[cmd] (og trykk enter) - Skriv .tt[cd c:\dev\play] (og trykk enter) - Skriv .tt[play] (og trykk enter), får du ASCII-art tilbake så er alt OK
.label[Linux/Mac] - Installer .tt[java] hvis du ikke har JDK fra før (installasjonsbeskrivelse ligger på usb) - Kopier .tt[play] til hjemmekatalog (fra usb) - Åpne en terminal, .tt[cd play] - Kjør .tt[./play], får du ASCII-art tilbake så er alt OK
.label.problem[Problemer?] - Java må være installert, Play leter etter java på path eller gjennom variablen .tt[JAVA_HOME] - Python-issues » team opp med en annen - Andre problemer? » Hør med en av oss
--- .center.middle # Test-drevet utvikling --- # Eksempel
.label[TDD by example] Vi skal bruke TDD til å implementere metoden .tt[factorial(int n)] i Java. Denne metoden skal regne ut og returnere fakultetet til tallet n som sendes inn som argument til metoden. *Eksempler:*
.tt[2! = 2 x 1 = 2]
.tt[5! = 5 x 4 x 3x 2 x 1 = 120]
###Definisjon av fakultet:
--- .center # TDD: en itera*tiv* prosess
--- # TDD: fordeler - **Koden får god testdekning** - Gir tiltro til at koden er korrekt - Koden blir garantert testbar - God dokumentasjon - Gjør det trygt og enkelt å refaktorere kode - Beskytter kode ved fremtidige endringer - **TDD gir oss hurtig tilbakemelding** - Mindre bruk av debugger - Gir mulighet til å oppdage feil tidligere - **Reduserer mengden av "kjekt å ha kode"** - **Fokus på mindre deler av systemet om gangen** --- .center # TDD: ungå waste
--- #TDD tvinger utvikler til å fokusere på mindre biter av problemet - Må fokusere på grensesnitt før kode implementeres - Tar små steg i retning av løsningen - Oppnår modularisert og fleksibel kode som enkelt lar seg utvide - Mindre komplekst enn å designe en hel løsning upfront --- # TDD: svakheter - Må skrive mer kode totalt - Mange tester og høy testdekning gir ikke høyere **kodekvalitet** - Kan bare garantere et *minimum* av kvalitet. - Det er kvaliteten på testene (og koden) som er avgjørende - Er en krevende prosess - Vanskelig å skrive gode, små og konkrete tester - Tar lengre tid til vi får funksjonalitet? - Rammeverk og bibliotek legger føringer for løsningens design - Vi må ofte tilpasse hvordan vi planlegger designet vårt basert på rammeverk vi bruker. - Kan på kort sikt gjøre deg mindre produktiv - bruker tid på å løse alle småfeil tidlig. --- .center.middle
# Hovedpoenget med TDD er ikke å skrive testene først, men derimot at vi
iterere
over
designet
kontinuerlig og stadig gjør små
forbedringer
.
--- .center.middle # Workshop --- # Utfordringer ved test-drevet webutvikling
* Lite kontroll på klientsiden * Mange avhengigheter nødvendig » vanskelig å finne generelle test-løsninger * Integrasjon med database, nettverk, webtjenester
--- # Introduksjon til oppgave
.label[Hva vi skal] 1. "Todo"-applikasjon som - Gir mulighet for å legge til oppgaver - Lister opp oppgavene som er lagt til 1. Funksjonaliteten skal drives frem ved å skrive tester først (TDD)
.label[Hva vi _ikke_ skal] - Lære detaljer i Play - Bry oss om utseende/styling - Prøve å implementere mest mulig funksjonalitet
--- # Opprett applikasjonen
1. Start » Kjør/Run » .tt[cmd] 1. .tt[cd c:\dev\play] 1. .tt[play new todo] 1. .tt[play eclipsify todo] 1. I eclipse: File/Import/General/Existing project for å importere prosjektet 1. .tt[play test todo] 1. Åpne .tt[http://localhost:9000] i en nettleser
--- # Iterasjon 1
.label[Hva skal implementeres?] - Klassen .tt[models.Todo] skal kunne opprettes med feltene .tt["beskrivelse"] (description) og .tt["prioritet"] (priority). Dette skal lagres i instansen som instansvariabler.
.label[Tips] 1. Lag en unit-test som lager en .tt["Todo"] og verifiserer at properties blir satt 1. Se at testen feiler 1. Opprett klassen .tt[models.Todo] og implementer nødvendig funksjonalitet i klassens konstruktør
--- # LF Iterasjon 1 // Test: public class TodoTest extends UnitTest { @Test public void testThatWeCanCreateTodo() { Todo todo = new Todo("beskrivelse", 1); assertEquals("beskrivelse", todo.description); assertEquals(1, todo.priority); } }
// Implementasjon: package models; public class Todo { public String description; public int priority; public Todo(String description, int priority) { this.description = description; this.priority = priority; } } --- # Persistering * Play bruker Hibernate * Modeller som extender .tt[Model] og blir annotert med .tt[@Entity] lagres til en in-memory-database # Fixtures * Testoppsett * Play tilbyr .tt[Fixtures.deleteDatabase()] for å renske opp mellom hver test --- # Iterasjon 2
.label[Hva skal implementeres?] Instanser av modellen - inkludert alle datafelter - skal kunne lagres til database.
.label[Tips] - Opprett unit-test .tt[test/models/TodoTest.java] som lager en .tt[Todo], kaller .tt[save()] på instansen og ser om den har blitt lagret i databasen. - Fiks feilende test ved å extende modellen med "Model", og annoter klassen med .tt[@Entity] - Husk at tester skal kjøres i isolasjon, bruk JUnits .tt[@Before] og .tt[Fixtures.deleteDatabase()]
@Test // JUnit public void shouldPersistTodo() { // opprett en todo og deleger lagring til Hibernate ved å kalle save() // hent alle todos med Todo.findAll() // bruk assertEquals() til å verifisere at antall todos er som forventet } // Modell: @Entity // Hibernate-annotering public class Todo extends Model { // Model er en hjelpeklasse fra Play // ... } --- # LF Iterasjon 2 .java @Before public void setup() { Fixtures.deleteDatabase() } @Test public void testThatWeCanStoreTodo() { Todo todo = new Todo("beskrivelse", 1); todo.save(); List
todos = Todo.findAll(); assertEquals(1, todos.size()); Todo firstTodo = todos.get(0); assertEquals("beskrivelse", firstTodo.description); assertEquals(1, firstTodo.priority); }
@Entity public class Todo extends Model { public String description; public int priority; public Todo(String description, int priority) { this.description = description; this.priority = priority; } --- # Funksjonelle tester i play .center
--- # Iterasjon 3
.label[Hva skal implementeres?] Vi skal implementere kontrolleren som tilbyr GET på følgende URL: .tt[/todo/add].
.label[Tips] - Opprett unit-test .tt[test/controller/TodoFunctionalTest.java] som gjør en GET-forespørsel på .tt[/todo/add] og verifiserer at responsen er 200 ok. - Sjekk at testen feiler. - Fiks feilende test ved å opprette en *Todo kontroller* som har en "add" action.
.label[Om kontrollere] - Skal ligge i *controllers* pakken - Må arve fra *Controller* - Alle actions skal være **static** i play.
--- # LF Iterasjon 3 //Test public class TodoFunctionalTest extends FunctionalTest { @Test public void shouldReturnOkForAddTodoPage() { Response response = GET("/todo/add"); assertIsOk(response); } }
//Kode public class Todo extends Controller { public static void add() { render(); } } --- # Iterasjon 4
.label[Hva skal implementeres?] Vi skal implementere en action i todo-kontrolleren som tilbyr POST til følgende URL: .tt[/todo/create] med to parameter inn: *description* og *priority*. Metoden skal også opprette en ny *todo* basert på input og lagre den.
.label[Tips] - Legg til et nytt test-case i .tt[test/controller/TodoFunctionalTest.java] som gjør en POST til .tt[/todo/create]. - Sjekk at testen feiler. - Fiks feilende test. - Husk at tester skal kjøres i isolasjon, bruk JUnits .tt[@Before] og .tt[Fixtures.deleteDatabase()] - Du kan verifisere at en TODO ble opprettet ved å hente ut alle todo's via TODO.findAll().
--- # LF Iterasjon 4 public class TodoFunctionalTest extends FunctionalTest { @Before public void setUp() { Fixtures.deleteDatabase(); } @Test public void shouldCreateAndStoreTodo() { Map
params = new HashMap
(); params.put("description", "En beskrivelse"); params.put("priority", "1"); POST("/todo/create", params); assertEquals(1, Todo.findAll().size()); } }
public class Todo extends Controller { public static void create(String description, int priority) { models.Todo todo = new models.Todo(description, priority); todo.save(); } } --- # Iterasjon 5
.label[Hva skal implementeres?] Vi ønsker at det skal finnes en index-action på todo-kontrolleren slik at løsningen vår svarer på "todo/index".
.label[Tips] * Dette er en funksjonell test
--- # LF Iterasjon 5 public class TodoFunctionalTest extends FunctionalTest { @Test public void shoulGetTodoIndex() { Response response = GET("/todo/index"); assertIsOk(response); } }
public class Todo extends Controller { public static void index() { //No code yet } --- # Iterasjon 6
.label[Hva skal implementeres?] Vi skal viderutvikle create-metoden slik at den redirecter oss til .tt[/todo/index] etter at vi har opprettet en ny *todo*.
.label[Tips] - ved å kalle en annen metode i kontrolleren vil Play utføre en redirect for oss.
--- # LF Iterasjon 6 @Test public void shouldRedirectAfterStoringTodo() { Map
params = new HashMap
(); params.put("description", "En beskrivelse"); params.put("priority", "1"); Response response = POST("/todo/create", params); assertHeaderEquals("Location", "/todo/index", response); }
public static void create(String description, int priority) { models.Todo todo = new models.Todo(description, priority); todo.save(); index(); } --- # Selenium * Automatisering av nettlesere * Play tar i bruk dette for akseptansetesting og testing av view * Selenium bruker "Selenese" som språk, vi bruker et minimalt subsett i dag * Bruk enkelt-quote (') og ikke vanlig quote (")
.html open('/en_url') assertTitle('tittel') assertElementPresent('css=cssSelector') --- # Iterasjon 7
.label[Hva skal implementeres?] * Når jeg åpner .tt[/todo/add] skal tittel på siden være "Legg til en todo"
.label[Tips] - Selenium egner seg til testing av HTML-respons, opprett derfor .tt[test/Todo.test.html] - Ta en titt på Selenese sin .tt[assertTitle()] - Kjør testen via .tt[http://localhost:9000/@tests] - Fiks feilende test (opprett et view i .tt[app/views/todo/add.html])
// fil: test/Application.test.html #{selenium} // Open the home page, and check that no error occured open('/') assertNotTitle('Application error') #{/selenium} --- # LF Iterasjon 7 // Test #{selenium} // Open page, and check that no error occured open('/todo/add') assertTitle('Add Todo') #{/selenium}
.html // Implementasjon: #{extends 'main.html' /} #{set title:'Add Todo' /} --- # DOM-Selectors i selenium * Hva er DOM? * Hva er en selector? * Vis!
.javascript css=div css=div.aDiv css=div#myDiv --- # Iterasjon 8
.label[Hva skal implementeres?] - Når jeg åpner .tt[/todo/add] skal jeg bli presentert med en form som har input-elementer for .tt["beskrivelse"] og .tt["prioritet"] - Formen skal poste til .tt[/todo/create] og ha en submit-knapp
.label[Tips] - Selenium egner seg til testing av HTML-respons, så fortsett på .tt[test/Todo.test.html] - .tt[assertElementPresent('css=cssSelector')] kan brukes til å kreve at HTML-elementer er tilstede - Kjør testen via .tt[http://localhost:9000/@tests] - Fiks feilende test (oppdater view)
.html
Et form-element som over kan hentes med følende css-selectors: .html 'css=form', 'css=form.aForm', 'css=#myForm', 'css=form#myForm', 'css=form[method=POST]' --- # LF Iterasjon 8 // Test #{selenium} // Check that content contains a form open('/todo/add') verifyElementPresent('css=form') verifyElementPresent('css=input#description') verifyElementPresent('css=input#priority') verifyElementPresent('css=input[type=submit]') #{/selenium}
.html // Implementasjon:
Legg til en ny TODO:
Beskrivelse
Prioritet
--- # Iterasjon 9
.label[Hva skal implementeres?] * Når man åpner .tt[/todo/index] skal man få listen over eksisterende todos
.label[Tips] * Selenium passer til denne testen * Fixtures kan brukes til å mocke test-data * Man trenger en index-metode i Todo-kontrolleren som leverer alle Todos til viewet
--- # LF Iterasjon 9 // Test #{fixture delete:'all', load:'data.yml' /} #{selenium} open('/todo/index') verifyText('css=li:nth-child(1)', 'Viktig todo*') verifyText('css=li:nth-child(2)', 'Lala en kulere beskrivelse*') verifyText('css=li:nth-child(6)', 'Lala en kul beskrivels*') #{/selenium}
.html // Implementasjon: #{extends 'main.html' /} #{set title:'List todo' /}
Liste over planlagte oppgaver:
#{list items:todos, as:'todo'}
${todo.description} - ${todo.priority}
#{/list}
--- # Iterasjon 10
.label[Hva skal implementeres?] Vi ønsker å kreve at todo-er som opprettes har en beskrivelse.
.label[Tips] * En funksjonell test passer her. * Benytt @Required anotering i models * Man trenger en index-metode i Todo-kontrolleren som leverer alle Todos til viewet * Har en lagre-funksjon i Play som validerer og lagrer: .tt[todo.validateAndSave()] * Vi må også verifisere at requesten blir redirgert tilbake til .tt[/todo/add]
--- # LF Iterasjon 10 @Test public void shouldRequireDescription() { Map
params = new HashMap
(); params.put("priority", "1"); Response response = POST("/todo/create", params); assertHeaderEquals("Location", "/todo/add", response); } //Kontroller public static void create(String description, int priority) { models.Todo todo = new models.Todo(description, priority); if (!todo.validateAndSave()) { validation.keep(); add(); } index(); } //Modell @Entity public class Todo extends Model { @Required public String description; ... --- # Iterasjon 11
.label[Hva skal implementeres?] * Når det er feilmeldinger i skjemaet skal disse vises til brukeren i en liste
.label[Tips] * .tt[http://www.playframework.org/documentation/1.2.4/tags#errors]
--- # LF Iterasjon 11 // Test #{selenium} // Verify that error's are showed. open('/todo/add') type('css=input#description', '') type('css=input#priority', '') clickAndWait('css=input[type=submit]') verifyTextPresent('Error') #{/selenium}
.html // Implementasjon: #{extends 'main.html' /} #{set title:'Add Todo' /} #{ifErrors}
Error…
#{errors}
${error}
#{/errors} #{/ifErrors}
Legg til en ny TODO:
--- # Ressurser 1. Test Driven Development: By Example - Kent Beck 2. Introduction to Test Driven Development - http://www.agiledata.org/essays/tdd.html 3. The Three Laws of TDD - Uncle Bob - http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd 4. http://en.wikipedia.org/wiki/Test-driven_development --- .center.middle # Takk for oss!
(Slideshow was created using [remark](http://github.com/gnab/remark).)