Test Driven Development
Sissejuhatus
Test Driven Development (TDD) on lähenemine, kus enne koodi kirjutamist luuakse testid ning eesmärk on need testid läbima saada. Kui tavapäraselt kirjutatakse esmalt kood ja selle järel testid, siis TDD pöörab selle järjekorra ümber: test määrab, mida kood tegema peab, ning seejärel kirjutad vaid nii palju koodi, et see test läbiks.
Red-Green-Refactor tsükkel
TDD tsükkel koosneb kolmest etapist:
1. Punane etapp - Kirjuta test järgmise funktsionaalsuse jaoks ning käivita see. See peaks ebaõnnestuma, kuna funktsionaalsust ei ole veel olemas. See kinnitab, et test tegelikult testib midagi.
2. Roheline etapp - Kirjuta valmis kood, mis läbiks testi. Elegantsuse või äärejuhtumite üle ei peaks siin etapis muretsema. Eesmärk on liikuda võimalikult kiirelt punasest etapist rohelisse.
3. Refaktoreerimise etapp - Kui testid läbivad, siis võib koodi refaktoreerida. Sinna hulka käivad duplikaatide eemaldamine, nimetuste parandamine, struktuuri korrastamine jne. Eelnevalt kirjutatud testid tagavad, et funktsionaalsus jääb samaks ning katkiminekul annavad märku sellest.
Ning järgmise funktsionaalsuse korral korratakse seda sama protsessi: kirjuta järgmine test, kirjuta kood mis läbiks seda ning refaktoreeri.
TDD praktiline näide
Tutvume TDD-ga läbi ühe konkreetse näite. Loome meetodi, mis kontrollib, kas sisendiks saadud sõne on palindroom (ehk kas sõne on eest- ja tagantpoolt lugedes sama).
1. samm: tühi sõne on palindroom
Esmalt kirjutame selle kontrollimiseks testi:
@Test
void testIsPalindrome_emptyString_returnsTrue() {
assertTrue(StringUtils.isPalindrome(""));
}
See test peaks läbi kukkuma kuna StringUtils.isPalindrome meetodit ei eksisteeri veel.
Kirjutage nii palju koodi kui vaja, et test läbiks.
Antud juhul piisaks sellest, kui tagastada lihtsalt true:
public static boolean isPalindrome(String input) {
return true;
}
Test läbib ilma probleemideta, liigume järgmise sammu juurde.
2. samm: ühe-täheline sõne on palindroom
Jälle, kirjutame esmalt testi:
@Test
void testIsPalindrome_singleCharacter_returnsTrue() {
assertTrue(StringUtils.isPalindrome("a"));
}
See test läbib praeguse implementatsiooniga ehk koodi muudatusi ei pea tegema.
3. samm: sõne mis ei ole palindroom peaks tagastama false
@Test
void testIsPalindrome_notAPalindrome_returnsFalse() {
assertFalse(StringUtils.isPalindrome("hello"));
}
See test kukub läbi, kuna praeguses olekus isPalindrome tagastab alati true.
Sellest tulenevalt peaks nüüd kirjutama meetodisse ka tegelikku loogikat:
public static boolean isPalindrome(String input) {
String reversed = new StringBuilder(input).reverse().toString();
return input.equals(reversed);
}
Kõik kolm testi läbivad.
4. samm: tõstutundetu võrdlus
@Test
void testIsPalindrome_mixedCase_ignoresCase() {
assertTrue(StringUtils.isPalindrome("Racecar"));
}
See test kukub läbi, kuna "Racecar" ei ole võrdne "racecaR"-iga.
Täiendame isPalindrome meetodit:
public static boolean isPalindrome(String input) {
String normalized = input.toLowerCase();
String reversed = new StringBuilder(normalized).reverse().toString();
return normalized.equals(reversed);
}
Kõik neli testi läbivad edukalt. Toome näiteks ka refaktoreerimise - eraldame normaliseerimise eraldi meetodiks selguse mõttes:
public static boolean isPalindrome(String input) {
String normalized = normalize(input);
String reversed = new StringBuilder(normalized).reverse().toString();
return normalized.equals(reversed);
}
private static String normalize(String input) {
return input.toLowerCase();
}
Kõik testid peaksid jätkuvalt läbima peale refaktoreerimist. Testid tagavad meile kindluse teha koodis muudatusi, ilma et midagi katki teeks selle käigus.
Ja sedasi see tsükkel jätkub.
Järgmises faasis võib näiteks lisanduda tühikute ignoreerimine (nt: "race car" on palindroom), mis tähendab, et normalize meetodit peaks muutma sellele vastavaks.
Miks TDD kasulik on
Väikesed sammud kiire tagasisidega. Iga tsükkel lisab ühe väikese funktsionaalsuse osa. Sa ei jää kunagi pikaks ajaks teadmatusse, kas kood töötab. See on motiveerivam ja vähem veaaldine kui suure hulga koodi kirjutamine ning alles siis vigade otsimine.
Teste kirjutatakse, mitte ei jäeta tahaplaanile. Kui testid on esimesed, siis igal funktsionaalsusel on definitsiooni järgi test olemas. Kui kirjutada kõigepealt kood, on kiusatus jätta testid kirjutamata koodile, mis "ilmselgelt töötab".
Sunnib keskenduma. TDD nõuab, et mõtleksid enne meetodi eesmärgile, kui hakkad mõtlema selle teostusele. See viib sageli puhtamate liidesteni, sest koged ise meetodit selle kasutaja vaatenurgast.
Vähendab vigade otsimist. Kui test ebaõnnestub kohe pärast selle kirjutamist, tead, et viga on just äsja kirjutatud ridades. Ei ole vaja otsida probleemi suurest koodibaasist.
Millal tasub TDD kasutust kaaluda
TDD toimib kõige paremini, kui:
- Rakendatav käitumine on selgelt määratletud (tead, millised peaksid olema sisend ja väljund).
- Probleemi saab jagada väikesteks, järk-järgulisteks sammudeks.
- Töötad loogikarohke koodiga (arvutused, algoritmid, ärireeglid).
TDD on ebamäärane:
- Prototüüpimisel, ringi uurimisel
- Kui nõuded on umbmäärased
- Kood on peamiselt integratsiooni- või kasutajaliidese töö, kus tagasisideahel on loomupäraselt aeglasem.
TDD on oskus, mis areneb harjutamisega. Alusta väikeste, hästi määratletud meetoditega. Harjumus kirjutada kõigepealt ebaõnnestuv test muutub aja jooksul loomulikuks.