Yksikkötestaus
Yksikkötestauksen perusidea on yksinkertainen: testataan ohjelman pieniä osia, kuten yksittäisiä luokkia tai metodeja, erillään muusta järjestelmästä. Tavoite on varmistaa, että kukin osa toimii oikein omalla vastuullaan. Kun testin ajatus pidetään pienenä ja tarkkarajaisena, virheiden paikantaminen on paljon helpompaa kuin tilanteessa, jossa koko ohjelmaa yritetään testata kerralla.
Yksikkötestit ovat hyödyllisiä, koska ne paljastavat virheitä nopeasti jo kehitysvaiheessa. Toiseksi ne toimivat eräänlaisena turvaverkkona: jos muutamme koodia myöhemmin, ajamalla yksikkötestit näemme nopeasti rikkoutuiko jokin aiemmin toiminut ominaisuus. Kolmanneksi ne pakottavat ohjelmoijan miettimään, millainen luokan rajapinta on ja mitä sen oikeastaan pitäisi tehdä.
Ajatellaan esimerkiksi Tehtavakokoelma-luokkaa. Voimme kirjoittaa testin,
joka lisää kokoelmaan yhden tehtävän ja tarkistaa, että listassa on nyt yksi
alkio. Voimme kirjoittaa toisen testin, joka yrittää lisätä tyhjän otsikon ja
tarkistaa, ettei tehtävää lisätä lainkaan. Kolmas testi voisi poistaa valitun
tehtävän ja varmistaa, että listan koko pienenee oikein. Jokaisessa näistä
testeistä tarkistetaan yksi selkeä käyttäytyminen.
JUnit
Java-maailmassa yksikkötestejä tehdään usein JUnit-kirjastolla. JUnit antaa valmiit työkalut testimetodien kirjoittamiseen sekä odotettujen tulosten tarkistamiseen.
Kirjoitushetkellä ajantasainen JUnit-versio on 6.0.3, joka tunnetaan myös
nimellä JUnit Jupiter. Lisätään junit-jupiter-riippuvuus pom.xml-tiedostoon.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.0.3</version>
<scope>test</scope>
</dependency>
Kokeillaan tehdä yksinkertainen testi käyttäen JUnitia. Tehdään uusi
Maven-projekti. Lisätään pääluokkaamme aliohjelma Keskiarvo, joka laskee
keskiarvon kuitenkin niin, jos listassa on lopetusluku tai sitä suurempi luku,
kaikki sen jälkeen olevat luvut jätetään huomioimatta.
public static double keskiarvo(List<Integer> luvut, int lopetusluku) {
if (luvut.isEmpty()) {
throw new IllegalArgumentException("Lista ei saa olla tyhjä");
}
int summa = 0;
int lukujenMaara = 0;
for (int luku : luvut) {
if (luku >= lopetusluku) {
break;
}
summa += luku;
lukujenMaara++;
}
return (double) summa / lukujenMaara;
}
Tehdään sitten tälle metodille yksikkötesti. Testit kirjoitetaan tavallisesti
hakemistoon src/test/java. Kun IDEAssa klikkaa src-kansion päältä New
directory, Maven-projektihallinta osaa automaattisesti tarjota
src/test/java-hakemiston luomista. Tee test/java-kansio ja sinne uusi luokka
KeskiarvoTest. Kirjoita seuraavat kaksi testimetodia. Jos kopioit alla olevan
koodin, muuta jälleen ensimmäinen import-lause vastaamaan oman pääluokkasi
nimeä.
import fi.jyu.ohj2.Main;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class KeskiarvoTest {
@Test
void keskiarvoLaskeeOikein() {
List<Integer> luvut = List.of(1, 2, 3, 4, 5);
double tulos = Main.keskiarvo(luvut, 10);
assertEquals(3.0, tulos, "Keskiarvon pitäisi olla 3.0");
}
@Test
void keskiarvoLopettaaOikein() {
List<Integer> luvut = List.of(1, 2, 3, 10, 4, 5);
double tulos = Main.keskiarvo(luvut, 10);
assertEquals(2.0, tulos, "Keskiarvon pitäisi olla 2.0, koska 10 ja sen jälkeen olevat luvut jätetään huomioimatta");
}
}
Testit voi ajaa esimerkiksi public class KeskiarvoTest-luokan vasemmalla
puolella olevasta vihreästä nuolesta. JUnit ajaa testit ja näyttää tulokset
IDE:n testinäkymässä. Jos kaikki testit menevät läpi, näet vihreän merkin. Jos
jokin testi epäonnistuu, näet punaisen merkin ja virheilmoituksen, joka kertoo
mikä meni pieleen.
Meiltä puuttuu yksi tärkeä testi: mitä tapahtuu, jos yhtään validia lukua ei
ole? Sovitaan tässä, että aliohjelmamme tulisi heittää
IllegalArgumentException- poikkeus. Kirjoitetaan vielä testi, joka varmistaa,
että tämä tapahtuu.
Poikkeuksen odottaminen JUnitissa onnistuu assertThrows-metodilla. Se ottaa
parametrina odotetun poikkeusluokan ja lambda-lausekkeen, joka sisältää testattavan koodin.
@Test
void keskiarvoHeittaaPoikkeuksenTyhjallaListalla() {
List<Integer> luvut = List.of(10, 20, 30, 40, 50, 60);
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> {
Main.keskiarvo(luvut, 10);
});
assertEquals("Yhtään lukua ei tullut mukaan keskiarvoon", exception.getMessage());
}
Nyt testitulos näyttää punaista:
org.opentest4j.AssertionFailedError: Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.
Nollalla jakaminen ei double-luvuilla laskettaessa heitä poikkeusta, vaan
palauttaa NaN-arvon. Korjataan tämä aliohjelmassamme.
public static double keskiarvo(List<Integer> luvut, int lopetusluku) {
// ... aiempi koodi ennallaan ...
// HIGHLIGHT_GREEN_BEGIN
if (lukujenMaara == 0) {
throw new IllegalArgumentException("Yhtään lukua ei tullut mukaan keskiarvoon");
}
// HIGHLIGHT_GREEN_END
return (double) summa / lukujenMaara;
}
Nyt kaikki testit menevät läpi!
Ota tehtävän 5.10 vastauksesi (tai mallivastaus), ja kirjoita sille yksikkötestit. Testaa aliohjelman toimivuutta ainakin viidellä eri syötteellä. Käytä tehtävässä annettuja esimerkkejä tai keksi itse uusia testitapauksia.
Todo-ohjelman testaaminen
Kun haluamme testata todo-sovellusta, kaikkea ei tarvitse lähestyä
käyttöliittymän kautta. Olennaista on testata sovelluksen bisneslogiikkaa eli
sitä, miten Tehtavakokoelma käyttäytyy eri tilanteissa. Tällöin emme klikkaile
nappeja tai avaa ikkunoita, vaan kutsumme suoraan malliluokan metodeja ja
tarkistamme, että lopputulos vastaa odotuksia.
Tällaisia testattavia asioita ovat esimerkiksi seuraavat:
lisaaTehtava("Käy kaupassa")lisää tehtävän listaanpoistaTehtava(tehtava)poistaa annetun tehtävän listastalisaaTehtava(" ")ei lisää tyhjää tehtävää lainkaan
Tämän vuoksi voisimme kirjoittaa src/test/java-hakemistoon esimerkiksi
seuraavan JUnit-testiluokan:
import fi.jyu.ohj2.nimi.todo.model.Tehtava;
import fi.jyu.ohj2.nimi.todo.model.Tehtavakokoelma;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class TehtavakokoelmaTest {
@Test
void lisaaTehtava_lisaaTehtavanListaan() {
Tehtavakokoelma kokoelma = new Tehtavakokoelma("testitehtavat.json");
kokoelma.lisaaTehtava("Käy kaupassa");
assertEquals(1, kokoelma.getTehtavat().size());
assertEquals("Käy kaupassa", kokoelma.getTehtavat().get(0).getOtsikko());
}
@Test
void poistaTehtava_poistaaTehtavanListasta() {
Tehtavakokoelma kokoelma = new Tehtavakokoelma("testitehtavat.json");
kokoelma.lisaaTehtava("Käy kaupassa");
Tehtava tehtava = kokoelma.getTehtavat().get(0);
kokoelma.poistaTehtava(tehtava);
assertEquals(0, kokoelma.getTehtavat().size());
}
@Test
void lisaaTehtava_eiLisaaTyhjaaTehtavaa() {
Tehtavakokoelma kokoelma = new Tehtavakokoelma("testitehtavat.json");
kokoelma.lisaaTehtava(" ");
assertEquals(0, kokoelma.getTehtavat().size());
}
}
Lisäsimme tässä Tehtavakokoelma-luokalle myös uuden konstruktorin, joka ottaa
parametrina tallennustiedoston nimen. Näin testit voivat käyttää erillistä
testitiedostoa, eikä oikea data sekoitu testien kanssa. Lisää tämä konstruktori Tehtavakokoelma-luokkaan:
public Tehtavakokoelma(String polku)
{
tiedostoPolku = Path.of(polku);
tehtavat.addListener((ListChangeListener<Tehtava>) change -> {
tallenna();
});
}
Ajatus testeissä on hyvin suoraviivainen: ensin valmistellaan testin lähtötilanne, sitten kutsutaan testattavaa metodia ja lopuksi tarkistetaan, että kokoelman tila muuttui oikein. Tämä on juuri sellaista bisneslogiikan testausta, jota MVC:n mukainen rakenne meille mahdollistaa.
Lisää .gitignore-tiedostoon rivi testitehtavat.json, jotta testitiedosto ei päädy
versiohallintaan. Jos ehdit jo lisäämään sen versiohallintaan, poista se sieltä
komennolla git rm --cached testitehtavat.json ja tee uusi commit.
Kirjoita Tehtavakokoelma-luokalle yksikkötestejä. Testaa ainakin
seuraavat asiat:
- Kun tehtävä lisätään otsikolla, jonka alussa ja lopussa on välilyöntejä, tyhjät poistetaan ennen tallentamista listaan.
- Kun kokoelmaan lisätään kaksi tehtävää, joilla on sama otsikko, molemmat oikeasti päätyvät listaan.
- Edelliseen jatkoa: Kun toinen noista tehtävistä merkitään tehdyksi, vain kyseinen tehtävä merkitään tehdyksi, ei toista.
- Kun kokoelmaan lisätään kaksi eri tehtävää peräkkäin, molemmat päätyvät listaan oikeassa järjestyksessä.
Palauta TehtavakokoelmaTest-luokka.
Yksikkötestaus ja MVC-arkkitehtuuri
Nyt kun olemme siirtyneet MVC-arkkitehtuuriin ja luoneet
Tehtavakokoelma-luokan, olemme erottaneet käyttöliittymän kokonaan irti
sovelluksen logiikasta ja datasta. Tällä on ratkaiseva ohjelmistotuotannollinen
hyöty. Jos yrittäisimme testata koodia käyttöliittymän kautta esimerkiksi
simuloimalla napin painalluksia, testaus olisi hidasta, altista satunnaisille
virheille ja vaatisi raskaiden JavaFX-kirjastojen käynnistämisen. Koska
kokoelmalla on nyt selkeä ohjelmointirajapinta (metodit lisaaTehtava,
poistaTehtava jne.), voimme rakentaa yksikkötestejä, jotka kutsuvat suoraan
kokoelmaa ja tarkistavat asioiden toimiutuvuuden millisekunneissa ilman ruudulle
aukeavia ikkunoita.
Valitettavasti tähän liittyy vielä yksi käytännön ongelma: nykyinen
Tehtavakokoelma tekee myös tiedosto-operaatioita. Siksi aivan näin
suoraviivainen testaus ei vielä ole täysin ongelmatonta.
IO on ongelmallista testauksessa
Mietitäänpä tilannetta, jossa lähtisimme testaamaan uutta hienoa
Tehtavakokoelma-luokkaamme. Mitä tapahtuu, jos testi tekee kokoelmaan kymmenen
uutta tehtävää ja testaa, että lukumäärä täsmää? Koska laitoimme observerin
varoittamaan muutoksista ja kutsumaan kokoelman tallenna()-metodia, ohjelma
tallentaa nämä testitehtävät oikealle kovalevylle (esim.
tehtavat.json-tiedostoon).
Oikealle kovalevylle kirjoittaminen on testeissä yleensä pahasta. Tyypillisessä tuotantosovelluksessa testejä saatetaan ajaa satoja peräjälkeen, ja levy-IO (input/output) tekee testeistä erittäin hitaita. Lisäksi, jos testit epäonnistuvat tai keskeytyvät kesken, ne voivat jättää levylle sotkuisen tilan, jossa on puoliksi kirjoitettuja tiedostoja tai vanhentunutta dataa.
Tallennuksen eriyttäminen abstraktion taakse: repository-suunnittelumalli
Ratkaisu on erottaa tallennus omaksi kokonaisuudekseen. Tällöin
Tehtavakokoelma ei enää itse lue tai kirjoita tehtavat.json-tiedostoa, vaan
delegoi tallennuksen erilliselle luokalle. Tämä helpottaa yksikkötestausta,
koska testit eivät ole suoraan sidoksissa oikeaan tiedostoon tai
tiedostojärjestelmään. Tällaista ratkaisua toteutetaan usein niin kutsutulla
repository-suunnittelumallilla.
Repository-suunnittelumalli tarkoittaa sitä, että datan tallennus ja lataus
piilotetaan oman rajapinnan tai luokan taakse. Tällöin muu ohjelma ei käsittele
suoraan tiedostoja, tietokantoja tai muita tallennusmekanismeja, vaan käyttää
repository-rajapinnan (esim. TehtavaRepository) tarjoamia metodeja.
Katsotaan Todo-sovelluksessamme, miten tämä toteutaan käytännössä.
1. Luodaan rajapinta TehtavaRepository. Tyypillisesti lataamiseen ja
tallentamiseen liittyvät metodit määritellään pakkaukseen persistence. Tehdään
mekin niin.
package fi.jyu.ohj2.nimi.todo.persistence;
import fi.jyu.ohj2.nimi.todo.model.Tehtava;
import java.util.List;
public interface TehtavaRepository {
List<Tehtava> lataa() throws RepositoryException;
void tallenna(List<Tehtava> tehtavat) throws RepositoryException;
}
Tehdään samalla erillinen luokka latausvirhettä kuvaavalle RepositoryException,
sillä nyt tehtäviä voidaan ladata muullakin tavalla kuin tiedostosta.
Sijoitetaan tämäkin persistence-pakkaukseen.
package fi.jyu.ohj2.nimi.todo.persistence;
public class RepositoryException extends Exception {
public RepositoryException(String message) {
super(message);
}
}
2. Irrotetaan JSON-tallennuskoodi mallista omaan toteutukseensa
JsonTehtavaRepository edelleen persistence-pakkaukseen.
Kopioidaan lataamiseen ja tallentamiseen liittyvät koodit Tehtavakokoelmasta
uuteen luokkaan, joka toteuttaa TehtavaRepository-rajapinnan.
package fi.jyu.ohj2.nimi.todo.persistence;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.core.JacksonException;
import fi.jyu.ohj2.nimi.todo.model.Tehtava;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class JsonTehtavaRepository implements TehtavaRepository {
private final Path tallennustiedosto;
private final ObjectMapper mapper = new ObjectMapper();
public JsonTehtavaRepository(Path tallennustiedosto) {
this.tallennustiedosto = tallennustiedosto;
}
@Override
public List<Tehtava> lataa() throws JacksonException {
if (Files.notExists(tallennustiedosto)) {
return List.of();
}
return mapper.readValue(tallennustiedosto.toFile(), new TypeReference<>() {});
}
@Override
public void tallenna(List<Tehtava> tehtavat) throws JacksonException {
mapper.writeValue(tallennustiedosto.toFile(), tehtavat);
}
}
3. Päivitetään Tehtavakokoelma huolimaan mikä tahansa tallentaja. Muokataan
konstruktoria niin, että sille annetaan jokin rajapinnan toteuttaja
tiedostojumpan sijaan. Tällaista toimintamallia dependency injection
-periaatteeksi. Dependency injection tarkoittaa sitä, että luokka ei itse luo
riippuvuuksiaan, vaan ne annetaan sille ulkopuolelta. Erityisesti tässä
tehtävässä Tehtavakokoelma-luokka ei enää luo JsonTehtavaRepository-oliota,
vaan saa sen konstruktorin parametrina. Tämän hyöty tulee myöhemmin vielä
paremmin esiin kun kirjoitamme konkreettisia testitapauksia.
public class Tehtavakokoelma {
// Riippuvuus tallennusmekanismista on nyt rajapinnan takana
private final TehtavaRepository repository;
// Konstruktoriin annetaan haluttu tallennusväline
// sisältäpäin luomisen sijaan (Dependency Injection)
public Tehtavakokoelma(TehtavaRepository repository) {
this.repository = repository;
this.tehtavat.addListener((ListChangeListener<Tehtava>) change -> {
tallenna();
});
}
public void lataa() {
try {
List<Tehtava> kaikkiTehtavat = repository.lataa();
tehtavat.addAll(kaikkiTehtavat);
} catch (RepositoryException e) {
IO.println(e.getMessage());
}
}
public void tallenna() {
try {
repository.tallenna(tehtavat);
} catch (RepositoryException e) {
IO.println(e.getMessage());
}
}
// ... kaikki muut lisaaTehtava yms. samat kuin aiemmin!
}
Vaihda myös MainController-luokassa tehtäväkokoelman alustus muotoon
private Tehtavakokoelma tehtavakokoelma = new Tehtavakokoelma(new JsonTehtavaRepository(Path.of("tehtavat.json")));
Repository tukee Todo-sovelluksemme tapauksessa MVC-mallin mukaista toteutusta,
koska sen avulla mallikerroksen sisällä vastuut pidetään erillään.
Tehtavakokoelma kuuluu malliin, mutta sen ei tarvitse tietää tallennuksen
teknisistä yksityiskohdista. Se voi pyytää repositorya lataamaan tai
tallentamaan tehtävät ja keskittyä itse sovelluslogiikkaan. Näin myös
kontrolleri ja näkymä pysyvät erossa tiedostokäsittelystä.
Rajapinnan tehokkuus piilee siinä, että Tehtavakokoelma-luokan ei sen jälkeen
enää tarvitse tietää, miten tai minne data tallennetaan (onko se
JSON-tiedosto, tietokanta vai vain keskusmuistilista testausta varten).
Mock- ja Fake-luokat
Testauksessa käytetään usein niin sanottuja mock- tai fake-olioita, kun testattava luokka tekee yhteistyötä jonkin toisen olion kanssa. Ajatus on, että oikean yhteistyöolion tilalle annetaan testissä kevyt korvike, jonka toimintaa on helpompi hallita. Näin testi voidaan kohdistaa juuri siihen luokkaan, jota halutaan testata, ilman että mukana on turhaan tiedostoja, tietokantoja, verkkoa tai muuta raskasta ympäristöä.
Ajatellaan yksinkertaista esimerkkiä, jossa luokka Pakkasvahti kysyy
lämpötilan toiselta oliolta, esimerkiksi Lampoanturi-rajapinnan kautta. Jos
haluamme testata, toimiiko Pakkasvahti oikein, emme välttämättä halua käyttää
oikeaa anturia, koska sellaista ei ehkä testissä ole olemassa tai sen palauttama
arvo vaihtelee koko ajan. Sen sijaan voimme tehdä valeanturin, joka palauttaa
aina esimerkiksi arvon 21.5. Tällöin testi on ennustettava: tiedämme tarkalleen,
mitä arvoa Pakkasvahti saa ja mitä sen pitäisi tehdä sillä.
Esimerkiksi:
public interface Lampoanturi {
double mittaaLampotila();
}
public class Pakkasvahti {
private final Lampoanturi anturi;
public Pakkasvahti(Lampoanturi anturi) {
this.anturi = anturi;
}
public boolean onkoPakkasta() {
return anturi.mittaaLampotila() < 0;
}
}
Oikeassa ohjelmassa Lampoanturi voisi lukea arvon fyysiseltä laitteelta, mutta
testissä voimme käyttää yksinkertaista valeoliota:
public class ValeLampoanturi implements Lampoanturi {
@Override
public double mittaaLampotila() {
return 21.5;
}
}
Tällöin Pakkasvahti-luokkaa testatessa tiedämme varmasti, että anturi
palauttaa aina saman arvon.
Tällaiset korvikeoliot ovat hyödyllisiä erityisesti silloin, kun oikea riippuvuus on hidas, vaikeasti hallittava tai aiheuttaa sivuvaikutuksia. Seuraavaksi hyödynnämme samaa ajatusta todo-sovelluksessa tekemällä vale-säiliön, joka teeskentelee tallentavansa dataa, mutta pitääkin sen vain muistissa testin ajan.
Testaaminen mock-säilöllä
Testiympäristössä (eli src/test/java...-kansiossa) voimme nyt luoda
mock-luokan, joka teeskentelee tallentavansa tietoja tiedostoon, mutta
todellisuudessa tallentaakin ne vain normaaliin Java-listaan laitteen
välimuistiin.
public class MockTehtavaRepository implements TehtavaRepository {
// Keskusmuistissa oleva data "tiedoston" sijaan testejä varten
private List<Tehtava> tallennetutTehtavat = new ArrayList<>();
@Override
public List<Tehtava> lataa() {
return tallennetutTehtavat;
}
@Override
public void tallenna(List<Tehtava> tehtavat) {
tallennetutTehtavat.clear();
// Teemme jokaisesta tehtävästä kopion
// Näin voimme testata, että tallennettu data täsmää myös kokoelmassa olevan datan kanssa
for (Tehtava tehtava : tehtavat) {
Tehtava kopio = new Tehtava();
kopio.setOtsikko(tehtava.getOtsikko());
kopio.setPrioriteetti(tehtava.getPrioriteetti());
kopio.setKuvaus(tehtava.getKuvaus());
kopio.setTehty(tehtava.getTehty());
tallennetutTehtavat.add(kopio);
}
}
public List<Tehtava> getTallennetutTehtavat() {
return this.tallennetutTehtavat;
}
}
Nyt voimme turvallisin mielin testata mallia JUnit-testeillä:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class TehtavakokoelmaTest {
@Test
void lisaaTehtava_lisaaTehtavanJaTallentaaSen() {
// 1. Arrange: Valmistellaan testidata. SYÖTETÄÄN VALE-säiliö!
MockTehtavaRepository mockRepo = new MockTehtavaRepository();
Tehtavakokoelma malli = new Tehtavakokoelma(mockRepo);
// 2. Act: Kutsutaan metodia
malli.lisaaTehtava("Käy kaupassa");
// 3. Assert: Tarkistetaan tulos oikeassa data-domainissa
assertEquals(1, malli.getTehtavat().size(), "Listassa pitäisi olla 1 tehtävä.");
assertEquals("Käy kaupassa", malli.getTehtavat().get(0).getOtsikko(), "Otsikon pitäisi täsmätä");
// 4. Assert 2: Varmistetaan mock-luokan avulla, että kokoelma laukaisi tallennuksen tapahtuman yhteydessä
assertEquals(1, mockRepo.getTallennetutTehtavat().size(), "Data olisi pitänyt tallentaa rajapinnan läpi!");
}
@Test
void lisaaTehtava_eiLisaaTyhjaaOtsikkoa() {
MockTehtavaRepository mockRepo = new MockTehtavaRepository();
Tehtavakokoelma malli = new Tehtavakokoelma(mockRepo);
malli.lisaaTehtava(" "); // Tyhjä syöte
assertEquals(0, malli.getTehtavat().size(), "Tyhjiä tehtäviä ei saa lisätä listaan.");
}
}
Voimme oikeastaan nyt mennä pidemmälle ja testata, että tehtäväkokoelma oikeasti tallentaa tehtävät aina, kun kokoelman tai yksittäisen tehtävien tila muuttuu. Voimme lisätä siis erilliset testit nimenomaan tallentamiselle:
@Test
void tehtavakokoelmaTallennusToimiiLisayksestaJaPoistosta() {
// Act ja Assert -vaiheita voi toistaa samassa testausyksikössä
MockTehtavaRepository repo = new MockTehtavaRepository();
Tehtavakokoelma kokoelma = new Tehtavakokoelma(repo);
// Act 1: Lisätään tehtävä
kokoelma.lisaaTehtava("Käy kaupassa");
// Assert 1: Tallennus onnistui
assertEquals(1, repo.getTallennetutTehtavat().size(), "Tehtävät tallentuvat, kun uusi tehtävä lisätään");
// Act 2: Poistetaan tehtävä
Tehtava tehtava = kokoelma.getTehtavat().getFirst();
kokoelma.poistaTehtava(tehtava);
// Assert 2: Tallennus onnistui
assertEquals(0, repo.getTallennetutTehtavat().size(), "Tehtävät tallentuvat, kun tehtävä poistetaan");
}
@Test
void tehtavakokoelmaTallennusToimiiAttribuuttienMuutoksesta() {
MockTehtavaRepository repo = new MockTehtavaRepository();
Tehtavakokoelma kokoelma = new Tehtavakokoelma(repo);
kokoelma.lisaaTehtava("Käy kaupassa");
Tehtava tehtava = kokoelma.getTehtavat().getFirst();
tehtava.setTehty(true);
Tehtava tallennettuTehtava = repo.getTallennetutTehtavat().getFirst();
assertEquals(tehtava.getTehty(), tallennettuTehtava.getTehty(), "Tehtävän tehty-tila tallentuu, kun se muutetaan");
tehtava.setOtsikko("Mene nukkumaan");
tallennettuTehtava = repo.getTallennetutTehtavat().getFirst();
assertEquals(tehtava.getOtsikko(), tallennettuTehtava.getOtsikko(), "Tehtävän otsikko tallentuu, kun se muutetaan");
tehtava.setPrioriteetti(Prioriteetti.KORKEA);
tallennettuTehtava = repo.getTallennetutTehtavat().getFirst();
assertEquals(tehtava.getPrioriteetti(), tallennettuTehtava.getPrioriteetti(), "Tehtävän prioriteetti tallentuu, kun se muutetaan");
tehtava.setKuvaus("Nukkuminen on kivaa");
tallennettuTehtava = repo.getTallennetutTehtavat().getFirst();
assertEquals(tehtava.getKuvaus(), tallennettuTehtava.getKuvaus(), "Tehtävän kuvaus tallentuu, kun se muutetaan");
}
Ilman MVC-arkkitehtuuriamme olisimme yrittäneet kutsua suoraan kontrollerin
logiikkaa Main.java-luokasta ja taistelisimme saadaksemme VBoxissa olevien
Checkboxien lukumäärän tarkistettua, samalla varoitellen sitä sekoittamasta
aitoa tehtavat.json-originaalitietokantaamme! Nyt voimme keskittyä vain
malliluokan testaamiseen, joka on tämän pienen vaivannäön jälkeen nopeaa,
luotettavaa ja helppoa.
Yhteenveto I/O-abstraktioista
Oikean arkkitehtuurijärjestelyn suurin hyöty näkyy yleensä ensimmäisenä
testauksen sujuvuudessa. Kuvion voi ajatella menevän näin: UI (Controller) ->
Business Logic (Tehtavakokoelma) -> Data Provider (TehtavaRepository)
UI:n testaus automatisoidusti on vaikeaa. Data providerin (oikean tallentamisen levylle) automaattinen testaus on tyypillisesti melko hidasta ja haurasta. Mutta eristetty bisneslogiikka eli sovelluksen hermokeskus voidaan suorittaa puhtaana logiikkakoodina sekunnin murto-osiin käyttämällä rajapintojen mahdollistamia mock-luokkia ympärillä olevien vaikeiden järjestelmien korvaamisessa testiajonaikaisesti.
Palauta osan 8.5 perusteella edistetty projekti. Kertaus tämän osan vaiheista:
- Lisää projektiin yksikkötestit.
- Eriytä tehtävien tallennus ja lataus erilliseen luokkaan, joka toteuttaa
TehtavaRepository-rajapinnan. - Tee testipakkaukseen mock-luokka, joka toteuttaa
TehtavaRepository-rajapinnan, mutta tallentaa datan vain muistissa. - Testaa tiedoston tallennus/lataus.
Kun vaihe on valmis, tee git add muuttuneille tiedostoille ja git commit.
Palauta TehtavaRepository-rajapinta sekä JsonTehtavaRepository,
MockTehtavaRepository ja TehtavakokoelmaTest-luokat. Muita luokkia tai
FXML-tiedostoja ei tarvitse palauttaa.