Skip to content

laskari 5

Matti Luukkainen edited this page Jun 19, 2016 · 20 revisions
Tehtävien palautuksen deadline su 24.4. klo 23.59

Ohjausta tehtävien tekoon to 21.4. 14-17 B221

palautetaan GitHubin kautta

  • palautusta varten voit käyttää samaa repoa kuin esim. viikon 2 tehtävissä
  • palautusrepositorion nimi ilmoitetaan tehtävien lopussa olevalla palautuslomakkeella

1. Yksikkötestaus ja riippuvuudet: Mockito, osa 1

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/ohtu2016 hakemistossa viikko5/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

2. Yksikkötestaus ja riippuvuudet: Mockito, osa 2

Hae repositorion https://github.com/mluukkai/ohtu2016 hakemistossa viikko5/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 metodia lataa 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.

3. Yksikkötestaus ja riippuvuudet: Mockito, osa 3

Testataan viikolta 2 tutun Verkkokaupan Kauppa-luokkaa

  • Spring-versio löytyy https://github.com/mluukkai/ohtu2016 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ä testeissä 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-olioiden käytöstä

Mock-oliot saattoivat tuntua hieman monimutkaisilta edellisissä 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.

4. IntJoukon testaus ja siistiminen

  • repositorion https://github.com/mluukkai/ohtu2016 hakemistosta viikko5/IntJoukkoSovellus aloittelevan ohjelmoijan ratkaisu syksyn 2011 Ohjelmoinnin Jatkokurssin viikon 2 tehtävään 3 (ks. http://www.cs.helsinki.fi/u/wikla/ohjelmointi/jatko/s2011/harjoitukset/2/)
    • ratkaisussa joukko-operaatiot on toteutettu suoraan luokkaan IntJoukko staattisina metodeina
  • koodi jättää hieman toivomisen varaa ylläpidettävyyden suhteen
  • refaktoroi luokan IntJoukko koodi mahdollisimman siistiksi
    • copypaste pois
    • muuttujille selkeät nimet
    • ei pitkiä (yli 8 rivisiä) metodeja
  • koodissa on refaktorointia helpottamaan joukko yksikkötestejä
    • kirjoita testejä tarpeen vaatiessa lisää, ne eivät ole välttämättä 100% kattavat

HUOM refaktoroi mahdollisimman pienin askelin ja pidä koodi koko ajan toimivana. Aja testit aina jokaisen refaktorointiaskeleen jälkeen! Järkevä refaktorointiaskeleen koko pieni muutos yhteen metodiin.

5. Tenniksen pisteenlaskun refaktorointi

Repositorion https://github.com/mluukkai/ohtu2016 hakemistosta viikko5/Tennis löytyy ohjelma joka on tarkoitettu tenniksen pisteenlaskentaan.

Pisteenlaskennan rajapinta on yksinkertainen. Metodi void getScore() kertoo voimassa olevan tilanteeen tennispisteenlaskennan määrittelemän tavan mukaan. Sitä mukaa kun jompi kumpi pelaajista voittaa palloja, kutsutaan metodia void wonPoint(String player) jossa parametrina on pallon voittanut pelaaja.

Esim. käytettäessä pisteenlaskentaa seuraavasti:

public static void main(String[] args) {
    TennisGame game = new TennisGame("player1", "player2");

    System.out.println(game.getScore());

    game.wonPoint("player1");
    System.out.println(game.getScore());

    game.wonPoint("player1");
    System.out.println(game.getScore());

    game.wonPoint("player2");
    System.out.println(game.getScore());

    game.wonPoint("player1");
    System.out.println(game.getScore());

    game.wonPoint("player1");
    System.out.println(game.getScore());
}

tulostuu

Love-All
Fifteen-Love
Thirty-Love
Thirty-Fifteen
Forty-Fifteen
Win for player1

Tulostuksessa siis kerrotaan mikä on pelitilanne kunkin pallon jälkeen kun player1 voittaa ensimmäiset 2 palloa, player2 kolmannen pallon ja player1 loput 2 palloa.

Pisteenlaskentaohjelman koodi toimii ja sillä on erittäin kattavat testit. Koodi on kuitenkin luettavuudeltaan erittäin huonossa kunnossa.

Tehtävänä on refaktoroida koodi luettavuudeltaan mahdollisimman ymmärrettäväksi. Koodissa tulee välttää "taikanumeroita" ja huonosti nimettyjä muuttujia. Koodi kannattaa jakaa moniin pieniin metodeihin, jotka nimennällään paljastavat oman toimintalogiikkansa.

Etene refaktoroinnissa todella pienin askelin. Aja testejä mahdollisimman usein. Yritä pitää ohjelma koko ajan toimintakunnossa.

Jos haluat käyttää jotain muuta kieltä kuin Javaa, löytyy koodista ja testeistä versioita useilla eri kielillä osoitteesta https://github.com/emilybache/Tennis-Refactoring-Kata

Tehtävä on kenties hauskinta tehdä pariohjelmoiden. Itse tutustuin tehtävään kesällä 2013 Extreme Programming -konferenssissa järjestetyssä Coding Dojossa, jossa tehtävä tehtiin satunnaisesti valitun parin kanssa pariohjelmoiden.

Lisää samantapaisia refaktorointitehtäviä osoitteessa Emily Bachen GitHub-sivulta

6. git: vahingossa tuhotun tiedoston palautus

tehtävien 6, 7 ja 8 ei tarvitse näkyä palautuksessa, riittää kun teet tehtävät

  • viikon 4 tehtävässä 6 palasimme jo menneisyyteen checkouttaamalla tagillä merkittyyn kohtaan
  • katsotaan nyt miten voimme palauttaa jonkun menneisyydessä olevan tilanteen uudelleen voimaan
  • tee tiedosto xxx, lisää ja committaa se
  • poista tiedosto ja committaa
  • tee jotain muutoksia johonkin tiedostoon ja committaa
  • historiasi näyttää seuraavalta
(1) - (2) - (3)
  • Nykyhetki eli HEAD on (3). Commitissa (1) tiedosto xxx on olemassa, nykyhetkellä ja (2):ssa xxx:ää ei ole.

    • huom: komennolla gitk voit tutkia historiaa
  • haluamme palauttaa tiedoston

  • selvitä sen commitin id, jossa tiedosto vielä on olemassa, tämä onnistuu gitk:lla tai git log -komennolla

  • anna komento git checkout 3290b03cea08af987ee7ea57bb98a4886b97efe0 -- xxx missä pitkä merkkijono on siis kyseisen commitin id

    • varmista että tiedosto on ilmestynyt staging-alueelle komennolla git status
  • tee commit

  • xxx on palannut!

  • HUOM: koko id:tä ei komennossa tarvitse antaa, riittää antaa alusta niin monta merkkiä, että niiden perusteella id voidaan päätellä yksikäsitteisesti repositoriosi historiassa

  • Täsmälleen samalla tavalla onnistuu olemassaolevan tiedoston vanhan version palauttaminen.

7. git: commitin muutosten kumoaminen, branchin "siirtäminen"

tehtävien 6, 7 ja 8 ei tarvitse näkyä palautuksessa, riittää kun teet tehtävät

  • huomaamme, että juuri tehty commit oli virhe, kumotaan se sanomalla git revert HEAD --no-edit
    • HEAD siis viittaa siihen committiin minkä kohdalla nyt ollaan
  • syntyy uusi commit, jossa edellisessä tehdyt muutokset on kumottu
    • ilman optiota no-edit pääset editoimaan kumoamiseen liittyvään commitiin tulevaa viestiä
    • huom: sanomalla git checkout HEAD^ pääsemme takaisin kumottuun tilanteeseen, eli mitään ei ole lopullisesti kadotettu
  • vastaavalla tavalla voidaan revertata mikä tahansa commit eli: git revert kumottavancommitinid

8. git: branchin "siirtäminen"

tehtävien 6, 7 ja 8 ei tarvitse näkyä palautuksessa, riittää kun teet tehtävät

  • tee repoosi branchi nimeltä haara ja tee masteriin ja haaraan committeja siten että saat aikaan seuraavankaltaisen tilanteen:
/------master
--
   \---haara
  • eli sekä master että haara ovat edenneet muutamien commitien verran haarautumisen tapahduttua
    • huom: komennolla gitk --all näet kaikki haarat, kokeile!
  • yhtäkkiä huomaat, että master:iin tekemäsi asiat eivät olekaan kovin hyviä ja haara:ssa on paljon parempaa tavaraa, haluaisitkin että haara:sta tulisi uusi master
  • tämä onnistuu kun menet masteriin ja annat komennon git reset --hard haara
    • varmista että komento toimii oikein
    • vanhan master-haarankaan tavarat eivät katoa mihinkään, jos niihin jostain syystä vielä halutaan palata

tehtävien kirjaaminen palautetuksi

tehtävien kirjaus:

  • Kirjaa tekemäsi tehtävät tänne
    • huom: tehtävien palautuksen deadline on su 24.4. klo 23.59

palaute tehtävistä:

  • Lisää viikon 1 tehtävässä 11 forkaamasi repositorion omalla nimelläsi olevaan hakemistoon tiedosto nimeltä viikko5
  • 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