A tesztvezérelt fejlesztés (TDD ) egy olyan szoftverfejlesztési technika , amely nagyon rövid fejlesztési ciklusok megismétlésére épül: először egy tesztet írnak, amely lefedi a kívánt változást, majd kódot írnak, amely lehetővé teszi a teszt sikerességét, és végül az újrafaktorálás. új kódot hajtott végre a vonatkozó szabványoknak megfelelően. A technika feltalálójának tartott Kent Beck 2003-ban azzal érvelt, hogy a tesztvezérelt fejlesztés ösztönzi az egyszerű tervezést és önbizalmat kelt [ 1 ] .
1999-ben, amikor megjelent, a tesztvezérelt fejlesztés szorosan kapcsolódott az extrém programozásban használt teszt -first koncepcióhoz [2] , később azonban önálló módszertanként jelent meg. [3] .
A teszt egy olyan eljárás, amely lehetővé teszi a kód működőképességének megerősítését vagy cáfolatát. Amikor egy programozó ellenőrzi az általa kifejlesztett kód működőképességét, kézi tesztelést hajt végre.
A tesztvezérelt fejlesztés megköveteli, hogy a fejlesztő automatizált egységteszteket hozzon létre , amelyek közvetlenül a tényleges kód megírása előtt határozzák meg a kód követelményeit. A teszt olyan állapotteszteket tartalmaz, amelyek teljesíthetők vagy nem. Amikor végrehajtják, azt mondják, hogy a teszt sikeres. A teszt sikeres teljesítése megerősíti a programozó által tervezett viselkedést. A fejlesztők gyakran használnak tesztelési keretrendszereket tesztkészletek létrehozására és automatizálására . A gyakorlatban az egységtesztek a kód kritikus és nem triviális részeit fedik le. Ez lehet olyan kód, amely gyakran változik, olyan kód, amely sok más kódot működésre késztet, vagy olyan kód, amely sok függőséggel rendelkezik.
A fejlesztői környezetnek gyorsan kell reagálnia a kis kódmódosításokra. A program architektúrája sok, nagy belső kohéziós komponens felhasználásán kell, hogy alapuljon, amelyek lazán kapcsolódnak egymáshoz, ami megkönnyíti a kód tesztelését.
A TDD nemcsak a helyesség ellenőrzését foglalja magában, hanem a program kialakítását is befolyásolja. A tesztek alapján a fejlesztők gyorsan el tudják képzelni, milyen funkciókra van szüksége a felhasználónak. Így a felület részletei már jóval a megoldás végleges megvalósítása előtt megjelennek.
Természetesen ugyanazok a kódolási szabványok követelményei vonatkoznak a tesztekre, mint a fő kódra.
Ez a munkafolyamat Kent Beck Test Driven Development: By example című könyvén alapul . [egy]
A teszteléssel történő fejlesztéskor minden új funkció ( eng. feature ) hozzáadása a programhoz egy teszt írásával kezdődik. Ez a teszt elkerülhetetlenül sikertelen lesz, mert a megfelelő kód még nincs megírva. (Ha az írásbeli teszt sikeres, akkor vagy már létezik a javasolt "új" funkció, vagy a tesztnek vannak hibái.) A teszt megírásához a fejlesztőnek egyértelműen meg kell értenie az új funkció követelményeit. Ehhez figyelembe veszik a lehetséges használati eseteket és felhasználói történeteket. Az új követelmények a meglévő teszteket is megváltoztathatják. Ez különbözteti meg a tesztvezérelt fejlesztést azoktól a technikáktól, amelyekben a teszteket a kód megírása után írják: ez arra kényszeríti a fejlesztőt, hogy a kód megírása előtt a követelményekre összpontosítson – ez egy finom, de fontos különbség.
Ebben a szakaszban ellenőrzik, hogy az imént írt tesztek nem mennek-e át. Ez a szakasz magát a tesztet is ellenőrzi: az írásbeli vizsga mindig sikeres lehet, ezért haszontalan. Az új tesztek nyilvánvaló okokból sikertelenek. Ez növeli a bizalmat (bár nem garantálja teljesen), hogy a teszt valóban azt teszteli, amire tervezték.
Ebben a szakaszban új kódot írnak, hogy a teszt sikeres legyen. Ennek a kódnak nem kell tökéletesnek lennie. Elfogadható, hogy valamilyen inelegáns módon megfeleljen a tesztnek. Ez elfogadható, mivel a következő lépések javítják és csiszolják azt.
Fontos, hogy olyan kódot írjunk, amelyet kifejezetten a teszt sikeres teljesítéséhez terveztek. Ne adjon hozzá szükségtelen és ennek megfelelően nem tesztelt funkciókat.
Ha minden teszt sikeres, a programozó biztos lehet benne, hogy a kód megfelel az összes tesztelt követelménynek. Ezt követően továbbléphet a ciklus utolsó szakaszába.
Ha a szükséges funkcionalitás elérte, a kód ebben a szakaszban megtisztítható. Az újrafaktorálás egy program belső szerkezetének megváltoztatásának folyamata anélkül, hogy befolyásolná a külső viselkedését, és azzal a céllal, hogy könnyebben megértsék a program működését, kiküszöböljék a kódduplikációkat, és megkönnyítsék a közeljövőben végrehajtandó változtatásokat.
A leírt ciklus megismétlődik, egyre több új funkciót megvalósítva. A lépéseknek kicsiknek kell lenniük, 1 és 10 közötti változtatásnak a tesztfutások között. Ha az új kód sikertelen az új teszteken, vagy a régi tesztek leállnak, a programozónak vissza kell térnie a hibakereséshez . Harmadik féltől származó programkönyvtárak használatakor ne végezzen olyan kicsi változtatásokat, hogy azok szó szerint magát a harmadik féltől származó könyvtárat teszteljék [3] , és ne az azt használó kódot, hacsak nem merül fel a gyanú, hogy a könyvtár hibákat tartalmaz.
A tesztvezérelt fejlesztés szorosan összefügg az olyan elvekkel, mint a „ legyen egyszerű, buta, csókolj ” és „ nem lesz rá szükséged, YAGNI ” . A dizájn tisztább és áttekinthetőbb lehet, ha csak a teszt sikeres teljesítéséhez szükséges kódot írja meg. [1] Kent Beck a " hamisít, amíg meg nem csinálod " elvet javasolja . A tesztelt funkcióhoz teszteket kell írni. Ennek két előnye van. Ez segít abban, hogy az alkalmazás tesztelhető legyen, mivel a fejlesztőnek a kezdetektől fogva gondolnia kell, hogy az alkalmazás hogyan lesz tesztelve. Segít abban is, hogy minden funkciót lefedjenek a tesztek. Amikor egy funkciót a tesztelés előtt írnak meg, a fejlesztők és a szervezetek hajlamosak a következő szolgáltatásra lépni anélkül, hogy a meglévőt tesztelnék.
Az újonnan megírt teszt sikertelenségének ellenőrzése segít abban, hogy a teszt valóban tesztel valamit. Csak ezen ellenőrzés után kezdje meg az új funkció bevezetését. Ezt a „vörös/zöld/refaktoring” néven ismert technikát „tesztvezérelt fejlesztési mantrának” nevezik. A piros itt azokat jelenti, akik nem mentek át a vizsgákon, a zöld pedig azokat, akik sikeresek.
A kialakult tesztvezérelt fejlesztési gyakorlat az Acceptance Test-driven Development (ATDD ) technika megalkotásához vezetett, amelyben a megrendelő által leírt kritériumok automatizálódnak átvételi tesztekké, amelyeket aztán a szokásos fejlesztési folyamatban egységteszttel használnak fel ( hun .unit tesztvezérelt fejlesztés, UTDD ). [4] Ez a folyamat biztosítja, hogy az alkalmazás megfeleljen a megadott követelményeknek. Az átvételi teszteléssel történő fejlesztés során a fejlesztőcsapat egy egyértelmű célra összpontosít: a vonatkozó felhasználói követelményeket tükröző elfogadási tesztek kielégítésére.
Átvételi (funkcionális) tesztek ( angol nyelvű ügyféltesztek, átvételi tesztek ) - olyan tesztek, amelyek ellenőrzik, hogy az alkalmazás funkcionalitása megfelel-e az ügyfél követelményeinek. Az átvételi teszteket az ügyfél oldalán végzik. Ez segít neki abban, hogy minden szükséges funkciót megkapjon.
Egy 2005-ös tanulmány kimutatta, hogy a tesztvezérelt fejlesztés több tesztet ír, és azok a programozók, akik több tesztet írnak, általában produktívabbak. [5] A kódminőség és a TDD közötti hipotézisek nem voltak meggyőzőek. [6]
Azok a programozók, akik TDD-t használnak az új projektekhez, arról számolnak be, hogy kevésbé valószínű, hogy szükségét érzik hibakereső használatának. Ha egyes tesztek hirtelen meghiúsulnak, a hibakeresésnél hatékonyabb lehet a visszaállítás a legújabb verzióra, amely minden teszten megfelelt. [7]
A tesztvezérelt fejlesztés nemcsak a validálást kínálja, hanem a programtervezést is befolyásolja. Ha kezdetben a tesztekre koncentrálunk, könnyebb elképzelni, milyen funkciókra van szüksége a felhasználónak. Így a fejlesztő a megvalósítás előtt átgondolja a felület részleteit. A tesztek arra kényszerítik, hogy tesztelhetőbbé tegye a kódot. Például hagyja el a globális változókat, a szingliváltozókat, kevésbé kapcsolja össze az osztályokat és könnyebben használhatóvá tegye. Az erősen csatolt vagy összetett inicializálást igénylő kódok tesztelése lényegesen nehezebb lesz. Az egységtesztelés hozzájárul a világos és kicsi interfészek kialakításához. Minden osztálynak meghatározott szerepe lesz, általában egy kicsi. Ennek következtében csökken az osztályok közötti elköteleződés, és nő az összeköttetés. A szerződéses programozás ( angol. design by contract ) kiegészíti a tesztelést, nyilatkozatokon keresztül alakítja ki a szükséges követelményeket ( eng. assertions ).
Bár a tesztvezérelt fejlesztés több kódot igényel, a teljes fejlesztési idő általában rövidebb. A tesztek védelmet nyújtanak a hibák ellen. Ezért a hibakeresésre fordított idő többszörösére csökken. [8] Számos teszt segít csökkenteni a kódban található hibák számát. A fejlesztés korai szakaszában lévő hibák kijavítása megakadályozza a krónikus és költséges hibákat, amelyek a későbbiekben hosszú és fárasztó hibakereséshez vezetnek.
A tesztek lehetővé teszik a kód újrafaktorizálását anélkül, hogy fennállna annak veszélye, hogy elrontja. Ha módosítja a jól tesztelt kódot, sokkal kisebb az új hibák bevezetésének kockázata. Ha az új funkcionalitás hibákhoz vezet, a tesztek, ha vannak, természetesen azonnal megmutatják. Ha olyan kóddal dolgozik, amelyhez nincs teszt, akkor a hiba hosszú idő elteltével fedezhető fel, amikor is sokkal nehezebb lesz a kóddal dolgozni. A jól tesztelt kód könnyen tolerálja az újrafaktorálást. Az a bizalom, hogy a változtatások nem törik meg a meglévő funkciókat, magabiztosságot ad a fejlesztőknek és növeli hatékonyságukat. Ha a meglévő kódot jól lefedik a tesztek, a fejlesztők sokkal szabadabban hozhatnak olyan építészeti döntéseket, amelyek javítják a kód kialakítását.
A tesztvezérelt fejlesztés modulárisabb, rugalmasabb és bővíthetőbb kódot ösztönöz. Ennek az az oka, hogy ezzel a módszertannal a fejlesztőnek úgy kell a programra gondolnia, mint sok kis modulra, amelyeket egymástól függetlenül írnak és tesztelnek, és csak ezután kapcsolják össze. Ez kisebb, speciálisabb osztályokat, kevesebb csatolást és tisztább interfészt eredményez. A mockok használata szintén hozzájárul a kód modularizálásához, mivel egyszerű mechanizmust igényel a hamis és a normál osztályok közötti váltáshoz.
Mivel csak a teszt sikeres teljesítéséhez szükséges kódot írják le, az automatizált tesztek lefedik az összes végrehajtási utat. Például egy új feltételes utasítás hozzáadása előtt a fejlesztőnek egy tesztet kell írnia, amely motiválja ennek a feltételes utasításnak a hozzáadását. Ennek eredményeként a tesztvezérelt fejlesztés eredményeként kapott tesztek meglehetősen teljesek: észlelik a kód viselkedésében bekövetkező nem kívánt változásokat.
A tesztek dokumentációként használhatók. A jó kód minden dokumentációnál jobban megmondja, hogyan működik. Előfordulhat, hogy a kódban található dokumentáció és megjegyzések elavultak. Ez zavaró lehet a fejlesztők számára, akik a kódot nézik. És mivel a dokumentáció a tesztekkel ellentétben nem mondhatja, hogy elavult, nem ritkák az olyan helyzetek, amikor a dokumentáció nem igaz.
A tesztcsomagnak hozzá kell férnie a tesztelés alatt álló kódhoz. Másrészt nem szabad megsérteni a tokozás és az adatrejtés elvét. Ezért az egységteszteket általában ugyanabban az egységben vagy projektben írják, mint a tesztelt kódot.
Előfordulhat, hogy a tesztkód nem fér hozzá a privát mezőkhöz és metódusokhoz . Ezért az egységteszt további munkát igényelhet. A Java nyelven a fejlesztő a tükrözés segítségével hivatkozhat a privátként megjelölt mezőkre . [10] Az egységtesztek megvalósíthatók a belső osztályokban, így hozzáférhetnek a külső osztály tagjaihoz. A .NET-keretrendszerben részosztályok használhatók privát mezők és metódusok elérésére egy tesztből.
Fontos, hogy a kizárólag tesztelési célokat szolgáló kódrészletek ne maradjanak a kiadott kódban. C nyelvben erre a feltételes fordítási direktívák használhatók. Ez azonban azt jelenti, hogy a kiadott kód nem egyezik pontosan a tesztelt kóddal. Az integrációs tesztek szisztematikus futtatásával egy kiadott builden biztosíthatja, hogy ne maradjon olyan kód, amely implicit módon az egységtesztek különböző aspektusaira támaszkodik.
A tesztvezérelt fejlesztést használó programozók között nincs egyetértés abban, hogy mennyire értelmes a privát, védett módszerek és adatok tesztelése . Egyesek meg vannak győződve arról, hogy bármelyik osztályt elég csak a nyilvános felületén tesztelni, mivel a privát változók csak egy implementációs részlet, amely változhat, és ennek változásai nem jelenhetnek meg a tesztcsomagban. Mások azzal érvelnek, hogy a funkcionalitás fontos aspektusai megvalósíthatók privát módszerekben, és ezek nyilvános interfészen keresztüli implicit tesztelése csak bonyolítja a dolgokat: az egységteszt a funkcionalitás lehető legkisebb egységeinek tesztelését jelenti. [11] [12]
Az egységtesztek minden egységet külön-külön tesztelnek. Nem számít, hogy a modul több száz tesztet tartalmaz, vagy csak ötöt. A tesztvezérelt fejlesztésben használt tesztek nem léphetik át a folyamathatárokat, használnak hálózati kapcsolatokat. Ellenkező esetben a tesztek sikeres teljesítése hosszú időt vesz igénybe, és a fejlesztők kevésbé valószínű, hogy a teljes tesztcsomagot lefuttatják. A külső moduloktól vagy adatoktól való függőség bevezetése az egységteszteket is integrációs tesztekké változtatja. Ugyanakkor, ha a lánc egyik modulja hibásan viselkedik, nem biztos, hogy azonnal világos, melyik.
Amikor a fejlesztés alatt álló kód adatbázisokat, webszolgáltatásokat vagy más külső folyamatokat használ, érdemes kiemelni a tesztelés által lefedett részt. Ez két lépésben történik:
Hamis és hamis objektumok használata a külvilág ábrázolására azt eredményezi, hogy a valódi adatbázist és más külső kódokat nem tesztelik a tesztvezérelt fejlesztési folyamat eredményeként. A hibák elkerülése érdekében a fent leírt interfészek valós implementációinak tesztelése szükséges. Ezek a tesztek elválaszthatók a többi egységteszttől, és valóban integrációs tesztek. Kevesebbre van szükségük, mint a modulárisaknak, és ritkábban indíthatók el. Leggyakrabban azonban ugyanazt a tesztelési keretrendszert használják , mint az egységteszteket .
Az adatbázisban lévő adatokat módosító integrációs teszteknek vissza kell állítaniuk az adatbázist a teszt futtatása előtti állapotba, még akkor is, ha a teszt sikertelen. Ehhez gyakran használják a következő technikákat:
Léteznek Moq, jMock, NMock, EasyMock, Typemock, jMockit, Unitils, Mockito, Mockachino, PowerMock vagy Rhino Mocks könyvtárak, valamint sinon a JavaScript számára, amelyek célja az álobjektumok létrehozásának egyszerűsítése.