-
Notifications
You must be signed in to change notification settings - Fork 57
Laskari 4
Ohjausta tehtävien tekoon pe 10.4 klo 12-14 (Pihla UMT) & klo 16-18 (doge), B221. Huom. Pääsiäisloman aikana ei ohjausta!
- palautusta varten tarvitaan yksityinen repositorio, jolla collaboratorina käyttäjä mluukkai
- kannattaa käyttää samaa repoa kuin viikon 2 ja 3 tehtävissä
- palautusrepositorion nimi ilmoitetaan tehtävien lopussa olevalla palautuslomakkeella
Useimmilla luokilla on riippuvuuksia toisiin luokkiin. Esim. viikon 2 verkkokauppaesimerkin luokka Kauppa riippui Pankista, Varastosta ja Viitegeneraattorista. Riippuvuuksien injektoinnilla ja rajapinnoilla saimme mukavasti purettua riippuvuudet konreettisten luokkien väliltä.
Vaikka luokilla ei olisikaan riippuvuuksia toisiin konkreettisiin luokkiin, on tilanne edelleen se, että luokan oliot käyttävät joidenkin toisten luokkien olioiden palveluita. Tämä tekee joskus yksikkötestauksesta hankalaa. Miten esim. luokkaa Kauppa tulisi testata? Tuleeko Kaupan testeissä olla mukana toimivat versiot kaikista sen riippuvuuksista?
Olemme jo muutamaan otteeseen (esim. Nhl-Statsreader-tehtävässä viikolla 2) ratkaisseet asian ohjelmoimalla riippuvuuden korvaavan "tynkäkomponentin". Javalle (niinkuin kaikille muillekin kielille) on tarjolla myös valmiita kirjastoja tynkäkomponenttien toiselta nimeltään "mock-olioiden" luomiseen.
Kuten pian huomaamme, mock-oliot eivät ole pelkkiä "tynkäolioita", mockien avulla voi myös varmistaa että testattava luokka kutsuu olioiden metodeja asiaankuuluvalla tavalla.
Tutustumme nyt Mockito-nimiseen mock-kirjastoon. Muita vaihtoehtoja esim.
Hae repositorion https://github.com/mluukkai/ohtu2015 hakemistossa viikko4/MockitoDemo oleva projekti. Kyseessä on yksinkertaistettu versio Verkkokauppaesimerkistä.
Kaupan toimintaperiaate on yksinkertainen:
Pankki myNetBank = new Pankki();
Viitegeneraattori viitteet = new Viitegeneraattori();
Kauppa kauppa = new Kauppa(myNetBank, viitteet);
kauppa.aloitaOstokset();
kauppa.lisaaOstos(5);
kauppa.lisaaOstos(7);
kauppa.maksa("1111");
Ostokset aloitetaan tekemällä metodikutsu aloitaOstokset
. Tämän jälkeen "ostoskoriin" lisätään tuotteita joiden hinta kerrotaan metodin lisaaOstos
parametrina. Ostokset lopetetaan kutsumalla metodia maksa
joka saa parametrikseen tilinumeron jolta summa veloitetaan.
Kauppa tekee veloituksen käyttäen tuntemaansa luokan Pankki
olioa. Viitenumerona käytetään luokan Viitegeneraattori
generoimaa numeroa.
Projektiin on kirjoitettu 6 Mockitoa hyödyntävää testiä. Testit testaavat, että kauppa tekee ostoksiin liittyvän veloituksen oikein, eli että se kutsuu pankin metodia maksa
oikeilla parametreilla, ja että jokaiselle laskutukselle on kysytty viitenumero viitegeneraattorilta. Testit siis eivät kohdistu olion pankki tilaan vaan sen muiden olioiden kanssa käymän interaktion oikeellisuuteen.
Testeissä kaupan riippuvuudet (Pankki ja Viitegeneraattori) on määritelty mock-olioina.
Seuraavassa testi, joka testaa, että kauppa kutsuu pankin metodia oikealla tilinumerolla ja summalla:
@Test
public void kutsutaanPankkiaOikeallaTilinumerollaJaSummalla() {
Pankki mockPankki = mock(Pankki.class);
Viitegeneraattori mockViite = mock(Viitegeneraattori.class);
kauppa = new Kauppa(mockPankki, mockViite);
kauppa.aloitaOstokset();
kauppa.lisaaOstos(5);
kauppa.lisaaOstos(5);
kauppa.maksa("1111");
verify(mockPankki).maksa(eq("1111"), eq(10), anyInt());
}
Testi siis aloittaa luomalla kaupan riippuvuuksista mock-oliot:
Pankki mockPankki = mock(Pankki.class);
Viitegeneraattori mockViite = mock(Viitegeneraattori.class);
kauppa = new Kauppa(mockPankki, mockViite);
kyseessä siis eivät ole normaalit oliot vaan normaaleja olioita "matkivat" valeoliot, jotka myös pystyvät tarkastamaan että niiden metodeja on kutsuttu oikein parametrein.
Testi tarkastaa, että kaupalle tehdyt metodikutsut aiheuttavat sen, että pankin mock-olion metodia maksa
on kutsuttu oikeilla parametreilla. Kolmanteen parametriin eli tilinumeroon ei kiinnitetä huomiota:
verify(mockPankki).maksa(eq("1111"), eq(10), anyInt());
Mock-olioille tehtyjen metodikutsujen paluuarvot on myös mahdollista määritellä. Seuraavassa määritellään, että viitegeneraattori palauttaa arvon 55 kun sen metodia seuraava
kutsutaan:
@Test
public void kaytetaanMaksussaPalautettuaViiteta() {
Pankki mockPankki = mock(Pankki.class);
Viitegeneraattori mockViite = mock(Viitegeneraattori.class);
// määritellään viitegeneraattorin metodikutsun vastaus
when(mockViite.seuraava()).thenReturn(55);
kauppa = new Kauppa(mockPankki, mockViite);
kauppa.aloitaOstokset();
kauppa.lisaaOstos(5);
kauppa.lisaaOstos(5);
kauppa.maksa("1111");
verify(mockPankki).maksa(eq("1111"), eq(10), eq(55));
}
Testin lopussa varmistetaan, että pankin mockolioa on kutsuttu oikeilla parametrinarvoilla, eli kolmantena parametrina tulee olla viitegeneraattorin palauttama arvo.
Tutustu projektiin ja sen kaikkiin testeihin.
Mockiton dokumentaatio: http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html
Hae repositorion https://github.com/mluukkai/ohtu2015 hakemistossa viikko4/LyyrakorttiMockito oleva projekti. Kyseessä on yksinkertaistettu versio ohjelmoinnin perusteista tutusta tehtävästä Kassapääte ja tyhmä lyyrakortti.
Tässä tehtävässä on tarkoitus testata ja täydentää luokkaa Kassapaate
. Lyyrakortin koodiin ei tehtävässä saa koskea ollenkaan! Testeissä ei myöskään ole tarkoitus luoda konkreettisia instansseja lyyrakortista, testien tarvitsemat kortit tulee luoda mockitolla.
Projektissa on valmiina kaksi testiä:
public class KassapaateTest {
Kassapaate kassa;
Lyyrakortti kortti;
@Before
public void setUp() {
kassa = new Kassapaate();
kortti = mock(Lyyrakortti.class);
}
@Test
public void kortiltaVelotetaanHintaJosRahaaOn() {
when(kortti.getSaldo()).thenReturn(10);
kassa.ostaLounas(kortti);
verify(kortti, times(1)).getSaldo();
verify(kortti).osta(eq(Kassapaate.HINTA));
}
@Test
public void kortiltaEiVelotetaJosRahaEiRiita() {
when(kortti.getSaldo()).thenReturn(4);
kassa.ostaLounas(kortti);
verify(kortti, times(1)).getSaldo();
verify(kortti, times(0)).osta(anyInt());
}
}
Ensimmäisessä testissä varmistetaan, että jos kortilla on riittävästi rahaa, kassapäätteen metodin ostaLounas
kutsuminen
varmistaa kortin saldon ja velottaa summan kortilta.
Testi ottaa siis kantaa ainoastaan siihen miten kassapääte kutsuu lyyrakortin metodeja. Lyyrakortin saldoa ei erikseen tarkasteta, sillä oletuksena on, että lyyrakortin omat testit varmistavat kortin toiminnan.
Toinen testi varmistaa, että jos kortilla ei ole riittävästi rahaa, kassapäätteen metodin ostaLounas
kutsuminen
varmistaa kortin saldon mutta ei velota kortilta rahaa.
Testit eivät mene läpi. Korjaa kassapäätteen metodi ostaLounas
.
Tee tämän jälkeen samaa periaatetta noudattaen seuraavat testit:
- kassapäätteen metodin
lataa
kutsu lisää lyyrakortille ladattavan rahamäärän käyttäen kortin metodialataa
jos ladattava summa on positiivinen - kassapäätteen metodin
lataa
kutsu ei tee lyyrakortille mitään jos ladattava summa on negatiivinen
Korjaa kassapäätettä siten, että määrittelemäsi testit menevät läpi.
Testataan viikolta 2 tutun Verkkokaupan Kauppa-luokkaa
- Spring-versio löytyy https://github.com/mluukkai/ohtu2015 hakemistossa viikko2/Verkkokauppa3 (xml:llä konfiguroitu) ja viikko2/Verkkokauppa4 (annotaatioilla konfiguroitu)
- ota edellisistä jompi kumpi pohjaksi jos et tehnyt tehtävää
Kaupalle injektoidaan konstruktorissa Pankki, Viitelaskuri ja Varasto.
Tehdään näistä testissä Mockitolla mockatut versiot.
Seuraavassa esimerkkinä testi, joka testaa, että ostostapahtuman jälkeen pankin metodia tilisiirto on kutsuttu:
@Test
public void ostoksenPaaytyttyaPankinMetodiaTilisiirtoKutsutaan() {
// luodaan ensin mock-oliot
Pankki pankki = mock(Pankki.class);
Viitegeneraattori viite = mock(Viitegeneraattori.class);
// määritellään että viitegeneraattori palauttaa viitten 42
when(viite.uusi()).thenReturn(42);
Varasto varasto = mock(Varasto.class);
// määritellään että tuote numero 1 on maito jonka hinta on 5 ja saldo 1
when(varasto.saldo(1)).thenReturn(10);
when(varasto.haeTuote(1)).thenReturn(new Tuote(1, "maito", 5));
// sitten testattava kauppa
Kauppa k = new Kauppa(varasto, pankki, viite);
// tehdään ostokset
k.aloitaAsiointi();
k.lisaaKoriin(1); // ostetaan tuotetta numero 1 eli maitoa
k.tilimaksu("pekka", "12345");
// sitten suoritetaan varmistus, että pankin metodia tilisiirto on kutsuttu
verify(pankki).tilisiirto(anyString(), anyInt(), anyString(), anyString(),anyInt());
// toistaiseksi ei välitetty kutsussa käytetyistä parametreista
}
Tee seuraavat testit:
- aloitetaan asiointi, koriin lisätään tuote ,jota varastossa on ja suoritetaan ostos (eli kutsutaan metodia kaupan tilimaksu()). varmistettava että kutsutaan pankin metodia tilisiirto oikealla asiakkaalla, tilinumerolla ja summalla
- tämä siis on muuten copypaste esimerkistä, mutta verify:ssä on tarkastettava että parametreilla on oikeat arvot
- aloitetaan asiointi, koriin lisätään kaksi eri tuotetta, joita varastossa on ja suoritetaan ostos. varmistettava että kutsutaan pankin metodia tilisiirto oikealla asiakkaalla, tilinumerolla ja summalla
- aloitetaan asiointi, koriin lisätään kaksi samaa tuotetta jota on varastossa tarpeeksi ja suoritetaan ostos. varmistettava että kutsutaan pankin metodia tilisiirto oikealla asiakkaalla, tilinumerolla ja summalla
- aloitetaan asiointi, koriin lisätään tuote jota on varastossa tarpeeksi ja tuote joka on loppu ja suoritetaan ostos. varmistettava että kutsutaan pankin metodia tilisiirto oikealla asiakkaalla, tilinumerolla ja summalla
- varmistettava, että metodin
aloitaAsiointi
kutsuminen nollaa edellisen ostoksen tiedot (eli edellisen ostoksen hinta ei näy uuden ostoksen hinnassa), katso tarvittaessa apua projektin MockitoDemo testeistä! - varmistettava, että kauppa pyytää uuden viitenumeron jokaiselle maksutapahtumalle, katso tarvittaessa apua projektin MockitoDemo testeistä!
Kaikkien testien tarkastukset onnistuvat mockiton verify-komennolla.
Tarkasta vanhan ystävämme coberturan avulla mikä on luokan Kauppa testauskattavuus. Jotain taitaa puuttua. Lisää testi joka nostaa kattavuuden noin sataan prosenttiin!
Muista lisätä pom.xml-tiedoston riippuvuuksiin mockito:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.0</version>
<scope>test</scope>
</dependency>
Lisää testitiedostoosi import:
import static org.mockito.Mockito.*;
Mock-oliot saattoivat tuntua hieman monimutkaisilta edellisisssä tehtävissä. Mockeilla on kuitenkin paikkansa. Jos testattavana olevan olion riippuvuutena oleva olio on monimutkainen, kuten esim. verkkokauppaesimerkissä luokka Pankki
, kannattaa testattavana oleva olio testata ehdottomasti ilman todellisen riippuvuuden käyttöä testissä. Valeolion voi toki tehdä myös "käsin", mutta tietyissä tilanteissa mock-kirjastoilla tehdyt mockit ovat käsin tehtyjä valeolioita kätevämpiä, erityisesti jos on syytä tarkastella testattavan olion riippuvuuksille tekemiä metodikutsuja.
Tehtävässä oletetaan, että sinulla on 2 repositoria GitHub:issa. Käytetään niistä nimiä A ja B
- kloonaa A koneellesi
- liitä B paikalliselle koneelle kloonaamaasi repositorioon etärepositorioksi
- nyt paikallisella repositoriollasi on kaksi remotea A (nimellä origin) ja B (määrittelemälläsi nimellä)
- tarkista komennolla git remote -v että näin todellakin on
- pullaa B:n master-haaran sisältö paikalliseen repositorioon (komennolla
git pull beelleantamasinimi master
)- pullaus siis aiheuttaa sen, että etärepositorin master-haara mergetään lokaalin repositorion masteriin, tämä voi aiheuttaa konfliktin, jos, niin ratkaise konflikti!
- pushaa paikallisen repositorion sisältö A:han eli originiin
jatketaan edellistä
- liitä paikalliseen repositorioosi (edellisen tehtävän A) remoteksi repositorio
git://github.com/mluukkai/ohtu2015.git
esim. nimellä ohtu - ei pullata ohtu-repossa olevaa tavaraa lokaaliin, vaan tehdään sille oma träkkäävä branchi:
- anna komennot
git fetch ohtu
jagit checkout -b ohtu-lokaali ohtu/master
- varmista komennolla
git branch
että branchi (nimeltä ohtu-lokaali) syntyi ja että olet branchissa - tee komento
ls
niin näet, että olet todellakin ohtu-repon lokaalissa kopiossa
- anna komennot
- siirretään (tai otetaan mukaan, alkuperäinen ei häviä) ohtu-lokaali:n hakemisto viikko2 paikallisen repon masteriin:
- palaa master-branchiin komennolla
git checkout master
- liitä branchin ohtu-lokaali hakemisto viikko2 masteriin komennolla
git checkout ohtu-lokaali viikko2
- varmista että hakemisto on nyt staging-alueella komennolla
git status
- committaa
- nyt sait siirrettyä sopivan osan toisen etärepositorion tavarasta lokaaliin repositorioon!
- palaa master-branchiin komennolla
- pushaa lokaalin repositorion sisältö sekä originiin että B:hen
Tee tämä tehtävä repositorioon, jonka palautat
- Lue http://git-scm.com/book/en/Git-Basics-Tagging (kohdat signed tags ja verifying tags voit skipata)
- tee tägi nimellä tagi1 (lightweight tag riittää)
- tee kolme committia (eli 3 kertaa muutos+add+commit )
- tee tägi nimellä tagi2
- katso
gitk
-komennolla miltä historiasi näyttää - palaa tagi1:n aikaan, eli anna komento
git checkout tagi1
- varmista, että tagin jälkeisiä muutoksia ei näy
- kokeile komentoa
git status
, mitä erikoista huomaat!
- palaa nykyaikaan
- tämä onnistuu komennolla
git checkout master
- tämä onnistuu komennolla
- lisää tägi edelliseen committiin
- onnistuu komennolla
git tag tagi1b HEAD^
, eli HEAD^ viittaa nykyistä "headia" eli olinpaikkaa historiassa edelliseen committiin - joissain windowseissa muoto
HEAD^
ei toimi, sen sijasta voit käyttää muotoaHEAD~
- tai katsomalla commitin tunniste (pitkä numerosarja) joko komennolla
git log
tai gitk:lla
- onnistuu komennolla
- kokeile molempia tapoja, tee niiden avulla kahteen edelliseen committiin tagit (tagi1a ja tagi1b)
- katso komennolla
gitk
miltä historia näyttää
Tagit eivät mene automaattisesti etärepositorioihin. Pushaa koodisi githubiin siten, että myös tagit siirtyvät mukana. Katso ohje täältä
Varmista, etä tagit siirtyvät Githubiin:
Tarkastellaan edelliseltä viikolta tutun toiminnallisuuden tarjoamaa esimerkkiprojektia joka löytyy repositorion https://github.com/mluukkai/ohtu2015 hakemistossa viikko4/LoginWeb2
Hae projekti ja käynnistä se komennolla
mvn jetty:run
Jetty on keyvt HTTP-palvelin ja Servlettien ajoympäristö. Projektiin on konfiguroitu Jetty Maven-pluginiksi. Jos kaikki menee hyvin, on sovellus nyt käynnissä ja voit käyttää sitä web-selaimella osoitteesta http://localhost:8090 eli paikalliselta koneeltasi portista 8090.
Jos koneellasi on jo jotain muuta portissa 8090, voit konfiguroida sovelluksen käynnistymään johonkin muuhun porttiin esim. 9999:n seuraavasti:
mvn -D jetty.port=9999 jetty:run
SpringWebMVC:stä tällä kurssilla ei tarvitse ymmärtää. Kannattaa kuitenkin vilkaista tiedostoa ohtu.OhtuController.java, joka sisältää sovelluksen eri osoitteisiin tulevista kutsuista huolehtivan koodin. Kontrolleri käyttää AuthenticationService-luokkaa toteuttamaan kirjautumisen tarkastuksen ja uusien käyttäjien luomisen. Kontrolleri delegoi www-sivujen renderöinnin hakemiston WebPages/WEB-INF-views alla oleville jsp-tiedostoille.
Eli tutustu nyt sovelluksen rakenteeseen ja toiminnallisuuteen. Saat sammutettua sovelluksen painamalla konsolissa ctrl+c tai ctrl+d.
Web-selaimen simulointi onnistuu mukavasti Selenium WebDriver -kirjaston avulla. Edellisessä tehtävässä olevassa projektissa on luokassa ohtu.Tester.java pääohjelma, jonka koodi on seuraava:
public static void main(String[] args) {
WebDriver driver = new HtmlUnitDriver();
driver.get("http://localhost:8090");
System.out.println( driver.getPageSource() );
WebElement element = driver.findElement(By.linkText("login"));
element.click();
System.out.println("==");
System.out.println( driver.getPageSource() );
element = driver.findElement(By.name("username"));
element.sendKeys("pekka");
element = driver.findElement(By.name("password"));
element.sendKeys("akkep");
element = driver.findElement(By.name("login"));
element.submit();
System.out.println("==");
System.out.println( driver.getPageSource() );
}
Käynnistä sovellus edellisen tehtävän tapaan komentoriviltä. Varmista selaimella että sovellus on päällä.
Aja Tester.java:ssa oleva ohjelma. Esim. NetBeansilla tämä onnistuu valitsemalla tiedoston nimen kohdalta oikealla hiiren napilla "Run file".
Katso mitä ohjelma tulostaa.
Tester-ohjelmassa luodaan alussa selainta simuloiva olio WebDriver driver. Tämän jälkeen "mennään" selaimella osoitteeseen localhost:8090 ja tulostetaan sivun lähdekoodi. Tämän jälkeen haetaan sivulta elementti, jossa on linkkiteksti login eli
WebElement element = driver.findElement(By.linkText("login"));
Linkkielementtiä klikataan ja jälleen tulostetaan sivun lähdekoodi. Seuraavaksi etsitään sivulta elementti, jonka nimi on username, kyseessä on lomakkeen input-kenttä, ja ohjelma "kirjoittaa" kenttään komennolla sendKeys() nimen "pekka".
Tämän jälkeen täytetään vielä salasanakenttä ja painetaan lomakkeessa olevaa nappia. Lopuksi tulostetaan vielä sivun lähdekoodi.
Ohjelma siis simuloi selaimen käyttöskenaarion, jossa kirjaudutaan sovellukseen.
Muuta koodia siten, että läpikäyt seuraavat skenaariot:
- epäonnistunut kirjautuminen: oikea käyttäjätunnus, väärä salasana
- epäonnistunut kirjautuminen: ei-olemassaoleva käyttäjätunnus
- uuden käyttäjätunnuksen luominen
- uuden käyttäjätunnuksen luomisen jälkeen tapahtuva ulkoskirjautuminen sovelluksesta
HUOM: salasanan varmistuskentän (confirm password) nimi on passwordConfirmation
Pääsemme jälleen käyttämään viime viikolta tuttua easyB:tä. Hakemistosta Other Test Sources/easyb löytyy valmiina User storyn User can log in with valid username/password-combination määrittelevä story. Yksi skenaarioista on valmiiksi mäpätty koodiin. Täydennä kaksi muuta skenaariota.
Kuten viime viikolta muistamme, toinen järjestelmän toimintaa määrittelevä User story on A new user account can be created if a proper unused username and a proper password are given
Löydät tämän Storyn easyB-pohjan viime viikon tehtävistä. Kopioi story projektiisi ja tee skenaarioista suoritettavia kirjoittamalla niihin Seleniumin avulla (edellisen tehtävän tyyliin) sovellusta testaavaa koodia. Muista lisätä story-tiedostoon Seleniumin vaatimat importit!
Huomioita
- voit tehdä Tester.java:n tapaisen pääohjelman sisältävän luokan jos haluat/joudut debuggaamaan testiä.
- Uuden käyttäjän luomisen pohjalla käytettävään luokkaan UserData on määritelty validoinnit käyttäjätunnuksen muodon ja salasanan oikeellisuuden tarkastamiseksi. Eli toisin kuin viime viikolla, ei AuthenticationServicen tarvitse suorittaa validointeja.
- Skenaarion "can login with succesfully generated account" mäppäävän koodin kirjoittaminen ei ole täysin suoraviivaista. Koska luotu käyttäjä kirjautuu automaattisesti järjestelmään, joudut kirjaamaan käyttäjän ensin ulos ja kokeilemaan tämän jälkeen että luotu käyttäjä pystyy kirjautumaan sivulle uudelleen.
- Huomaa, että jos luot käyttäjän yhdessä testissä, et voi luoda toisessa testissä samannimistä käyttäjää uudelleen!
tehtävien kirjaus:
- Kirjaa tekemäsi tehtävät tänne
- huom: tehtävien palautuksen deadline on su 12.4. klo 23.59
palaute tehtävistä:
- Lisää viikon 1 tehtävässä 11 forkaamasi repositorion omalla nimelläsi olevaan hakemistoon tiedosto nimeltä viikko4
- tee viime viikon tehtävän tapaan pull-request
- anna tehtävistä palautetta avautuvaan lomakkeeseen
- huom: jos teet tehtävät alkuviikosta, voi olla, että edellistä pull-requestiasi ei ole vielä ehditty hyväksyä ja et pääse vielä tekemään uutta requestia