-
Notifications
You must be signed in to change notification settings - Fork 2
JUnit ohje
Tutustutaan yksikkötestien tekemiseen JUnit-testauskehyksen avulla. Yksikkötesteissä testauksen kohteena ovat ohjelman pienimmät rakenneosaset eli yksittäiset oliot ja niiden metodit.
Esimerkkinä seuraavassa Ohjelmoinnin perusteista tuttu luokka LyyraKortti (luokan nykyinen nimi tosin on Maksukortti, kyseessä kuitenkin täsmälleen sama koodi). Kortin koodi:
public class LyyraKortti {
private double arvo;
private final double EDULLINEN = 2.5;
private final double MAUKAS = 4.0;
public LyyraKortti(double arvoaAlussa) {
this.arvo = arvoaAlussa;
}
public void syoEdullisesti() {
if (this.arvo >= EDULLINEN) {
this.arvo -= EDULLINEN;
}
}
public void syoMaukkaasti() {
if (this.arvo > MAUKAS) {
this.arvo -= MAUKAS;
}
}
public void lataaRahaa(double rahamaara) {
if (rahamaara < 0) {
return;
}
this.arvo += rahamaara;
if (this.arvo > 150) {
this.arvo = 150;
}
}
@Override
public String toString() {
return "Kortilla on rahaa " + this.arvo + " euroa";
}
}
Palauta mieleen kortin käyttötapa lukemalla tehtäväkuvaus
Tehdään NetBeans-projekti, luo luokka LyyraKortti ja copypastea siihen yllä oleva koodi. Lopputuloksen pitäisi näytää seuraavalta:
Jos et osaa käyttää NetBeansia, lue tämä ja kysy tarvittaessa apua ohjaajalta. Tällä kurssilla tullaan käyttämään sen verran modernien ohjelmointiympäristöjen edistyneitä piirteitä että tekstieditorin käyttö ohjelmointitehtävissä on hankalaa. Netbeansin lisäksi sopivia ohjelmointiympäristöjä ovat Eclipse ja IntelliJ, niille ei kuitenkaan tule ohjeita.
Seuraavaksi aloitetaan testien luominen. Klikataan projektia hiiren oikealla näppäimellä ja valitaan new > other > Unit Tests > JUnit test > next. Annetaan testiluokalle nimi, esim. LyyraKorttiTest (testiluokan nimen on päätyttävä sanaan Test). Valitaan versio JUnit 4.x
Jos toimit oikein testi ilmestyy projektin sisälle kohdan Test Packages alle:
Eli varsinainen koodi kirjotetaan Source Packages:in alle ja testit Test Packages:in alle.
Testiluokka näyttää alussa seuraavalta:
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
public class LyyraKorttiTest {
public LyyraKorttiTest() {
}
@BeforeClass
public static void setUpClass() {
}
@AfterClass
public static void tearDownClass() {
}
@Before
public void setUp() {
}
@After
public void tearDown() {
}
// TODO add test methods here.
// The methods must be annotated with annotation @Test. For example:
//
// @Test
// public void hello() {}
}
Ei välitetä vielä sisällöstä. Poistetaan kommentti alimmasta metodista (vihje voit poistaa kommentit maalaamalla kommentoidun osan ja painamalla ctrl, shift ja c, samalla tavalla voit myös kommentoida koodia):
public class LyyraKorttiTest {
// ...
@Test
public void hello() {}
}
Kyseessä on testi joka ei testaa mitään. Ajetaan kuitenkin testi valitsemalla Run > Test project (tai alt F6). Jotain tapahtuu ja NetBeansin alareunaan tulee joko vihreä palkki joka ilmaisee että kaikki testit menevät läpi. Jos palkki ei ole näkyvissä , saat sen esiin valitsemalla NB:stä Window > IDE Tools > Test results.
Kirjoitetaan ensimmäinen oikea testi. Testit ovat testiluokan sisällä olevia metodeja, joiden yläpuolella/edessä on annotaatio eli merkintä @Test. Metodien paluuarvon tyyppi on yleensä void. Tehdään testi, eli kirjoitetaan seuraava koodinpätkä hello():n alapuolelle:
@Test
public void konstruktoriAsettaaSaldonOikein() {
LyyraKortti kortti = new LyyraKortti(10);
String vastaus = kortti.toString();
assertEquals("Kortilla on rahaa 10.0 euroa", vastaus);
}
Ensimmäinen rivi luo kortin jonka saldoksi tulee 10 euroa. Testin on tarkoitus varmistaa, että konstruktorin parametrina oleva luku menee kortin alkusaldoksi. Tämä varmistetaan selvittämällä kortin saldo. Kortin saldo selviää kortin toString-metodin muodostamasta merkkijonoesitysestä. Testin toinen rivi kutsuu toStringia ja ottaa merkkijonoesityksen talteen muuttujaan vastaus
. Viimeinen rivi tarkastaa onko vastaus sama kuin odotettu tulos, eli "Kortilla on rahaa 10.0 euroa".
Tarkastus tapahtuu JUnitissa paljon käytettyä assert- eli väittämäkomentoa käyttäen. Komento testaa onko sille ensimmäisenä parametrina annettu odotettu tulos sama kuin toisena parametrina oleva testissä saatu tulos.
Seuraavaksi ajetaan testi (Run > Test project tai alt F6) ja toivotaan että testi menee läpi.
Vaihtoehtoinen tapa määritellä sama testi olisi seuraava:
@Test
public void konstruktoriAsettaaSaldonOikein() {
LyyraKortti kortti = new LyyraKortti(10);
assertEquals("Kortilla on rahaa 10.0 euroa", kortti.toString());
}
eli metodikutsun palauttamaa arvoa ei oteta erikseen talteen muuttujaan vaan sitä kutsutaan suoraan assertEquals-vertailun sisällä. Käy niin, että ennen kuin varsinainen vertailu suoritetaan, tehdään metodikutsu ja vertailtavaksi tulee metodin palauttama arvo.
Kannattaa varmistaa, että JUnit todellakin löytää virheet, eli muutetaan edellistä testiä siten että se ei mene läpi (assertEqualsissa väitetään että saldo olisi 9):
@Test
public void konstruktoriAsettaaSaldonOikein() {
LyyraKortti kortti = new LyyraKortti(10);
assertEquals("Kortilla on rahaa 9.0 euroa", kortti.toString());
}
Varmistetaan että saamme alalaitaan punaisen palkin.
Korjataan testi taas ennalleen.
Painamalla työkalupalkin kohdalla hiiren oikeaa nappia ja valitsemalla Customize voit vetää listasta palkkiin oman napin testien ajamista varten. Näin kannattaa tehdä välittömästi. Jos olet käyttänyt koneella TMC:tä, se on luonut napin (musta silmä) jo valmiiksi.
Tehdään seuraavaksi testi, joka varmistaa, että kortin saldo pienee kutsuttaessa metodia syoEdullisesti
.
@Test
public void syoEdullisestiVahentaaSaldoaOikein() {
LyyraKortti kortti = new LyyraKortti(10);
kortti.syoEdullisesti();
assertEquals("Kortilla on rahaa 7.5 euroa", kortti.toString());
}
Jälleen testi alkaa kortin luomisella. Seuraavaksi kutsutaan kortin testattavaa metodia ja viimeisenä on rivi joka varmistaa, että tulos on haluttu, eli että kortin saldo on pienentynyt edullisen lounaan hinnan verran.
- Molemmat testit ovat yksinkertaisia ja testaavat vain yhtä asiaa, tämä on suositeltava käytäntö vaikka on mahdollista laittaa yhteen testiin useitakin assert:eja
- Testit on nimetty siten, että nimi kertoo selvästi sen mitä testi testaa
- Kaikki testit ovat toisistaan riippumattomia, esim. kortilla maksaminen ei vaikuta kortin saldoon kuin siinä testissä missä korttimaksu tapahtuu. Molemmat testit toimivat siis kuin kaksi erillistä pientä 'main':ia. Testien järjestyksellä testikoodissa ei ole merkitystä.
- Testit kannattaa ajaa mahdollisimman usein, eli aina kun teet testin (tai muutat normaalia koodia) aja testit!
Tehdään muutama testi lisää
@Test
public void syoMaukkaastiVahentaaSaldoaOikein() {
LyyraKortti kortti = new LyyraKortti(10);
kortti.syoMaukkaasti();
assertEquals("Kortilla on rahaa 6.0 euroa", kortti.toString());
}
@Test
public void syoEdullisestiEiVieSaldoaNegatiiviseksi() {
LyyraKortti kortti = new LyyraKortti(10);
kortti.syoMaukkaasti();
kortti.syoMaukkaasti();
// nyt kortin saldo on 2
kortti.syoEdullisesti();
assertEquals("Kortilla on rahaa 2.0 euroa", kortti.toString());
}
Ensimmäinen testeistä tarkastaa, että maukkaasti syöminen vähentää saldoa oikein. Toinen testi varmistaa, että edullista lounasta ei voi ostaa jos kortin saldo on liian pieni.
Huomaamme, että testikoodissamme toistoa: kolme ensimmäistä testiä luovat kaikki samanlaisen 10 euron saldon omaavan kortin.
Siirrämmekin metodin luonnin testiluokassa määriteltyyn alustusmetodiin:
public class LyyraKorttiTest {
LyyraKortti kortti;
@Before
public void setUp() {
kortti = new LyyraKortti(10);
}
@Test
public void konstruktoriAsettaaSaldonOikein() {
assertEquals("Kortilla on rahaa 10.0 euroa", kortti.toString());
}
@Test
public void syoEdullisestiVahentaaSaldoaOikein() {
kortti.syoEdullisesti();
assertEquals("Kortilla on rahaa 7.5 euroa", kortti.toString());
}
@Test
public void syoMaukkaastiVahentaaSaldoaOikein() {
kortti.syoMaukkaasti();
assertEquals("Kortilla on rahaa 6.0 euroa", kortti.toString());
}
@Test
public void syoEdullisestiEiVieSaldoaNegatiiviseksi() {
kortti.syoMaukkaasti();
kortti.syoMaukkaasti();
kortti.syoEdullisesti();
assertEquals("Kortilla on rahaa 2.0 euroa", kortti.toString());
}
}
Merkinnällä @before varustettu setUp() suoritetaan ennen jokaista testitapausta (eli testimetodia). Jokainen testitapaus siis aloittaa tilanteesta, jossa on luotu kortti jonka saldo on 10.
Tehdään vielä testi metodille lataaRahaa. Ensimmäinen testi varmistaa, että lataus onnistuu ja toinen testaa, ettei kortin saldo kasva suuremmaksi kuin 150 euroa.
@Test
public void kortilleVoiLadataRahaa() {
kortti.lataaRahaa(25);
assertEquals("Kortilla on rahaa 35.0 euroa", kortti.toString());
}
@Test
public void kortinSaldoEiYlitaMaksimiarvoa() {
kortti.lataaRahaa(200);
assertEquals("Kortilla on rahaa 150.0 euroa", kortti.toString());
}
Yllä jo mainittiin että testit ovat toisistaan riippumattomia eli molemmat testit toimivat siis kuin kaksi erillistä pientä "main":ia. Mitä tämä oikein tarkoittaa?
LyyraKorttia testataan usealla pienellä testimetodilla joista jokaisen alussa on annotaatio @Test. Jokainen erillinen testi testaa yhtä pientä asiaa, esim. että kortin saldo vähenee lounaan hinnan verran. On tarkoituksena, että jokainen testi aloittaa "puhtaalta löydältä", eli ennen jokaista testiä luodaan alustuksen tekevässä setUp-metodissa uusi varasto.
Jokainen testi siis alkaa tilanteesta jossa kortti on juuri luotu. Tämän jälkeen testi joko kutsuu suoraan testattavaa metodia tai ensin saa aikaan sopivan alkutilanteen ja tämän jälkeen kutsuu testattavaa metodia (näin tapahtui testimetodissa syoEdullisestiEiVieSaldoaNegatiiviseksi, maukkaastisyömisellä saldo väheni 2 euroon jonka jälkeen testattiin ettei edullisestisyöminen vie saldoa negatiiviseksi).
Olemme tyytyväisiä, uskomme että testitapauksia on nyt tarpeeksi. Onko tosiaan näin? Onneksi NetBeansissa on työkalu, jolla voidaan tarkastaa testien lausekattavuus, eli se mitä koodirivejä testien suorittaminen on tutkinut. Tutstumme testien lausekattavuuden mittaamiseen myöhemmin kurssin aikana.
Testikattavuuden mittaus paljastaa että koodi on lähes kattavasti testattu. Ainoa testien tutkimatta jättänyt komento on tilanteessa, jossa kortille yritetään ladata negatiivinen saldo.
Kortin alkusaldo on kaikissa testeissä 10. Varmempaa olisi testata myös muutamaa muuta alkusaldoa.
Testien kirjoittaminen ohjelmoinnin jälkeen raskasta ja turhauttavaa. Parempi tapa onkin kirjoittaa testejä jo ennen kun itse ohjelma on tehty. Tutustumme tähän testit ensin -ohjelmointiin (englanniksi Test Driven Development eli TDD) myöhemmin kurssilla.
Kerrataan vielä testien toiminnan kannalta erittäin tärkeä asia: testiluokan nimen on päätyttävä sanaan Test
Testiluokan konstruktori sekä metodit setUpClass (suoritetaan ennen kuin testaus aloitetaan), tearDownClass (suoritetaan testauksen päätyttyä) ja tearDown (suoritetaan jokaisen testin jälkeen) on poistettu sillä testimme ei niitä tarvitse.
public class LyyraKorttiTest {
LyyraKortti kortti;
@Before
public void setUp() {
kortti = new LyyraKortti(10);
}
@Test
public void konstruktoriAsettaaSaldonOikein() {
assertEquals("Kortilla on rahaa 10.0 euroa", kortti.toString());
}
@Test
public void syoEdullisestiVahentaaSaldoaOikein() {
kortti.syoEdullisesti();
assertEquals("Kortilla on rahaa 7.5 euroa", kortti.toString());
}
@Test
public void syoMaukkaastiVahentaaSaldoaOikein() {
kortti.syoMaukkaasti();
assertEquals("Kortilla on rahaa 6.0 euroa", kortti.toString());
}
@Test
public void syoEdullisestiEiVieSaldoaNegatiiviseksi() {
kortti.syoMaukkaasti();
kortti.syoMaukkaasti();
kortti.syoEdullisesti();
assertEquals("Kortilla on rahaa 2.0 euroa", kortti.toString());
}
@Test
public void kortilleVoiLadataRahaa() {
kortti.lataaRahaa(25);
assertEquals("Kortilla on rahaa 35.0 euroa", kortti.toString());
}
@Test
public void kortinSaldoEiYlitaMaksimiarvoa() {
kortti.lataaRahaa(200);
assertEquals("Kortilla on rahaa 150.0 euroa", kortti.toString());
}
}