Budapesti M¶szaki és Gazdaságtudományi Egyetem Villamosmérnöki és Informatikai Kar Méréstechnika és Információs Rendszerek Tanszék
Tesztelési megoldások .NET és Windows Phone környezetben
Szakdolgozat
Készítette
Konzulens
Biró Loránd
dr. Micskei Zoltán
2014. má jus 25.
Tartalomjegyzék Kivonat
4
Abstract
5
Bevezet®
6
1. Szoftvertesztelés
8
1.1.
A tesztelés célja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
1.2.
Szoftvertesztelés fajtái
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
1.3.
Tesztelés komplexitása . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
1.4.
Tesztelési szintek
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
1.4.1.
Egységteszt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
1.4.2.
Integrációs teszt
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
1.4.3.
Rendszer teszt
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
Lazán csatolt rendszerek . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
1.5.1.
14
1.5.
Objektum-orientált tervezési elvek
. . . . . . . . . . . . . . . . . . .
2. A tesztelend® alkalmazás
18
2.1.
Funkcionális specikáció . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
2.2.
Architektúra
21
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3. A tesztelés megtervezése 3.1.
23
Egységtesztek . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
3.1.1.
Domain Model
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
3.1.2.
Domain Repositories . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
3.1.3.
Domain Services
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
3.1.4.
Application és Presentation réteg . . . . . . . . . . . . . . . . . . . .
26
3.2.
Integrációs tesztek
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
3.3.
Teljesítmény tesztek
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
3.4.
Manuális tesztek
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
4. Technológiai megoldások
28
4.1.
Windows alkalmazás egységtesztelése . . . . . . . . . . . . . . . . . . . . . .
28
4.2.
Windows Phone alkalmazás egységtesztelése . . . . . . . . . . . . . . . . . .
30
4.3.
Windows Phone alkalmazás egységtesztelése Windows platformon . . . . . .
32
1
5. Tesztek implementálása 5.1.
5.2.
5.3.
Domain Model
35
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
5.1.1.
Entry
5.1.2.
Category
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
5.1.3.
RecurrenceRule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
40
Domain Services
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
42
5.2.1.
SummariesDomainService
. . . . . . . . . . . . . . . . . . . . . . . .
42
5.2.2.
CategoriesDomainService
. . . . . . . . . . . . . . . . . . . . . . . .
44
Domain Repositories . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
6. Értékelés
48
6.1.
Kódfedettség
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
48
6.2.
Tesztelés során felderített hibák . . . . . . . . . . . . . . . . . . . . . . . . .
49
6.3.
Élesben felderített hibák . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
50
Összefoglalás
51
Irodalomjegyzék
53
Függelék
54
F.1. A Domain réteg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
54
HALLGATÓI NYILATKOZAT Alulírott
Biró Loránd, szigorló hallgató kijelentem, hogy ezt a szakdolgozatot meg nem
engedett segítség nélkül, saját magam készítettem, csak a megadott forrásokat (szakirodalom, eszközök stb.) használtam fel. Minden olyan részt, melyet szó szerint, vagy azonos értelemben, de átfogalmazva más forrásból átvettem, egyértelm¶en, a forrás megadásával megjelöltem. Hozzájárulok, hogy a jelen munkám alapadatait (szerz®(k), cím, angol és magyar nyelv¶ tartalmi kivonat, készítés éve, konzulens(ek) neve) a BME VIK nyilvánosan hozzáférhet® elektronikus formában, a munka teljes szövegét pedig az egyetem bels® hálózatán keresztül (vagy autentikált felhasználók számára) közzétegye. Kijelentem, hogy a benyújtott munka és annak elektronikus verziója megegyezik. Dékáni engedéllyel titkosított diplomatervek esetén a dolgozat szövege csak 3 év eltelte után válik hozzáférhet®vé.
Budapest, 2014. május 25.
Biró Loránd hallgató
Kivonat Az utóbbi évtizedekben a számítástechnika egyre inkább beépült az emberek hétköznapjaiba, és a felhasználóknak egyre nagyobb igénye lett a szoftverekkel szemben. Így a szoftverfejlesztési folyamatok hosszúak és bonyolultak lehetnek, ennek során pedig elkerülhetetlenül hibákat is vétenek. Ennek egyik oka lehet egy apró gyelmetlenség a fejleszt® részér®l, de a nagy szoftverek komplexitása miatt könny¶ hibázni. Ezen túl a specikációban, vagy akár az igényekben is lehetnek hibák, amik sokkal súlyosabb következményekkel járhatnak. Egy jó szoftvertesztelési folyamat ezekre nyújt megoldást. A szakdolgozatban a szoftvertesztelési alapfogalmaknak és elveknek a bemutatásával kezdem, amelyek elengedhetetlenek a hatékony teszteléshez. Egy fejleszt®nek ezenkívül tisztában kell lennie azokkal a szoftvertervezési irányelvekkel, amelyek egyáltalán lehet®séget adnak egységtesztek leírására, ezért bemutatom a SOLID alapelveket is. Ezek átláthatóbb, kezelhet®bb és tesztelhet®bb szoftvert eredményeznek. A tesztelend® Windows Phone alkalmazás bemutatása után, a tesztelési folyamatot tervezem meg, vagyis meghatározom, hogy a szoftver különböz® szintjein milyen módon célszer¶ tesztelni: egységtesztekkel az alacsonyabb szint¶ szoftverkomponensek tesztelését valósítom meg, gyelembe véve a komponensek közötti függ®ségeket; integrációs tesztekkel a komponensek helyes együttm¶ködésér®l bizonyosodom meg; rendszertesztekkel pedig az alkalmazás teljes funkcionalitását vizsgálom. A tesztek implementációja el®tt kitérek a platformon elérhet® tesztel® eszközök m¶ködésére, mert a .NET és Visual Studio által nyújtott eszközök a Windows Phone 8 platformon csak korlátozottan érhet®ek el. Az egyik legsúlyosabb hiányosság a kódfedettség mérésének lehet®sége, ezért megvizsgálom ennek technikai okát és bemutatok egy megoldást rá. Ezután az alkalmazáshoz készített egység és integrációs teszteken keresztül mutatom be a tipikus tesztírás problémáit és megoldásait. Végül a megtervezett tesztelési folyamat számos hibát akadályozott meg és derített fel, ráadásul pozitív hatással volt az alkalmazás kódmin®ségére. Az alkalmazáshoz összesen 112 egység és integrációs teszt készült el, amelyek a modell rétegen 99,1%-os, a szolgáltatás rétegen pedig 100%-os kódfedettséget biztosítottak. Mindezt könnyen átlátható és fenntartható tesztekkel sikerült elérnem.
4
Abstract Over the last few decades technology has become integrated within our lives, and the user's expectations of applications have grown more and more. The software processes got longer and more complex, which implies inevitable mistakes. One of the causes can be a slight inattention of a developer, but it's hard not to make any mistakes in a big and complex software code. Also there might be errors in the specication, or even in the requirements, which can lead to serious consequences. In this thesis I start with presenting the basic concepts and ideas of software testing, which are crucial to eective testing. Also a developer should be familiar with the basic design principles, the guidelines which make the testing possible; therefore I present the SOLID principles. The usage of these principles will make the software code cleaner, more maintainable and more testable. After introducing the tested Windows Phone application, I plan the testing process. I determine the eective testing techniques for the dierent parts of the software: I cover the low level software components using unit tests with consideration of the dependencies between the components, I make sure of the correctness of cooperation between the components using integration tests, and nally I investigate the application's full functionality with system tests. Before presenting the test implementations, I cover the operation of the testing tools on dierent platforms, because the .NET and Visual Studio provided tools have some limitations on the mobile platform. One of the most signicant limitations is the lack of code coverage measurement, so I examine the reasons and present a workaround. After that, through the application's unit and integration tests, I present some typical test coding problems and their solutions. In the end the planned testing process prevented and discovered several defects, also it had a positive eect on code quality. 112 unit and integration tests were made for this application, which resulted in a 99.1% code coverage in the model layer and a 100% code coverage on the service layer, while the test code remained clean and maintainable.
5
Bevezet® A szoftvertesztelés a fejlesztési folyamatnak egy nagyon fontos része. Ahogy egy szoftver tudása n®, ahogy a kódbázis mérete és komplexitása is egyre nagyobb lesz, egyre költségesebbé válik fenntartani egy rendszert. Ennek egyik legf®bb oka az, hogy fejlesztés során elkerülhetetlenül hibák kerülnek a rendszerbe. Egy felmérés szerint átlagosan 1-25 új hibára lehet számítani minden 1000 sor megírt forráskódban [7], amit ha összevetünk a
Windows XP
45 millió soros kódbázisával [8], érezhet® a probléma súlyossága. A Microsoft
a fejlesztései során 1000 soronként átlagosan 10-20 hibát tapasztalt [7], amit házon belüli teszteléssel és kódfelülvizsgálati technikákkal 0.5 hibára sikerült csökkentenie, de olyan érett fejlesztési folyamatok is léteznek, ahol 1000 sor kód hibáinak átlagos számát 3-ról 0,1-re sikerül csökkenteni [10]. Természetesen ez a hibaarány a képességeinek megfelel®en fejleszt®r®l-fejleszt®re változik, de csapat szintjén els®sorban a fejlesztési folyamat és az alkalmazott fejlesztési technikák eredményezik, hogy hány hiba kerül, és hány hiba marad a rendszerben. Ennek a problémának az els® fele az, hogy hány hiba kerül a rendszerbe, amit els®sorban a fejlesztési technikák határoznak meg. Néhány száz sornyi forráskódot könnyedén átlát egy fejleszt®, de amikor csapatok dolgoznak egy több százezer sorból álló projekten, már nem várható el, hogy mindenki átlássa és pontosan megértse a kód minden részletét. Ennek hatása közvetlenül tapasztalható a fejleszt®k produktivitásában: minél nagyobb a kódbázis, annál kevesebb új kód születik adott id® alatt, és annál tovább tart kinyomozni és kijavítani egy hibát. Ahhoz, hogy az ilyen nagy komplexitású rendszerek kezelhet®ek maradjanak, olyan szoftvertervezési mintákat és technikákat kell alkalmazni, amivel a rendszer átlátható és könnyen tesztelhet® marad. A másik kérdés az az, hogy hány hiba marad a rendszerben, amire maga a tesztelés ad megoldást. Statisztikák alapján belátható, hogy egy nagyobb rendszernél a tesztelési fázis elengedhetetlen, ezt pedig tipikusan több fonton vívják: egyrészr®l a fejleszt®k a fejlesztés során nem csak a funkciók implementálásán és módosításán dolgoznak, hanem ezzel egyidej¶leg, vagy közel ugyanakkor automatizált teszteket (egységteszteket) is írnak az adott komponens helyességének ellen®rzésére, másrészt fejlesztési ismeretekkel nem feltétlenül rendelkez® tesztel® szakemberek is ellen®rzik a szoftver helyességét. Utóbbi általában automatizált és manuális technikák együttes alkalmazásából áll.
6
Motiváció Egy cégnél fejlesztenem kellett egy mobilalkalmazást. Ez csak másodlagos feladat volt, az id® nagy részében egy csapattal kellett dolgoznom egy másik terméken, de igény támadt egy egyszer¶ Windows Phone alkalmazásra, amit lehet®ségem volt önállóan megvalósítani. Miután sikerült specikálni a szoftvert egy hétköznapi költségek követésére alkalmas alkalmazást rövid id®n belül sikerült is el®állni egy alfa verzióval. Egy társammal együtt tesztelésképpen el is kezdtük használni az alkalmazást, végül sikerült egy tucatnyi kisebbnagyobb hibát felderíteni és kijavítani. Ezután, hogy már a gyakorlatban lehetett látni az eredeti koncepciót, el®kerültek új ötletek és változtatni kellett az alkalmazáson, ami végül további hibákat és hibajavításokat eredményezett. Kis id®vel ezután felismertük, hogy az alkalmazás teljesítménye nem a megfelel® egy bizonyos bejegyzés szám fölött, ennek megoldására pedig már alapjaiban kellett módosítani a szoftvert. Ez sajnos megint tucatnyi új hibát eredményezett, amit csak az éles használatban, manuális teszteléssel tudtunk felderíteni, ráadásul egyre átláthatatlanabbá vált a kódja a rendszernek. Ezt persze az ember valamilyen szinten kudarcnak éli meg. Lehet, hogy az alkalmazás éppen jól m¶ködik, de tudtam, hogy ha megint b®víteni vagy változtatni kell az alkalmazáson, újra hibák sorozatával kell majd megküzdenem. Nemrég újra igény támadt néhány változtatásra és új funkcióra, és újra el® kellett vennem ezt a kódot, de végül nem akartam hozzányúlni. Az els® verzió óta több munkában részt vettem, és láttam, hogy mennyivel jobban meg lehetett volna valósítani a rendszert. Mivel ez az alkalmazás nem élvez nagy prioritást, lehet®ségem volt újraírni azt. Még tisztán emlékeztem milyen nehézségeket okozott ez az alkalmazás az els® alkalommal, ezért úgy döntöttem, hogy a fejlesztési folyamattal együtt immár a tesztelésbe is komolyabb er®feszítést teszek. A célom egy megbízható, hibamentes alkalmazás, és egy kidolgozott tesztelési folyamattal azt szeretném elérni, hogy a rendszer további változtatásai se eredményezzenek hibákat.
A szakdolgozat szerkezete El®ször áttekintem a szoftvertesztelés alapfogalmait és technikáit, majd megvizsgálom az irányelveket, amik egy jól tesztelhet® szoftver tervezéséhez szükségesek (1. fejezet). A 2. fejezetben a tesztelend® alkalmazást mutatom be, mind funkcionálisan és architektúrálisan is, majd ezeknek fényében a 3. fejezetben megtervezem a szoftver tesztelését. Az emulátorban a tesztel®i eszközök csak korlátozottan érhet®ek el, ezért a 4. fejezetben egy megoldást mutatok be arra, hogy hogyan lehet a mobilalkalmazás kódjának bizonyos részeit Windows platformon tesztelni. Az 5. fejezetben az automatizált tesztek implementációinak egy részét mutatom be, végül a 6. fejezetben kiértékelem a teljes tesztelési folyamat eredményeit.
7
1. fejezet
Szoftvertesztelés Mint ahogy minden ember elkövet kisebb-nagyobb hibákat az életében, a szoftver fejlesztésében résztvev® személyek is törvényszer¶en vétenek hibákat. Az elkészített szoftver nem mindig viselkedik úgy, mint ahogy kéne neki. Ezek lehetnek apróságok, amiket akár gyelmen kívül is lehet hagyni, de egy hibásan m¶köd® szoftver komoly következményekkel is járhat:
Elvesztett pénz
Az ügyfelek elpártolhatnak, vagy konkrét pénzbírság is járhat, ha bizo-
nyos feltételeknek nem sikerül eleget tenni.
Elvesztett id®
Sok ember függhet egy élesben m¶köd® szoftvert®l, annak meghibásodása
pedig láncreakció szer¶en okozhatja az emberek munkájának leállását.
Elvesztett bizalom
Az ügyfelek egy szoftver hibáját a vállalat inkompetenciájával/meg-
bízhatatlanságával azonosítják. Talán azonnal nem is érezhet®, de hosszú távon hatalmas bevételkiesést eredményezhet.
Sérülés vagy halál
Vannak bizonyos biztonság-kritikus rendszerek, amelyek meghibáso-
dása akár emberéleteket is követelhetnek. (Légiforgalom irányító szoftver, utasszállító/¶rrepül® vezérlés, stb.)
Ezen felül a jó szoftverfejleszt® büszke akar lenni a munkájára. Olyat akar letenni az asztalra, ami pontosan azt csinálja, amit elvárnak t®le. Olyat, ami megbízhatóan m¶ködik, és ami nem omlik össze rögtön, ahogy módosítani kell rajta. Egyszóval min®ségre törekszik. A következ® fejezetben megpróbálom bemutatni az általános tesztelési módszereket és alapelveket, majd azokat a szoftver tervezési irányelveket, amik egy könnyen tesztelhet®
The Art of Software Magyar Szoftvertesztel®i
szoftver megalkotásához szükségesek. A fejezet els® fele els®sorban a
Testing [11] m¶re támazkodik, a magyar terminológiát pedig a Tanács Egyesület [5] kifejezésgy¶jteményének megfelel®en használtam.
1.1. A tesztelés célja A szoftvertesztelés egyik deníciója: A tesztelés a termékmin®ség kiértékelése, majd növelése a problémák és hibák azonosításával. [1] A tesztelés a fejlesztési folyamat szerves
8
Küls® min®ség Bels® min®ség Jöv®beli min®ség Helyesség
Hatékonyság
Rugalmasság
Megbízhatóság
Tesztelhet®ség
Újrahasznosíthatóság
Használhatóság
Dokumentáció
Fenntarthatóság
Integritás
Struktúra
1.1. táblázat. Tipikus min®ségi szempontok[4]
része, ami ugyan úgy inkrementális és iteratív lehet, mint maga a fejlesztés. A szoftver korai és rendszeres tesztelésével csökkenthetjük a fejlesztési költségeket és növelhetjük a végfelhasználói elégedettséget a szoftver min®ségének növelésével. A szoftver tesztelésének célja röviden megfogalmazva a min®ség növelése, de a min®ség különböz® ágazatokban, különböz® szoftvereknél és különböz® embereknél mást jelent. Egy gyártási folyamat min®ségét teljesen máshogy értelmezik, mint egy szoftver min®ségét, de egy banki rendszer min®ségér®l is máshogy kell beszélni, mint egy hírportál min®ségér®l. Hogy mit is jelent a min®ség, azt az adott alkalmazás és üzlet igényei határozzák meg. Egy lehetséges kifejtése a min®ségi szempontoknak a 1.1 táblázatban látható. Ha a szoftver tesztelésér®l beszélünk, nem csak a helyességet kell vizsgálni, valamilyen módon a többi szempontot is vizsgálni kell, de azok kívül esnek a szakdolgozat témáján. Az olyan szempontok vizsgálatára koncentrálok, amelyeket tipikusan a szoftverfejleszt® tesztel a rendelkezésére álló eszközökkel.
1.2. Szoftvertesztelés fajtái A szoftverfejlesztési technikákat és módszereket sokféleképpen lehet kategorizálni:
Statikus/Dinamikus
Vagy a kódot vizsgáljuk és hibákat/hibákra utaló jeleket keresünk
(statikus), vagy kipróbáljuk a rendszert/komponenst és a kimenetének helyességét ellen®rizzük (dinamikus). A statikus ellen®rzés történhet például kód felülvizsgálat (review) formájában, amikor más szoftverfejleszt®kkel együtt átnézésre kerül a kód, de a modern fejleszt®eszközökben automatizált kód analizátorok is elérhet®ek. Például a Visual Studio megfelel® kiadásaiban rendelkezésünkre áll a
Code Analysis
eszköz, ami rengeteg
könnyen elkövethet® hibát képes detektálni. (Például azt, ha egy paraméterül kapott objektumot azel®tt használnánk, hogy ellen®riznénk
null -t
kaptunk-e. Ez és számos
ehhez hasonló probléma nehezen felderíthet® hibákhoz vezethet.) A dinamikus tesztelés legegyszer¶bb ad-hoc formája az, amikor a fejleszt® elindítja az alkalmazást és kipróbálja az új funkciók m¶ködését. Ekkor a fejleszt® leginkább az aktuális technikai kihívásokra koncentrál, ezért ez általában elég felületes. A fejlesztéshez az elmének egy konstruktív állapotában kell lennie, míg a teszteléshez egy kritikus hozzáállásra van szükség, ezért a kett®t érdemes elkülöníteni. Egy szoftver dinamikus tesztelése id®igényes, ezért nem elvárható, hogy a tesztel®k folyamatosan fejben tudják tartani épp mit és hogyan kell tesztelni. Ennek megoldá-
9
sára teszteseteket deniálnak és tesztesetek hosszú sorát állítják össze, amivel az egyes funkciók helyességér®l nyerhetnek magabiztosságot. Ezzel próbálják meg elérni, hogy a tesztelési folyamat ellen®rizhet® és reprodukálható legyen. Egy ilyen tesztesetnek két részb®l kell állnia: az elvégzend® lépések sorából és az elvárt viselkedés leírásából. Például Kattints a listában szerepl® els® termék mellett található Kosárba feliratú gombra. A kosár mellett található számnak n®nie kell eggyel. A tesztesetek lehetnek ehhez hasonlóan informálisak, de akár olyan részletesek is, amiket akár a számítógép is el tud végezni.
Automatizált/Manuális
Az el®z® pontban szó esett az automatizált és manuális stati-
kus tesztelésr®l is, viszont a dinamikus tesztelés automatizálására is van lehet®ség. Bizonyos rendszerek lehet®séget adnak arra, hogy egy jól deniált nyelvet használva fogalmazhassuk meg a tesztesetetek lépéseit és elvárt eredményeit, amiknek a teljesülését a rendszer automatikusan ellen®rizni tudja. Ezek tipikusan az alkalmazás felhasználói felületén m¶köd® rendszerek, amik technikailag a felhasználó egér és billenty¶ használatát szimulálják, ezért ezeket
GUI
vagy
UI Automation
tesztelésnek
hívják. Sajnos két komoly hiányossága van ennek a technikának: Egyrészt a leíró nyelv, amit egy ilyen rendszer használ, korlátokat szab a tesztesetek eszköztárában, másrészt jellegéb®l adódóan nagyon magas szint¶ek az esetek és csak az egész rendszer tesztelésére ad lehet®séget. Az automatizált tesztelésnek egy sokkal elemibb formája az, ha például minden teszteset egy-egy függvény a vizsgált szoftver nyelvében megírva. Egy ilyen függvény kipróbálhatja a rendszer magas szint¶ komponenseinek együttm¶ködését, vagy akár elemi m¶veletek helyességét is ellen®rizheti, hiszen az adott nyelv képességeinek megfelel®en minden fajta teszt megvalósítható. Erre a fajta tesztelésre kés®bb részletesebben is kitérek. Az automatizált tesztelés egyik legnagyobb el®nye, hogy újra és újra könnyedén lefuttatható, így fáradalmas manuális tesztelés nélkül is meg lehet gy®z®dni arról, hogy egy változás vagy hibajavítás nem okozott újabb hibákat. A hátránya pedig leginkább az, hogy a tesztesetek formális deniálása id®igényes és a fenntartása költségessé válhat, ha maga a rendszer és a funkciói gyakran megváltoznak. A manuális teszteléssel az a legnagyobb probléma, hogy az emberi agy egy megbízhatatlan rendszer. Ha a tesztel® azt szeretné látni, hogy a rendszer jól m¶ködik, hajlamos olyan teszteseteket végrehajtani, amir®l tudja, vagy amir®l úgy véli, hogy jól fog m¶ködni, viszont ennek nem feltétlenül van haszna. A manuális tesztelés jellegéb®l adódóan er®sen repetitív, az ember tudatalattija pedig képes rutint épít az ismétl®d® feladatokból. Ezzel azt éri el, hogy kevesebb koncentrációval is képes elvégezni a teszteseteket, de ezt úgy, hogy a lényegtelennek t¶n® részleteket kisz¶ri és eldobja, így könnyen el lehet siklani egy-egy apró hibán. Ezekkel szemben a manuális tesztelésnek az a nagyszer¶sége, hogy olyan hibákat is képes észrevenni, amiket nem is keresett.
10
Feketedoboz/Fehérdoboz
A feketedoboz tesztelés lényege, hogy a tesztel® egy kívül-
állóként áll hozzá a teszteléshez. Nem tudja, hogy hogy m¶ködik az, amit tesztel (legyen az egy osztály vagy egy komplett rendszer), csak azt tudja, hogyan kell használni és hogy mit vár el t®le. Ezzel szemben a fehérdoboz tesztelés során a tesztel® nemcsak, hogy tisztában van a m¶ködéssel, de az alapján deniálja a teszteseteit. Elméletileg, ha a fekedoboz tesztelés során minden lényeges bemenet/lépés kombinációt letesztelünk, a mögöttes kód minden sora és utasítása legalább egyszer lefut minden lényeges állapotában. Sajnos ezt feketedoboz tesztelés során nehezen lehet elérni, mert képtelenség minden lehetséges bemenetre tesztelni, és az implementáció ismerete nélkül nem tudjuk pontosan meghatározni mik azok az esetek, amikre az adott megoldás érdekesen viselkedik. Például a .NET
List
osztály
Sort()
metódusának egyszer¶ a feladata: a listában szerepl® elemeket újrarendezi növekv® sorrendben. Tesztelhetjük egy 0 elem¶ listára, egy 1 elem¶ listára, néhány tucat kisebb listára, néhány hatalmas listára, és azt hihetjük, hogy alaposak voltunk. Viszont ha belenézünk, hogy hogyan is m¶ködik a kérdéses metódus, kiderül, hogy 16 elemszám alatt beszúrásos rendezést használ, bizonyos esetekben pedig
Quicksort
Heapsort
vagy
algoritmust használ. Ezen információk nélkül nem valószín¶, hogy minden
lényeges tesztesetet sikerül majd deniálni, könnyen megeshet például, hogy a tesztek során nem is lesz kipróbálva a
Heapsort
algoritmus.
A fehérdoboz tesztelés során meg kell vizsgálni az implementáció részleteit, mert annak ismeretében azonosíthatóak a határesetek. Lényegében a kódban szerepl® elágazásokat és az ott használt utasítások feltételeit kell megvizsgálni. A határesetek azonosításával egyszer¶síteni tudjuk a problémateret, mert általában a határesetek körül fordulnak el® a hibák, a határesetek között pedig feltételezhetjük, hogy homogén a komponens helyessége. Persze csak óvatosan, mert sose lehetünk ezekben teljesen biztosak, de ez egy nagyon jó irányt ad a tesztesetek deniálásához. A dinamikus automatizált fehérdobozos tesztelés során mérni szokták a kód fedettségét, ami azt mondja meg, hogy a futtatott tesztek során a kódban szerepl® utasításokból és kifejezésekb®l hány lett érintve. Sokszor ez nagyon hasznos, de néha félrevezet® tud lenni. A Visual Studio-ban van lehet®ség kód fedettség mérésére, ami a névterek, osztályok és metódusuk százalékos fedettségét mutatja, továbbá a konkrét kódban is megjelöli, hogy mely sorok és utasítások lettek érintve. Ezt használva könnyen észrevehetjük, hol vannak a kódban olyan elágazások, amikre még nem tértek ki a tesztjeink. A probléma ezzel a metrikával, hogy a fejleszt®k hajlamosak kísértésbe esni és olyan teszteseteket írni, amik csak arra szolgálnak, hogy növeljék a fedettséget. Az ilyen tesztesetek írására sok id®t lehet pazarolni, anélkül hogy bármit is javítanánk a szoftver min®ségén, ezért fontos tisztában lenni azzal, hogy a kódfedettség nem ekvivalens a rendszerünk vagy komponensünk m¶ködésének helyességével. Egy utasítás érintése néhány teszt során nem ekvivalens azzal, hogy minden esetben jól fog m¶ködni az az utasítás.
11
1.3. Tesztelés komplexitása Ha korlátlan id® és er®forrás áll a rendelkezésünkre, a szoftver tesztelés abból állna, hogy az alkalmazást kipróbáljuk a bemenetek minden lehetséges kombinációjával, és összehasonlítjuk a specikációban foglaltakkal. Ez a módszer matematikai pontossággal bizonyítaná a szoftver helyességét, de ez csak nagyon kevés esetben alkalmazható. Akár a legegyszer¶bbnek t¶n® alkalmazásnak is több ezer vagy millió különböz® bemeneti kombinációja lehet, ha pedig a bemenetek id®belisége is számít - nem beszélve a többszálú alkalmazásokról akkor a lehetséges kombinációk száma már kezelhetetlenül nagy. Ezt a problémát fogalmazza meg a
Complexity Barrier
elv (Boris Beizer, Software Test-
ing Techniques. Second edition. 1990), ami azt állítja, hogy a szoftverek és hibák komplexitása átlépi azt a határt, amit ember még képes kezelni. A tesztelési technikáink er®ssége határozza meg, hol van ez a határ, és hogy mik azok a hibák, amiket még képesek vagyunk detektálni, de tisztában kell lenni azzal, hogy nem deríthet® fel minden. Egy teszteset, ami egy komponens helyességét vizsgálja a bemenetek egy kombinációjára legyen az automatizált vagy manuális a problématérnek csak nagyon kis részét fedi le. Azt be tudjuk látni, hogy egy komponens az adott bemenetekre helyesen vagy hibásan m¶ködik, de a komponens m¶ködésének abszolút helyességét nem lehet bizonyítani dinamikus teszteléssel.
1.4. Tesztelési szintek Ha egy rendszert bármilyen el®zetes tesztelés nélkül egészében vizsgálunk, nem tudhatjuk, hogy a felbukkanó hibák honnan erednek. Ha egy kivételt kapunk egy alacsony szint¶ komponensben, vagy az alacsony szint¶ komponens hibázott, vagy már hibás bemeneteket kapott egy magasabb szint¶ komponenst®l. Ha egy magasabb szint¶ komponensben kapunk kivételt, az azért lehet, mert hibásan számolt, vagy már hibás bemeneteket kapott egy alacsony szint¶ komponenst®l. Ha a szoftver felhasználói felületén találunk hibás kimenetet, annak felel®se akármelyik komponens lehet, amelyekt®l függ az adott felület. Ha valamely komponens hibás eredményeket állít el®, az lehet, hogy a szoftver egy teljesen más részében okoz végzetes hibát, de az is lehet, hogy csak a felületen jelenik meg rossz eredmény. Persze az adott hiba jellegéb®l néha kikövetkeztethet® a hiba forrása, de egy komplex rendszernél így hibát javítani nagyon id®igényes lehet. Ennek megoldására a különböz® tesztelési szinteket érdemes elkülöníteni.
1.4.1. Egységteszt Az egységtesztelés (Unit Testing) során a tesztel® kiragadja a tesztelend® szoftver egy minimális részét, izolálja azt az alkalmazás többi részét®l és meghatározza, hogy az az elvárásoknak megfelel®en viselkedik-e. Ezt a szint¶ tesztelést a szoftverfejleszt®k végzik. Egy egységteszt tipikusan egy rövid procedúra az alkalmazás nyelvén megírva, ami inicializálja a tesztelend® objektumot és megvizsgálja, hogy a specikációnak megfelel®en viselkedik-e egy bizonyos bemenetre. Léteznek egységteszt keretrendszerek, amelyek a fejleszt®eszközbe integrálódnak, így ezeket a teszteket egyszer¶en és gyorsan ki lehet értékelni.
12
Egyszer¶ esetben a tesztelend® osztály/függvény nem függ más szoftver komponenst®l, így ha valamelyik tesztünk hibát jelez, a tesztelt komponensben van a hiba forrása. (Megesik, hogy magában az egységteszt kódjában vétettünk hibát, de általában az egységtesztek jóval egyszer¶bbek, mint a tesztelt szoftver, ezért ez ritkábban fordul el® és könnyebb észreveni.) Ha a tesztelt kódnak van függ®sége, már bonyolultabb a helyzet. Ha az
A a
A
osztályunk a
B
osztályt használja a feladatának elvégzésére, nem tudjuk az
osztály helyességét tesztelni anélkül, hogy az a
B
B
osztály helyességét®l ne függne. Ha
osztályban hiba van, minden ®t használó komponensben is hibákat fognak jelezni a
tesztjeink, egy nagy rendszernél pedig nem valószín¶, hogy a fejleszt® fejben tudja tartani az összes olyan függ®séget, ami ezt okozhatja. Hogy az egységtesztek pontosan detektálják a hiba forrását, izolálnunk kell a magasabb szint¶ komponenst a függ®ségeit®l, ezt pedig csak úgy lehet elérni, ha a magasabb szint¶ komponens függ®ségei lecserélhet®ek. Ezt például úgy lehet elérni, ha a
B
A példányt (ezt a technikát B osztályt úgy inicializáljuk,
osztály a konstruktorában vár egy
Dependency Injection -nek nevezik). hogy az A-nak csak egy teszt célú
Ekkor a tesztekben a
implementációját kapja meg, aminek viselkedése és
eredményei a tesztben lettek meghatározva. Ezzel azt érjük el, hogy az
A
és
B
osztály is
függetlenül vannak tesztelve, így ha hibát jeleznek az egységtesztek, azonosítottuk is a hiba forrását.
1.4.2. Integrációs teszt Nem minden osztályt lehet vagy célszer¶ egységtesztelni. Egy nagyon magas szint¶ komponensnek sok függ®sége lehet, és lehet, hogy aránytalanul sok munka lenne azt ténylegesen izolálni a rendszert®l. Még ha képesek is vagyunk minden komponenst egységtesztekkel vizsgálni, semmi sem garantálja, hogy azok összeintegrálva is helyesen m¶ködnének. Az integrációs teszt során a külön-külön letesztelt komponenseket együttesen teszteljük, így a komponensek közötti kommunikáció helyességér®l szerezhetünk magabiztosságot. Tipikusan ezt a tesztelést is szoftverfejleszt®k végzik, olyan teszt procedúrákat írnak, amelyekkel tipikus interakciókat szimulálnak a tesztelt komponenseken.
1.4.3. Rendszer teszt A legmagasabb szintje a tesztelésnek, amikor az egész rendszert egészében teszteljük. Regisztrálás után be lehet-e jelentkezni, bejelentkezés után lehet-e adatokat felvinni, 5-6 adat felvétele után helyesek-e a jelentések, stb... Ez általában a feketedoboz tesztelés kategóriájába esik, vagyis itt már nem foglalkozunk a rendszerünk bels® m¶ködésével, csak a funkcionális specikációban meghatározottak szerint tesztelünk.
1.5. Lazán csatolt rendszerek Egy komplex rendszer tervezése és fejlesztése során folyamatosan szem el®tt kell tartani néhány szorosan összefügg® tulajdonságot: fenntarthatóság, rugalmasság, b®víthet®ség és tesztelhet®ség. Tipikusan, ahogyan n® egy rendszer komplexitása, a legegyszer¶bb módosítások és javítások is egyre több id®t vesznek igénybe. Az egyre gyarapodó komponensek
13
közötti függ®ségek miatt nehezebb átlátni és megérteni a rendszer m¶ködését és sokszor egyszerre több komponensen kell módosítani. Emiatt nagyon könny¶ nem észrevehet® hibákat is véteni, ezeket kés®bb pedig csak komoly többletmunkával lehet felderíteni és kijavítani. Ahogy egyre több funkció van implementálva a rendszerben, a függ®ségek hálója egyre s¶r¶bb és bonyolultabb lesz, ami végül ellehetetlenít minden komolyabb változtatást. Ezekre a problémákra nyújt megoldást az, ha az alkalmazásunkat felépít® komponensek lazán vannak csatolva. A lazán csatolás mindössze annyit jelent, hogy csökkentjük a komponensek közötti függ®ségek számát, ami könnyebbé és biztonságosabbá teszi a komponensek módosítását és tesztelését, mert azok izolálva lesznek a rendszer többi részét®l. Ha egy jól tesztelt alkalmazásra van szükség nem elég az a képesség, hogy jó teszteket tudunk írni, könnyen tesztelhet® alkalmazást kell írni, amiben nem jelennek meg hibák minden apró változtatás után.
1.5.1. Objektum-orientált tervezési elvek Jól kezelhet® és könnyen tesztelhet® rendszert fejleszteni nehéz, de van néhány elv, ami iránymutatást ad. A
SOLID
bet¶szóval szoktak hivatkozni az öt tervezési elvre, amelyet
Robert C. Martin[6] fektetett le a fent említett problémák orvoslására.
S - Single responsibility principle Egy osztálynak egy, és kizárólag egy oka lehet arra, hogy megváltozzon.
Más szavakkal ez
annyit tesz, hogy egy osztálynak pontosan egy feladata kell legyen, és úgy kell deniálnunk az osztályt, hogy csak akkor kelljen megváltoztatnunk, ha maga a feladat változik meg. Példának vegyünk egy osztályt, ami adatokat gy¶jt adatbázisból egy jelentéshez, megformázza, majd kinyomtatja azt. Ennek a modulnak számos oka lehet a változtatásra: más adatokat, vagy máshonnan kell az adatokat összegy¶jteni; változtatni kell a jelentés kinézetén; esetleg nem kinyomtatni kell a jelentést, hanem email-ben elküldeni. Az elv szerint ezeknek mind külön felel®sségeknek kell lenniük, tehát külön osztályban kéne implementálni azokat. Ennek betartása azt eredményezi, hogy a kód szosztikáltabb lesz és a funkciók több osztályba lesznek szétszórva, de ez el®ny a tesztelhet®ség szempontjából. Ha ez a funkcionalitás három osztályban van megvalósítva és mind a három osztály külön-külön le van tesztelve, magabiztosak lehetünk abban, hogy egy változtatás az egyiken nem ronthatja el a többit.
O - Open/close principle Egy szoftver entitásnak (osztály, modul, függvény, stb.) nyitottnak kell lennie b®vítésre, de zártnak a módosításra. Erre egy jó példa a .NET keretrendszer StreamWriter osztálya, ami egyszer¶ adattípusok (string, int, long, stb.) szerializálását valósítja meg a megadott bináris adatfolyamra. Azt nem lehet megváltoztatni hogyan alakítja át az adattípusokat bájtokká, tehát a módosításra zárt, viszont tetsz®leges
Stream -et
alá lehet tenni, így könnyedén
újrahasználható más helyeken. Tesztelhet®ség szempontjából nem okozna gondot, ha csak a FileStream-en m¶ködne a
14
StreamWriter, viszont ha lehet®ség lenne arra, hogy leszármazzunk bel®le és megváltoztassuk például a lebeg®pontos számok bináris ábrázolását, értelmetlenné válna az osztály tesztelése. Hiába bizonyosodnánk meg a m¶ködésének helyességér®l, sose lehetnénk biztosak abban, hogy egy paraméterként kapott StreamWriter is helyesen m¶ködik. Egy szemléletesebb példa talán a WPF vagy a Silverlight
Button
vezérl®je. A keretrend-
szer nagyon rugalmas és viszonylag egyszer¶en mindent meg lehet változtatni egy gombon, amit csak el lehet képzelni. A gomb felirata helyén lehet egy kép, a színét és formáját teljesen meg lehet változtatni, animációja lehet annak is, ahogy felévisszük az egeret, viszont ezek a gomb funkcionalitását tekintve nem számítanak módosításoknak. Egyszer¶en fogalmazva a gomb az egy olyan vezérl®, amit a felhasználó valamilyen módon megnyomhat, ezt pedig nem lehet megváltoztatni, így nyugodtak lehetünk, hogy mindaz, amit egy
Button
garantál, az teljesülni fog. Ez azért kritikus, mert ha a komponensünk elemi m¶ködése módosítható, ellehetetlenül és értelmetlenné válik annak tesztelése. Egy osztály a helyességét csak akkor tudja garantálni, ha a feladatának elvégzése csak rá tartozik.
L - Liskov substitution principle Ez az elv azt mondja ki, hogy ha egy akkor a
T
S
komponens leszármazottja a
példányai lecserélhet®eknek kell lenniük
S
T
komponensek,
példányaira, anélkül hogy módosulna
bármilyen eddig is elvárt tulajdonság. Ez els®re triviálisnak t¶nhet, hiszen szinte akármelyik objektum-orientált nyelv lehet®séget ad eéle polimorzmusra, de azt semmi nem biztosítja, hogy egy rendszer egy ilyen csere után is helyesen m¶ködne. Matematikában a négyzet a téglalap egy specializáltja, így logikusnak t¶nhet ezt egy leszármazással ábrázolni. Ha a téglalap osztályból származik a négyzet osztály, akkor a négyzetnek mindenhol használhatónak kell lennie, ahol a kód téglalapot vár. A probléma, hogy a téglalappal szemben olyan elvárásaink lehetnek, amit a négyzet nem feltétlenül teljesít. Például legyen a téglalapnak módosítható szélesség és magasság tulajdonsága és egy metódusa, amivel a téglalap területét kaphatjuk meg. Mivel a leszármazottak is tartalmazni fogják ezeket, ebb®l csak úgy tudunk négyzetet csinálni, ha felülírjuk a szélesség és magasság tulajdonságokat úgy, hogy az egyik érték beállítása a másik érték beállítását is eredményezi. A helyesen m¶köd® téglalappal szemben az a logikus elvárásunk, hogy miután beállítjuk a szélességet és a magasságot két különböz® értékre, a területszámító metódusunk a két szám szorzatát adja vissza. A probléma, hogy a négyzet osztályunk ezt már nem elégíti ki, és ha kicseréljük a téglalapokat négyzet példányokra, nem lehetünk biztosak abban, hogy a rendszerünk továbbra is helyesen fog m¶ködni. Ez az elv szorosan kapcsolódik az el®z®höz. Ha meggátoljuk a leszármazottban a szélesség és magasság tulajdonság módosításának lehet®ségét, vagyis nem engedjük módosítani a téglalapunk viselkedését, biztosak lehetünk benne, hogy minden leszármazottban helyes lesz a terület számítása.
15
I - Interface segregation principle Semminek se szabad függnie olyan metódustól, amit nem használ.
Az elv az objektumok
interfészének meghízását próbálja megakadályozni, ami a komponensek tesztelhet®ségét és a rendszer változtathatóságát növeli. Tegyük fel, hogy van egy adatbázis elérésért felel®s
Database objektumunk, ami tartalmazza például a GetCategories(), GetProducts(int categoryId) és SearchProduct(string name) metódusokat még egy tucatnyi másik mellett. Ezzel két probléma lehet:
•
Database típustól függ egységteszteDatabase implementációt, hogy a tesztelt
Ha egy osztályra vagy metódusra ami a ket kell írni, biztosítanunk kell egy hamis
Database helyességét®l függjön. Az egyértelm¶, hogy egy-egy komponensnek nincs szüksége a Database minden metódusára, de az nem mindig egyentitás helyessége ne a
értelm¶, hogy pontosan melyik metódusokra van szüksége. Ha mondjuk a keresésért felel®s komponenst teszteljük, valószín¶ hogy az használja a
SearchProduct
metódust,
de anélkül hogy belenéznénk a kódba vagy kipróbálnánk, nem lehetünk biztosak benne hogy csak attól a metódustól függ. Ez nehezíti az egységtesztek megírását, mert nem mindig egyértelm¶ hogy miket kell biztosítanunk a tesztelend® komponensnek.
Database interfészét szerepek szerint: Legyen egy ICategoryRepository, egy IProductRepository és egy IProductSearcher intefészünk, amik a feladatuknak megfelel® metódusokat deklarálják, a Database pedig Ez az elv azt tanácsolja, hogy osszuk fel a
mindet implementálja. Ha a keres® nézetet megvalósító osztálynak nincs szüksége másra, csak egy metódusra, amivel szövegesen kereshet, esetleg szüksége van valahogy a kategóriák listájára is, akkor ez a szignatúráján tisztán látszani fog. Ha mondjuk csak az 1 metódussal rendelkez®
IProductSearcher
komponenst®l függ, sok-
kal könnyebben tudunk tesztcélú hamis implementációkat készíteni.
•
Az ennyire elhízott interfésszel rendelkez® komponensek a rendszer változtatásait is megnehezítik. El®fordulhat, hogy az adatbázis mérete miatt alternatív keres® technológiára kell váltani (pl. Lucene), ami azt eredményezi, hogy változtatni kell a
Database
osztályon, és meg kell változtatni egy tucatnyi komponenst, ami a keresés miatt függött a
Database -t®l.
(És általában ilyenkor buknak ki olyan problémák, amik miatt
újra kell szabni a rendszer nagy részét...) Ha szerepek szerint fel lett bontva a
base
Data-
intefésze és az új technológia implementálható a interfészek mögé, magabiztosak
lehetünk abban, hogy az új komponensek változtatás nélkül helyesen fognak m¶ködni és a meglév® egységtesztjeinket se kell újraírni.
D - Dependency inversion principle A rendszer magas szint¶ komponensei nem függhetnek az alacsony szint¶ komponensekt®l, mindkett®nek absztrakcióktól kell függjenek. Ez els®re ellentmondásosnak t¶nhet a tipikus objektumorientált szemlélettel szemben, de az újrahasználhatóságot növeli. Például legyen az alacsony szint¶ komponensünk egy
Linq2Sql
adatelérési réteg, a magas szint¶ kompo-
nensünk pedig egy nézete a mobilalkalmazásunknak. Ha a kett® között közvetlen függ®ség
16
van, és ki kell cserélni a perzisztencia technológiát, biztosan módosítanunk kell a nézet kódjában is. Egyszer¶ esetekben ez nem t¶nik nagy gondnak, de egy komplex rendszer esetén komoly következményekkel járhat. Az elv azt tanácsolja, hogy absztraháljuk el az adatelérési réteget például interfészeken keresztül, és így az alacsony és magas szint¶ komponensek is az absztrakciótól fognak függeni, és nem egymástól. Ezt úgy lehet megvalósítani, ha a magas szint¶ komponens például a konstruktorában kapja meg a szükséges implementációkat és nem az ® felel®ssége, hogy példányosítsa azokat. Ha az interfészek megfelel®en lettek deniálva és az implementáció teljesíti a Liskov behelyettesítési elvet, magabiztosan kicserélhetjük az implementációt.
17
2. fejezet
A tesztelend® alkalmazás A tesztelend® szoftvernek egy már elkészített alkalmazásomat választottam, a
ney Manager -t.
Mobile Mo-
Ez egy Windows Phone 7 és 8-as verziókat támogató alkalmazás, ami
a hétköznapi kiadásaink vagy bevételeink rögzítésére alkalmas. Az alkalmazással a pénzmozgásainkat kategóriákba sorolhatjuk, majd kés®bb kimutatásokat kérhetünk arról, hogy hogyan oszlanak el a költségeink és id®ben ezek hogyan változtak. Az alkalmazás els® verziója m¶ködik és elérhet® ingyenesen a piactéren, viszont azóta több tucatnyi új igény támadt az alkalmazással szemben, leginkább a felhasználók visszajelzései alapján. Amikor nekiálltam átgondolni, hogy pontosan milyen módosításokra is van szükség, felismertem hogy az eddig elkészült kód már nem alkalmas az új funkciók implementálására, leginkább az el®z® fejezetben taglalt okok miatt. Régen készítettem és tipikusan spagetti kód volt. Mivel már rég volt, hogy utoljára foglalkoztam vele, komoly er®feszítésekbe tellett újra megérteni a kódot és átlátni az új változtatások következményeit. Jobbnak láttam újraírni az alkalmazást, viszont arra is emlékeztem milyen fejfájást okoztak azok a hibák amikre csak az alkalmazás publikálása után derült fény. Ezért döntöttem úgy, hogy a következ® verzió elkészítése során teszteket is készítek, ami remélhet®leg visszahozza a befektetést hosszútávon. Ebben a fejezetben mutatom be röviden az alkalmazás új verziójának funkcióit és az architektúráját.
2.1. Funkcionális specikáció Terjedelmi megfontolásokból egy minden részletet tárgyaló funkcionális specikáció helyett csak egy rövid összefoglalót írok, csak olyan mélységben, ami a tesztelési feladatok megértéséhez szükséges. Az alkalmazás lehet®séget ad költségek és bevételek kategorikus rögzítésére és kés®bb a megtekintésére. Két gyökér kategória van, a költségek és bevételek kategória. Ezek alatt tetsz®leges számú és mélység¶ további kategória hozható létre szabadon választható névvel, így tehát két fában helyezhet®ek el a bejegyzések. Az alkategóriák megjelölhet®ek kiemelt kategóriának, amik így felsorolásnál a lista elején kerülnek megjelenítésre. A kategóriák kés®bb átnevezhet®ek, vagy akár törölhet®ek is, viszont ez nem jelent valódi törlést. A kategória, annak alkategóriái és mindezek bejegyzései az adatbázisban maradnak, de ezeket
18
2.1. ábra. Új bejegyzés hozzáadása
2.2. ábra. Kategória áttekint® képerny®
már csak jelentésen keresztül lehet megtekinteni. Ennek a m¶veletnek a visszavonására nincs lehet®ség. A kategóriák nem helyezhet®ek át. A kategóriákban a bejegyzések csak nullánál nagyobb összegeket tárolhatnak, opcionálisan szöveges megjegyzés is írható hozzájuk (2.1 ábra). A jóváírás dátuma alapértelmezetten az aktuális id®pont, de ez módosítható múltbeli és jöv®beli dátumra is. Az összeg és a megjegyzés kés®bb is szabadon módosítható, viszont más kategóriába nem helyezhet® át. A bejegyzések a mobiltelefon aktuális régió beállításának megfelel® valutát tárolnak. Egy kategória megtekintésénél a következ®k szerepelnek (2.2 ábra):
•
A közvetlen alkategóriák, gyelembe véve a kiemeléseket. Itt kiemelhet®ek, módosíthatóak vagy törölhet®ek a kategóriák.
•
Az összes ide és az alkategóriákba tartózó bejegyzések közül az utolsó három, ami még az aktuális id®pont el®tt szerepel. A három bejegyzésnél megjelenik az összeg, a pontos kategória és a megjegyzés.
•
Az utolsó három heti vagy havi összefoglaló. Egy összefoglaló az adott id®szak összes bejegyzésének és alkategóriák bejegyzéseinek összegét mutatja. Az, hogy milyen fajta összefoglalókat mutat a program, a beállításokban választható. Havi összefoglaló esetén az évszám és hónap neve, heti összefoglaló esetén pedig az évszám és a hét sorszáma szerepel az összeg el®tt.
19
2.3. ábra. A megadott id®szak bejegyzései
2.4. ábra. A költségek napi bontásban
Egy bejegyzés létrehozásánál lehet®ség van ismétl®d® bejegyzés deniálására, amelyek kés®bb egy külön listában érhet®ek el. Az ismétl®dés gyakorisága lehet napi, heti, havi, negyedéves vagy éves. Ekkor a jóváírás dátuma helyett az els® el®fordulás dátumát és id®pontját lehet deniálni, ami alapértelmezetten az aktuális id®pont, de lehet múltbeli és jöv®beli is. Múltbeli dátum esetén az aktuális id®pontig létrehozza az ismétl®dési szabály szerint az összes bejegyzést, de erre el®ször gyelmeztet az alkalmazás és meger®sítést kér. Egy ismétl®d® bejegyzésb®l létrejöv® bejegyzés teljesen önálló és független más bejegyzést®l, az módosítható és törölhet® bármi következmény nélkül a többi bejegyzésre nézve. Egy ismétl®d® bejegyzés esetén módosítható az ismétl®dés gyakorisága és az els® el®fordulás id®pontja is. Ekkor el®fordulhat, hogy az ismétl®dési szabály szerint rögtön létre fog hozni már esedékes bejegyzéseket, viszont ez csak a már el®z®leg létrehozott utolsó bejegyzés létrehozása utáni id®szakra vonatkozik. (Például, ha a napi ismétlés délután 4-kor esedékes, és 3-kor átállítjuk az els® el®fordulás id®pontját délután kett®re, akkor az aznapi délután két órás el®fordulás azonnal létrejön, de a tegnap vagy azel®tt esedékes létez® bejegyzések érintetlenül maradnak.) Ilyen esetben is meger®sítést kér az alkalmazás. Az ismétl®d® bejegyzéseket egy külön felületr®l lehet elérni és kezelni. Lehet®ség van jelentés készítésére egy adott kategóriáról. El®ször meg kell határozni egy intervallumot két dátum megadásával, majd a következ® információk kerülnek megjelenítésre (2.3 és 2.4 ábra):
•
Az összes ide és az alkategóriákba tartózó bejegyzések, még a törölt kategóriák bejegy-
20
zései is. A bejegyzéseknél megjelenik az összeg, az hogy pontosan melyik kategóriában lett létrehozva, és a megjegyzés. Itt a bejegyzések módosíthatóak és törölhet®ek.
•
Egy vonaldiagram ami az adott intervallumon megjeleníti az adott kategória és az összes alkategória összes bejegyzéseinek összegzését napi felbontásban.
•
Egy tortadiagram, ami a közvetlen alkategóriák adott intervallumon történ® teljes összegzés eloszlását mutatja.
Az alkalmazás nyitó képerny®je az elérési pontja a bevételek és költségek kategória fákhoz, továbbá itt is megjelenítésre kerül az utolsó három heti vagy havi összegzés, viszont ez abszolút összegzi a költségeket és bevételeket is, így az lehet negatív szám is. A bevételek pozitívnak, a költségek negatívnak számít.
2.2. Architektúra Az alkalmazás architektúrája rétegezetten lett kialakítva. Domain Driven Design[3] elveknek megfelel®en szerveztem a kódot, aminek a célja az volt, hogy az alkalmazás logikája lazán csatolva, minden technológiai részlett®l függetlenül legyen kialakítva. Alapvet®en nagyon hasonlít a tipikus 3 réteg¶ architektúrára.
2.5. ábra. A Mobile Money Manager architektúrája
Presentation
Információk megjelenítéséért és a felhasználói utasítások feldolgozásáért
felel®s. Ez jelen esetben egy Windows Phone 8 alkalmazás. Az Application réteg által deniált szolgáltatásoknak továbbítja az utasításokat, és onnan kapja meg a megjelenítésre szánt információkat. Nem tud az adatbáziskapcsolatról vagy a modell szabályairól, mindössze az adatok megjelenítésért és az utasítások továbbításáért felel.
Application
Deniálja a feladatokat amiket az alkalmazásnak meg kell tudnia oldania,
ezeknek megoldására pedig a Domain rétegben deniált modellt használja. Ez a réteg vékony, nincs állapota és nincs ismerete a modell szabályairól, mindössze koordinálja a munkát.
21
Domain
Az adatmodellt és az azon értelmezett m¶veleteket deniálásáért, valamint azok
szabályainak betartásáért felel®s.
Model
A perzisztenciafüggetlen adatmodell, aminek egyetlen felel®ssége a szabályok
és invariánsok betartása. Technikai okokból és az egyszer¶ség kedvéért nem lett teljesen függetlenítve a Linq2Sql adatbázismotortól, de annak megléte nélkül is tökéletesen kell m¶ködnie.
Repositories
A perzisztenciát valósítja meg és absztrahálja a rendszer többi részét®l
az úgynevezett Repository pattern-t használva. A különböz® repository interfészek deniálják az adatbázislekérdezés m¶veleteit, amik aztán úgy használhatók mintha csak memóriában tárolt kollekciók lennének. Nem biztosít általános lekérdezési mechanizmusokat (pl. IQueryable), minden lekérdezési logika a repository-k felel®ssége. Ez az absztrakció lehet®séget ad arra, hogy kicseréljem a perzisztencia technológiát csak ennek a rétegnek a lecserélésével.
Services
A modellen értelmezett összetett m¶veleteket deniálja. Minden olyan m¶-
velet itt van implementálva, ami általában véve a pénzmozgások vizsgálata során értelmezett.
Ez az architektúra egy ilyen egyszer¶ alkalmazásnál túlszosztikáltnak t¶nhet, viszont könnyen fenntartható, kifejezetten jól tesztelhet® és a kialakításának többletmunkája hosszútávon megtérül.
22
3. fejezet
A tesztelés megtervezése Miel®tt a fejleszt® nekilendül teszteket írni át kell gondolni milyen teszteket érdemes végezni az alkalmazáson. Az alkalmazásnak mely részei azok, amelyeket érdemes automatikus tesztekkel felbástyázni és melyek azok a részek amiket célszer¶bb manuálisan tesztelni. A tesztelési er®feszítéseknek gátat kell szabni, mert könnyen el®fordulhat hogy egy komponens vagy funkció tesztjének megírása több energiába telik mint maga a funkció elkészítése. Az is könnyen el®fordulhat, hogy a tesztelésbe több energiát fektetnek, mint ami a tesztelés nélküli rendszer utólagos javításába kellett volna. Menedzsment oldalról közelítve arra a pontra kell törekedni, ahol a várható hibajavítások költsége már kevesebb mint tesztelés költsége, pontosabban amikor a kett® összege minimumon van. Ez elméletben szé-
3.1. ábra. Tesztelés költsége és megtérülése
pen hangzik, de magabiztosan megjósolni a
potenciális hibák javításának költségét szinte lehetetlen. Ebben csak igazán érett szoftverfejlesztési folyamattal rendelkez® vállalatok tudnak jeleskedni, akik már nagyszámú statisztikával rendelkeznek az adott feladattípusról és technológiáról.
3.1. Egységtesztek A legelemibb tesztelési módszer az egységtesztelés, így el®ször azt vizsgálom meg, mi az a szint amíg érdemes ilyen teszteket írni. A következ®ket érdemes átgondolni miel®tt teszteket írunk egy osztályra (vagy komponensre):
•
Milyen komplex az osztály? Nagyon egyszer¶ osztályokkal lehet, hogy nem érdemes
23
bajlódni.
•
Milyen kritikus az osztály? Mekkora problémát jelentene egy hiba az éles környezetben? Egy pénzkiadó automatát remélhet®leg nagyon alaposan kitesztelnek.
•
Ki fogja használni a komponenst? Ha egy publikus API-ról van szó, vagy egy osztálykönyvtárról, várhatóan olyan módon fogják használni, ahogy a fejleszt® eleinte se tudta volna képzelni, és általában ekkor jönnek el® a nem várt hibák. Ez a helyzet megegyezik azzal, amikor nagy csapatban kell dolgozni, és mások az általunk készített komponenst®l függnek.
•
A követelmények mennyire véglegesek? Ha egy osztályt várhatóan meg kell majd változtatni, vagy újraírni a közeljöv®ben, talán szerencsésebb nem foglalkozni vele.
Gyakran el®forduló probléma az, amikor küszködéssé válik néhány egységteszt megírása, amikor nem egyértelm¶ hogyan lehet elemeiben tesztelni egy komponenst. Ilyenkor a nehezen megszületett teszt bonyolult és nehezen átlátható, és ez két problémát jelezhet. Az egyik, hogy olyan komponenst probálunk tesztelni, amit már egyszer¶en nem célszer¶ így tesztelni, a másik pedig, hogy nincsenek jól deniálva és elkülönítve a felel®sségek. Utóbbi esetén érdemes átgondolni az architektúrát és átszervezni a kódot, ami jobb teszteket és fenntarthatóbb kódot eredményez.
3.1.1. Domain Model A Mobile Money Manager-ben a legegyszer¶bben tesztelhet® és ugyanakkor az egyik legkritikusabb
részt,
a
Domain
modellt
szeretném egységtesztekkel lefedni. Mivel a teljes architektúrának a legalacsonyabb szintjér®l van szó, az itt fellép® hibák akármelyik felette lév® rétegben hibákhoz vezethetnek, amiket nehéz és id®igényes lehet kijavítani. Itt az egységtesztekkel arról szeretnék megbizonyosodni, hogy a modellen nem végezhet® el olyan m¶velet vagy m¶veletsor, ami inkonzisztens állapotba vinné a rendszert. A Domain modellben deniált entitá-
3.2. ábra. Modell egységtesztelése
sok és osztályok minden más komponenst®l, még a perzisztenciától is függetlenek, így egyszer¶ és könnyen értelmezhet® teszteket lehet írni rájuk. A modell osztályai között vannak függ®ségek, viszont úgy éreztem nem érdemes itt izolálni ®ket egymástól. Persze így nem is hívhatjuk ezeket a teszteket igazi egységteszteknek, de a modellben deniált entitások nem függnek egymás viselkedését®l, csak egymás állapotától. Konkrétan a
Category 24
entitás az
Entry
entitásnak csak a getter
metódusait használja, ha interfészekkel hamis implementációkat használnék a tesztekhez, csak a kódot és a teszteket bonyolítanám, igazi haszna nem lenne.
3.1.2. Domain Repositories Az architektúrában a következ® szinten a
Repository
réteg van, ami a lekérdezések
deniálásáért és a perzisztencia megvalósításáért felel®s. A perzisztenciát a
Linq2Sql
valósítja meg, amir®l feltételezhetjük, hogy helyesen m¶ködik, vagyis csak az a kérdés, hogy helyesen van-e használva. Minden egyes lekérdezés (pl.
GetRecentEntries )
egységtesztjében létre kéne hozni az adatbázist, feltölteni azt nyers adatokkal, majd ellen®rizni, hogy helyesek-e a lekérdezések eredményei.
3.1.3. Domain Services
3.3. ábra. Repository réteg egységtesztelése
A Domain rétegben szerepl® szolgáltatások deniálnak összetett m¶veleteket a Domain modellen, így ezt is kritikusnak érzem alaposan kitesztelni. A probléma ezzel a komponenssel, hogy valamivel már nehezebb letesztelni mint a modellt, mivel függ a modellt®l és a
Repository
komponens-
t®l is. Az eéle függ®ség nem szerencsés, mert így az itt megírt egységtesztek lefutásának sikeressége attól is függ, hogy az alatta lév® komponensek (konkrétan az adatbázis) megfelel®en m¶ködik-e. Az ott fellép® meghibásodások dominó szer¶en terjednének végig az egységtesztjeinken.
3.4. ábra. Domain szolgáltatások egységtesztelése
Ennek a függ®ségnek a feloldása jól megtervezett rendszer esetén nem okozhat nagy problémát, itt a lazán csatolt
Repositories
komponens jól deniált interfészekkel el van abszrahálva, így könnyedén alkalmazhatóak
Mock
és
Fake
objektumok. A
Domain
szolgáltatásoknak ugyanilyen függ®sége van a
modellt®l, viszont az nincs elabszrahálva interfészekkel, így a feldoldása nem lehetséges. A modellben deniált osztályok olyan egyszer¶ek, hogy az elabszrahálásával járó komplexitás növekedés nehezebben átlátható rendszert eredményezne és nem adna el®nyt tesztelhet®ség szempontjából.
25
3.1.4. Application és Presentation réteg Az Application réteget úgy ítélem már nem lenne szerencsés egységtesztekkel lefedni, mert a feladatok koordinálásán (vagyis néhány függvény egymás utáni meghívásán) kívül nincs sokkal több felel®ssége, ennek leteszteléséhez pedig sok függ®séget kéne feloldani, ami hosszú és bonyolult egységteszteket eredményezne. A
Presentation
réteget tipikusan nehéz, vagy lehetetlen lenne egységtesztekkel vizsgálni.
A függ®ség az alkalmazás rétegt®l itt is feloldható lenne, alkalmazhatóak lennének a és
Fake
Mock
objektumok, viszont nincs egyszer¶ és megbízható módja annak, hogy egységtesz-
tekkel bizonyosodjunk meg az információk helyes megjelenítésér®l.
3.2. Integrációs tesztek Integrációs tesztekkel a már önállóan letesztelt komponensek együttm¶ködésének helyességét kell vizsgálni. Egységtesztekkel le lett már fedve a Domain modell, a
pository
Re-
és a szolgáltatás réteg, így tudjuk,
hogy azok különállóan helyesen m¶ködnek. Ennek a három komponensnek az integrációs tesztjei megegyeznének a
Domain
szol-
gáltatások egységtesztjeivel, mindössze a függ®ség feloldásában lenne különbség, így az ilyen integrációs teszt redundánsnak t¶nik. A következ® szint ami még nincs tesztelve az az alkalmazás réteg, ami egy nagyon
3.5. ábra. Integrációs tesztek
vékony réteg. A feladatok koordinálásán és válasz objektumok el®állításán kívül nincs más felel®ssége. Elabsztrahálni a függ®ségeket, és egységteszteket írni az alkalmazás szolgáltatásaira nagyobb munka lenne mint magukat a szolgáltatásokat megírni, tehát az integrációs tesztjeim err®l a szintr®l fognak történni. Ezzel megbizonyosodom az alkalmazás és
Domain
réteg együttm¶ködésének helyességér®l.
3.3. Teljesítmény tesztek Az adatbázisok elérési ideje elkerülhetetlenül megn®, ahogy növekszik az adatmennyiség, ennek mértékét pedig célszer¶ megvizsgálni. Egy reális terhelésmodell például, ha egy felhasználó minden nap felvisz 20 tételt 3 éven át. Ez 21.900 bejegyzést jelent, vagyis a többtízezres nagyságrendben kéne megvizsgálni a teljesítményt. Fontos, hogy az alkalmazás alapfunkciói ilyen adatmennyiségnél is elfogadható sebességgel m¶ködjenek: tétel rögzítése, utolsó 3 tétel megtekintése, adott hónap/hét tételeinek megtekintése. Ha a kérdéses terhelésnél megfelel®en m¶ködik az alkalmazás, azzal is jó tisztában lenni, hogy mi az az adatmennyiség, ami már problémát okoz.
26
A teljesítmény vizsgálata az integrációs tesztekhez hasonló annyiban, hogy nem a grakus felületen, hanem közvetlenül az alkalmazás réteg szolgáltatásain keresztül kell tesztelni az alkalmazást, a különbség hogy nem a helyességet, hanem a m¶veletek elvégzéséhez szükséges id®t kell mérni.
3.4. Manuális tesztek Bonyolult alkalmazásoknál célszer¶ lehet tesztforgatókönyveket deniálni, ott ahol nehéz fejben tartani és átlátni minden lényeges tesztesetet, de ez az alkalmazás messze van ett®l a komplexitástól. Egyrészt ad-hoc jelleg¶ manuális tesztelést végzek a szoftveren, másrészt alfa tesztelést, vagyis a saját telefonon használva vizsgálom a napi költségeim rögzítésével, hogy helyesen m¶ködik-e.
27
4. fejezet
Technológiai megoldások A .NET keretrendszerben fejlett tesztelési megoldások állnak rendelkezésünkre, de a Windowws Phone operációs rendszeren ezek csak korlátozottan érhet®ek el. A tervezett egység és integrációs tesztek megvalósítására is egy egységteszt keretrendszer használatára van szükség, ami a megírt egységtesztek futtatását és kiértékelését végzi. Egy egységteszt technikailag egy rövid procedúra, ami lefuttatja a tesztelend® komponenst bizonyos bemeneti paraméterekkel és összehasonlítja a futás eredményét a specikációban meghatározottakkal. Relatív egyszer¶en meg lehet írni egy önálló alkalmazást erre a célra, de erre a feladatra már léteznek keretrendszerek. Sajnos a telefonos operációs rendszeren elérhet® egységteszt keretrendszerek kevesebb funkciót nyújtanak, a fejezetben az ebb®l adódó problémákat vizsgálom meg.
4.1. Windows alkalmazás egységtesztelése .NET környezetben lehet®ség van arra, hogy a tesztjeinket (konkrétan függvényeket) megjelöljük attribútumokkal, amiket egy küls® tesztfuttató alkalmazás automatikusan fel tud deríteni és le tud futtatni. Ilyen teszt keretrendszer például a Visual Studio-ba beépített
MSTest,
de ezen kívül még elterjedt alternatívák például az open-source
NUnit
és
xUnit
keretrendszerek is, amik ugyancsak be tudnak épülni a fejleszt®környezetbe. Mindnek megvan az el®nye és hátránya, de nagyvonalakban ugyan azt tudják: attribútumokat adnak a tesztek megjelölésére, és metódusokat az eredmények ellen®rzésére. A 4.1. ábrán két példa teszt látható az
MSTest
használatával, az ott látható osztályok rövid magyarázata a
következ® listában olvasható:
TestClassAttribute
Azt jelzi, hogy az osztály teszteket tartalmaz.
TestMethodAttribute
Egy futtatandó tesztet jelöl.
ExpectedExceptionAttribute
A teszt csak a megadott típusú
Exception
fellépése ese-
tén sikeres.
Assert
Segédfüggvényeket deniál, amivel a tesztelt komponens eredményeit lehet össze-
hasonlítani az elvártakkal szemben. Az egységteszt sikertelennek min®sül, ha egy ilyen ellen®rzés nem teljesül.
28
[ TestClass ] public class CategoryTests { [ TestMethod ] [ ExpectedException ( typeof ( DomainException ))] public void ConstructorWithEmptyNameThrowDomainException () { Category category = new Category ( CategoryType . Income , ""); } [ TestMethod ] public void ConstructorWithNotEmptyName () { Category category = new Category ( CategoryType . Income , " Test ");
}
}
Assert . AreEqual ( CategoryType . Income , category . CategoryType ); Assert . AreEqual (" Test ", category . Name );
4.1. ábra. Két egységteszt implementációja az MSTest használatával.
Egy Windows alkalmazás esetén az egységteszteket egy külön Windows osztálykönyvtárban szokták deniálni, ami a választott keretrendszert használva implementálja a teszteket. A tesztfuttató Windows alkalmazás (ami általában a Visual Studio-val integrált) betölti a memóriába a teszteket tartalmazó szerelvényeket, felderíti
Reection -nel
a teszteket, és
egy áttekint® felületet ad err®l (4.2).
4.2. ábra. A Visual Studio 2013-ba beépített teszt futtató ablak
Err®l a felületr®l elindítható a háttérben a tesztelés és végül itt jelennek meg az egységtesztek eredményei. Arra is lehet®ség van, hogy a kódfedettség vizsgálatát kérjünk a keretrendszert®l, az pedig ad egy összefoglaló nézetet (4.3), de akár a kódban is megtekinthet®, hogy mely utasításokat érintették a tesztjeink (4.4).
4.3. ábra. Az MSTest kódfedettség összefogglalója
29
4.4. ábra. A kódfedettség utasításonként is vizualizálható
4.2. Windows Phone alkalmazás egységtesztelése Ha egy Windows alkalmazást szeretnénk egységtesztelni, vannak Windows-on m¶köd® tesztfuttató alkalmazások, amik különösebb probléma nélkül le tudják futtatni a tesztjeinket. Ehhez hasonlóan, ha egy Windows Phone alkalmazást szeretnénk tesztelni, egy Windows Phone-on elindítható tesztfuttató alkalmazásra van szükség. A mobil platformon viszont az alkalmazások egymástól csak izoláltan futtathatók, tehát nekünk kell egy csomagban, egy alkalmazáson belül feltölteni a tesztelend® szoftvert és a tesztfuttató keretrendszert is. Így, hogy ezek az egységtesztek csak emulátoron vagy mobiltelefonon futtathatóak, valamilyen szinten integrációs teszteknek min®sülnek, mert a tesztelend® alkalmazást nem a fejleszt®gépen, hanem a célkörnyezetben teszteljük. Ugyanakkor ez ugyan úgy igaz a Windows-on futtatott egységtesztekre is: ha Visual Studio-ban lefuttatunk egy egységtesztet, az függ az aktuális .NET verziótól és Windows operációs rendszert®l is, de ezt gyelmen kívül szokták hagyni. A
Windows Phone SDK -val feltelepül a Windows Phone Unit Test App
projekt sablon
is, ami erre ad megoldást. A sablonnal egy önállóan m¶köd® Windows Phone alkalmazás hozható létre, ami lényegében egy
MSTest
teszt futtató alkalmazás. A 4.5. ábrán az látha-
tó, ahogyan az alkalmazás a f®képerny® megjelenítésével elindítja a háttérben az
30
MSTest
Windows Phone verzióját.
public MainPage () { this . InitializeComponent ();
}
TestExecutorServiceWrapper wrapper = new TestExecutorServiceWrapper (); ServiceMain testService = new ServiceMain ( (id , args ) = > wrapper . SendMessage (( ContractName )id , args )); Thread thread = new Thread ( testService . Run ); thread . Start ();
4.5. ábra. A tesztfuttató rutin indítása a Windows Phone alkalmazás f®oldalának konstruktorában.
Ezzel az alkalmazás egy olyan szolgáltatást indít el, amire a Visual Studio csatlakozni tud, így a tesztek futtatása továbbra is a fejleszt®környezetbe integrálódhat. Magukat a teszteket ebben a tesztfuttató alkalmazásban kell megírni, ugyan úgy, mint Windows platform esetén. A
TestMethod
attribútum és
Assert
osztályok ugyan úgy használhatóak
és az így megjelölt tesztek meg is jelennek a Visual Studio
Test Explorer
ablakában. Az
így felderített tesztek lefuttathatóak, aminek hatására, ha még nem történt meg, elindul az emulátor és feltelepül a teszt alkalmazás. A tesztjeink megírásához hivatkoznunk kell a tesztelend® alkalmazás szerelvényére, így a tesztfuttató alkalmazással együtt az is feltelepül az emulátorra. A Visual Studio elindítja az alkalmazást, csatlakozik hozzá, levezényli a tesztek végrehajtását, megjeleníti az eredményeket, végül bezárja az alkalmazást. Windows Phone alkalmazás fejlesztése közben valószín¶leg már amúgy is fut az emulátor, vagyis kényelmesen használható ez a megoldás is, viszont számos hátránya van:
•
A tesztek írását támogató funkciók nem mindegyike érhet® el Windows Phone platformon. Amivel a fejlesztés során szembesültem, az az
ExpectedException
attribútum
hiánya.
•
Kivétel esetén a Visual Studio nem tudja megjeleníteni a hiba pontos helyét. Windows platformon, ha nem várt hiba miatt megszakad az egyik teszt futása, a teszt eredményeir®l egyenesen el lehet navigálni a kódnak azon sorára, ahol a kivétel el lett dobva. A Windows Phone teszteknél is megtekinthet® a stack trace, vagyis látható melyik függvényb®l jött a kívétel, de a forráskódban már nem tudja megjelölni, hogy melyik sorról van szó.
•
Nem tud kódfedést mérni. Kódfedés méréséhez szükség van a .NET keretrendszer bizonyos bels® szolgáltatásaira, viszont a Windows Phone operációs rendszere túlságosan zárt ahhoz, hogy a .NET egységteszt alkalmazás használni tudja azokat és vissza tudja küldeni a fejleszt®környezetnek.
•
Az el®z®höz okokhoz hasonlóan a
Prole test
funkció se érhet® el Windows Phone-on,
ami például a lassan lefutó tesztek kielemzésére adna lehet®séget. Ezek els®re komoly hiányosságoknak t¶nhetnek, de ezek nélkül is alaposan ki lehet tesztelni egy szoftvert.
31
4.3. Windows Phone alkalmazás egységtesztelése Windows platformon A .NET keretrendszer a Java-hoz hasonlóan egy virtuális gépnek tekinthet®. Amíg egy
C++
fordító bináris kimenete csak a megadott processzorarchitektúrán és operációs rend-
szeren m¶ködik, a .NET nyelvek fordítói egy úgynevezett kódot állítanak el®, amiket kód egy
assembly -hez
EXE
vagy
DLL
IL
(Intermediate Language)
kiterjesztés¶ fájlokba csomagolnak. Ez az
IL
hasonló alacsony szint¶, de platform független programozási nyelv,
amit a .NET keretrendszer futási id®ben fordít platform specikus processzor utasításokra. A .NET a Java-hoz és sok más
script
nyelvhez hasonlóan így tud platform független len-
ni. Ezt a platformfüggetlenséget kihasználva lehet elérni, hogy a mobilalkalmazás tesztjeit emulátoron és eszközön kívül lehessen futtatni. Egy Windows Phone alkalmazás is egy .NET szerelvény, vagyis ugyan olyan DLL fordul bel®le, ugyan olyan
IL
kóddal, mint egy Windows osztálykönyvtár esetén. A probléma,
hogy a mobiltelefonon nem ugyan az a .NET keretrendszer fut, mint a Windows-on. A Windows Phone platform a
Silverlight
keretrendszeren alapszik, ami csak egy csökkentett
BCL-lel1 rendelkezik. Az alap típusok, mint pl. az
Int32, DateTime, String, List ugyan
úgy elérhet®ek, de a platform igényeinek megfelel®en bizonyos részei mint pl. a fájlkezelés hiányoznak a rendszerb®l. Ezek az alap osztályok az
mscorlib
és a
System
szerelvényekben vannak deniálva mind-
két platform esetén, de egy Windos Phone és egy Windows alkalmazás az alap osztálykönyvtárak különböz® verzióira hivatkoznak. A Microsoft mérnökei szerencsére úgy oldották meg ezt, hogy a Windows Phone ezen szerelvényeib®l csak kivettek bizonyos osztályokat és metódusokat, de a benne hagyott funkciók interfészén és m¶ködésén nem változtattak. Felfogható ez úgy is, hogy a Windows Phone-on elérhet® elérhet®
BCL
BCL interfészét kielégíti a Windows-on
is. Ennek köszönhet®, hogy egy Windows alkalmazás képes a memóriájába
tölteni és futtatni egy Windows Phone szerelvényt is. Legyen a tesztelend® osztály, és az egységtesztet implementáló osztály a következ®: A tesztelend® osztály egy Windows Phone alkalmazásban van deniálva, a tesztet tartalmazó osztály pedig egy Windows osztálykönyvtárban, ami hivatkozik a Windows Phone alkalmazás szerelvényére. Fordítás után a teszt megjelenik a Visual Studio Test Explorer ablakában. Amikor a Windows-on m¶köd® tesztfuttató alkalmazás megpróbálja lefuttatni a tesztet, a .NET keretrendszer el®ször betölti a
BCL
Windows verzióját, majd futásid®-
ben észreveszi, hogy a kód egy Windows Phone szerelvényre is hivatkozik. Ekkor a .NET megpróbálja betölteni azokat a függ®ségeket is, amik annak a futtatáshoz szükségesek, de már nincs szükség a
BCL Windows Phone verziójának betöltésére, mert a memóriában már
egy olyan verzió van, ami kielégíti annak interfészét. Így a Windows Phone alkalmazást gond nélkül lehet emulátor nélkül tesztelni. A tesztelés ott válik problémássá, amikor a kódunk platform specikus osztálykönyvtárakat is használ, mint például a
Linq2Sql
Windows Phone portját. Windows-on elérhet®
ORM-ekkel, pl. Entity Framework-kel vagy NHibernate-tel a kód túlbonyolítása nélkül is megoldható lenne egy platform független
1
Domain
Base Class Library rövidítése.
32
modell, de mobil környezetben erre nem
// Windows Phone assembly public class Calculator { public int Add ( int x , int y) { return x + y; } } // Windows assembly , referencing the Windows Phone assembly [ TestClass ] public class CalculatorTests { [ TestMethod ] public void AddTest () { int x = 3; int y = 4; Calculator calc = new Calculator (); int actual = calc . Add (x , y );
}
}
Assert . AreEqual (7 , actual );
volt egyszer¶ mód. Például a
Category
entitásom egy lecsupaszított implementációja a
következ®:
[ Table ( Name = " Categories ")] public class Category { private readonly EntitySet < Entry > childEntries = new EntitySet < Entry >(); [ Column ( IsPrimaryKey = true , IsDbGenerated = true )] public int Id { get ; private set ; } [ Column ] public string Name { get ; set ; }
}
[ Association ( ThisKey = " Id ", OtherKey = " ParentCategoryId ", Storage = " childEntries ")] public EntitySet < Entry > ChildEntries { get { return this . childEntries ; } }
Egyrészt használnom kell a
Table, Column
és
Association
ható az adatbázis struktúrája, másrészt használnom kell a
attribútumokat, amikkel leír-
Linq2Sql EntitySet -jét
amivel
a bejegyzések listáját kezelem. Ha a teszt projektb®l hivatkozok erre a kódra és példányosítom a
Category
osztályt, a kód lefordul és a teszt elindítható, de a futása megszakad a
példányosításnál. A .NET a kód függ®ségeit futásid®ben próbálja feloldani és elszáll, mert
Linq2Sql verziót. Amíg Windows-on a Linq2Sql mindössze egy ORM, addig Windows Phone-on egy teljes
nem találja a megfelel®
adatbáziskezel®t is implementálnia kell, ami ráadásul nem hálózati adatkapcsolaton keresztül kommunikál, hanem közvetlenül a Windows Phone
33
Isolated Storage tárolójába dolgozik.
Ha az egységteszt projektb®l egy olyan osztályt próbálunk példányosítani, aminek a publikus interfészén a
Linq2Sql
osztálykönyvtár osztályai szerepelnek, akkor az egységteszt
projektnek is hivatkoznia kell a
Linq2Sql
megfelel® verziójára. Erre gyelmeztet is a .NET
miközben megáll a tesztek futtatása. Ha megvizsgáljuk Visual Studio-ban egy Windows Phone alkalmazás hivatkozott sze-
c:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\WindowsPhone\v8.0\ mappában találhatóak. Itt meg is található a Linq2Sql -hez szükséges System.Data.Linq szerelvény megfelel® verziója. Ha a relvényeit, az látható, hogy az osztálykönyvtárak a
Visual Studio-t használva beállítjuk ezt a hivatkozást a kód lefordul és a teszt elindítható, de a futás
InvalidProgramException
hibával megszakad. Az MSDN-en található dokumen-
táció szerint ez a kivétel hibás IL kódra vagy a szerelvény hibás meta adataira utal, tehát egy helyesen lefordított .NET szerelvénynek a célplatformok különbsége esetén se kéne produkálnia ilyet. Ha megvizsgáljuk a kérdéses szerelvényt valamilyen disassembler-rel (pl. ILSpy vagy Reector), az látható, hogy a hivatkozott szerelvény nem tartalmaz implementációt, csak a publikus osztályok publikus tagjainak deklarációit. Ez magyarázza, hogy miért fordul le a kód és miért száll el aztán. Egy keresés után megtalálható a tényleges
c:\Program Files (x86)\Microsoft SDKs\Windows Phone\v8.0\Tools\MDILXAPCompile\Framework\ mappában, amit ha behivatkozunk, a tesztek a Category entitásra már lefutnak. A tesztfuttató alkalmazás betölti a memóriájába a Linq2Sql szerelvényt, és mivel a használt attribútumok csak a BCL osztályaira hivatkoznak, gond nélkül lefordulnak és példányosodnak. Ezzel a megoldással a Domain rétegben tesztelhet® a modell, de a Repository implementációja sajnos már nem. A teszt projektben a Linq2Sql -lel deniált DataContext használata további függ®ségek láncolatát okozza (Microsoft.Phone.Data.Internal, Microsoft.Phone.Interop, Microsoft.Phone ), amit már hiába oldunk meg az el®z® logika szerint, implementációt tartalmazó osztálykönyvtár (amit maga az emulátor használ) a
olyan operációs rendszer specikus funkciókat (pl. Isolated Storage) próbál használni a tesztünk, ami nyilván csak emulátoron vagy az eszközön érhet® el. A függ®ségek egy egyszer¶sített áttekint® vázlata a 4.6 ábrán látható.
MSTest.exe <<Windows>>
MoneyManager.Tests.dll <<Windows>>
MoneyManager.dll <<Windows Phone>>
System.dll <<Windows>>
System.Data.Linq.dll <<Windows Phone>>
Microsoft.Phone.dll <<Windows Phone>>
System.dll <<Windows Phone>>
4.6. ábra. A különböz® szerelvények közötti függ®ségek
34
5. fejezet
Tesztek implementálása A következ® fejezetben azt mutatom be milyen módon sikerült megvalósítani a tervezett teszteket Windows Phone platformon.
5.1. Domain Model A modell réteg egy szoftverben a világ egy egyszer¶sített reprezentálásáért felel®s, és fontos, hogy ez a reprezentáció mindig olyan állapotban legyen, ami értelmezhet® a szoftver számára. Könnyen el®fordulhat, hogy a modell olyan állapotban kerül mentésre, ami kés®bb hibákat okozhat, akár olyanokat is, amit csak a szoftver újratelepítésével lehet megoldani. Els®sorban ezért szeretnék megbizonyosodni arról, hogy a modellen nem végezhet® el olyan m¶velet, ami inkonzisztens állapotba vezethet.
MoneyManager.Client.Domain
Windows Phone osztálykönyvtárban
van implementálva, az ehhez tartozó teszteket pedig a
MoneyManager.Client.Domain.Tests
A Domain réteg a
projektben implementáltam. Ez a teszt projekt egy Windows osztálykönyvtár, ami hivatkozik az
MSTest
egységteszt keretrendszerre, a Domain réteget implementáló Windows
Phone osztálykönyvtárra, és a 4. fejezetben leírtaknak megfelel®en a és
Microsoft.Phone.Data.Internal.dll
System.Data.Linq.dll
Windows Phone szerelvényekre. Ezzel a kicsit kö-
rülményes beállítással tudtam elérni azt, hogy az
MSTest
Windows verziójával lehessen
tesztelni a Domain réteget.
5.1.1. Entry A tesztek implementálását el®ször a modell egyik legegyszer¶bb, de központi entitásán keresztül mutatom be, aminek interfésze a 5.1 ábrán látható. Ezt az objektumot az egyzer¶ tesztelhet®ség és használhatóság érdekében immutábilisra terveztem, tehát a már létrehozott
Entry
példányok nem módosíthatóak. Ezt sajnos a
maradéktalanul megvalósítani, mert a
ParentCategory -nak
Linq2Sql
miatt nem lehetett
mindenképp írhatónak kellett
lennie, de mivel az internal láthatóságú, az objektumok használói számára immutábilisnek t¶nnek az objektumok. Tesztek deniálása el®tt elengedhetetlen, hogy maradéktalanul tisztában legyünk a tesztelt szoftverkomponens felel®sségével és feladatával. Az
35
Entry
egy bizonyos id®pillanatban
public class Entry { public Entry ( decimal amount , DateTime date , string comment = "") { } public int Id { get ; private set ; } public decimal Amount { get ; private set ; } public DateTime DateTime { get ; private set ; } public string Comment { get ; private set ; }
}
public int ParentCategoryId { get ; private set ; } public Category ParentCategory { get ; internal set ; }
5.1. ábra. Az Entry osztály lecsupaszított váza.
megtörtént pénzügyi tranzakciót reprezentál. A tranzakció irányáról nem tud, az határozza meg, hogy bevétel vagy kiadás, hogy melyik kategóriában lett elhelyezve. Habár tud az osztály arról, hogy melyik kategóriában lett elhelyezve, ezért a relációért a
Category
osztály
felel®s és csak technikai okok miatt kell szerepelnie az osztályban. Az osztály közvetlenül csak azért felel, hogy a tárolt pénzösszeg, dátum és megjegyzés értelmezhet® legyen, ezekre pedig csak 2 szabály vonatkozik:
•
A mennyiség csak pozitív nem nulla szám lehet.
•
A megjegyzés lehet üres
String, de nem lehet null.
Ezek alapján már deniálhatóak is a szükséges egységtesztek: meg kell vizsgálni, hogy a hibás bemenetekre megfelel® kivételeket dob-e az osztály, és hogy helyes bemenetekre helyesen m¶ködik-e. Ezen a ponton két dolgot kellett átgondolnom: a kivételkezelési stratégiát és az egységtesztek elnevezési konvencióját. Két ilyen szabály megsértése nem feltétlenül, de ebben az esetben két különböz® jelleg¶ hibára utal. Az a szabály például, hogy szabad-e nulla érték¶ tranzakciót végezni a rendszerben, a modell felel®ssége, és a felette lév® rétegek nem feltétlenül tudnak err®l a szabályról. (Ez akár kés®bb változhat is.) Lehet, hogy a prezentációs rétegben kitüntetett szerepet kap ez a szabály, esetleg ott nincs is lehet®ség negatív számot vagy nullát bevinni, de ha a modell ilyen jelleg¶ hibás paramétert kap, az felhasználói hibára utal. Az viszont, ha
null
érték¶ megjegyzést kap az
osztály, kizárólag programozói hiba lehet, ezért ezt érdemes megkülönböztetni az el®z®t®l. A felhasználónak nem célszer¶ bels® m¶ködésre utaló hibaüzeneteket megjeleníteni, de egy hibás bemenetr®l fontos tájékoztatni a felhasználót. Ezen megfontolásokból lett bevezetve a rendszerbe a
DomainExcpetion kivétel, aminek a Message tulajdonsága a felhaszná-
ló számára megjeleníthet® hibaüzenetet tartalmaz. Hibás mennyiség esetén erre a kivételre
null érték¶ megjegyzés esetén a .NET alapkönyvtáraiban deniált ArgumentNullExcpeption eldobására kell várni. (A kivételkezelési stratégia meghatározása kell számítani, míg
nem a tesztel® felel®ssége, viszont elengedhetetlen, hogy tisztában legyen vele.) Az elnevezési konvenció egy kevésbé kritikus kérdés, de célszer¶ az elején lefektetni és konzisztensen betartani, hogy sok teszt bevezetése után is áttekinthet® maradjon az egységtesztek listája. Általában véve egy egységteszt akkor lett jól elnevezve, ha a név alapján egyértelm¶en leolvasható a tesztelt komponens, a vizsgált állapot és az elvárt viselkedés.
36
Általában az összes egy bizonyos osztályhoz tartozó tesztet egy ahhoz tartozó tesztosztályban implementálják, így fölösleges a névbe foglalni azt. Ezek alapján egy egységteszt nevének logikai felépítése a következ® kell legyen:
dés.
Metódus+Állapot+Elvárt viselke-
Amikor elkezdtem az egységteszteket megírni, azt próbáltam megfogalmazni, hogy
az adott egységteszt milyen elvárást fogalmaz meg a rendszerrel szemben, ami azt eredményezte, hogy az egységtesztjeim listája egy specikációhoz hasonlít. Az
Entry
osztályra 4
tesztet deniáltam a következ® nevekkel:
• ConstructorWithNegativeAmountShouldThrowDomainException gatív mennyiség esetén
- A konstruktor ne-
DomainException -t dob.
• ConstructorWithZeroAmountShouldThrowDomainException - A konstruktor nulla érték esetén
DomainException -t dob.
• ConstructorWithWithNullCommentShouldThrowArgumentNullException ruktor
null
megjegyzés esetén
ArgumentNullException -t dob.
• ConstructorWithPositiveAmountAndNotNullCommentShouldWork pozitív mennyiség és nem
null
- A konst-
- A konstruktor
megjegyzés esetén sikeresen lefut.
Mivel ez egy immutábilis osztály, elég volt csak a konstruktorra teszteket írni. A 5.2 kódrészletben egy hibát és egy helyességet ellenörz® teszt kódja látható. Az els® tesztnél az
ExpectedException
attribútum jelenléte azt jelzi az
MSTest
keretrendszernek, hogy a
teszt csak akkor sikeres, ha a megadott típusú kivétel dobódik a teszt metódusból. Az ott szerepl® teszt metódusnak innent®l kezdve nem kell másból állnia, csak az osztály hibás példányosításából. A második teszt már kicsit eröltetettnek t¶nhet. Triviálisnak t¶nhet, hogy a konstruktornak átadott paraméterek eltárolódnak a megfelel® tulajdonságokba, de tesztek írásánál fontos, hogy ne csak a kivételekre koncentráljunk, hanem a helyes m¶ködés bemutatására is.
[ TestMethod ] [ ExpectedException ( typeof ( DomainException ))] public void ConstructorWithNegativeAmountShouldThrowDomainException () { Entry entry = new Entry ( -10.0 m , DateTime . Now ); } [ TestMethod ] public void ConstructorWithPositiveAmountAndNotNullCommentShouldWork () { decimal amount = 10.0 m; DateTime dateTime = DateTime . Now ;
}
Entry entry = new Entry ( amount , dateTime , " test "); Assert . AreEqual ( amount , entry . Amount ); Assert . AreEqual ( dateTime , entry . DateTime ); Assert . AreEqual (" test ", entry . Comment );
5.2. ábra. Egy kivételkezelést és egy helyességet vizsgáló teszt az Entry osztály konstruktorára.
Ez az osztály a rendszerben a legegyszer¶bbek közé tartozik, a következ®kben az ilyen jelleg¶ egyszer¶ tesztekre már nem térek ki.
37
5.1.2. Category Egy tranzakció-kategória reprezentálásáért felel®s, ami lehet bevételi, kiadási vagy gyökér kategória. Publikus interfésze a 5.3 ábrán látható. Az
Entry
entitáshoz képest ez az osz-
tály nem csak a saját tulajdonságainak helyességéért felel, hanem a kategóriákhoz tartozó entitásokkal fenntartott relációk menedzseléséért is. Konkrétan egy
Entry
objektum ön-
magában értelmezhet®, viszont egy kategória a tulajdonságain túl a hozzá rendelt
Entry
példányok halmazából is áll. Ezek a viszonyok a F.1.1 ábrán láthatóak.
public class Category { public Category ( CategoryType categoryType , string name ) { } public int Id { get ; private set ; }
}
public public public public
string Name { get ; set ; } CategoryType CategoryType { get ; private set ; } bool IsDeleted { get ; set ; } bool IsPinned { get ; set ; }
public public public public
EntitySet < Entry > ChildEntries { get ; } EntitySet < Category > ChildCategories { get ; } EntitySet < Summary > ChildSummaries { get ; } EntitySet < RecurrentEntry > ChildRecurrentEntries { get ; }
public int ? ParentCategoryId { get ; private set ; } public Category ParentCategory { get ; private set ; }
5.3. ábra. Az Category osztály lecsupaszított váza.
ChildEntries tulajdonságán keresztül az Add metódussal lehetséges. Az EntitySet típus egy Linq2Sql -ben deniált kollekció, amibe példányosításkor injektálhatóak a validálási szabályok, így a Category osztály garantálni tudja, hogy csak helyes m¶veletek végezhet®ek el rajta. Egy kategóriához egy bejegyzés hozzárendelése például a kategória
Ez az osztály már lényegesen több szabály betartásáért felel, de fölöslegesen hosszú lenne maradéktalanul megtárgyalni azokat. A kategória és bejegyzés entitások viszonyára két szabály vonatkozik, ezekre a következ® elnevezés¶ teszteket készítettem el:
•
•
Gyökér kategória nem tartalmazhat bejegyzést.
ChildEntriesAddOnRootShouldThrowInvalidOperationException
ChildEntriesAddOnIncomeShouldWork
ChildEntriesAddOnExpenseShouldWork
Gyerek bejegyzés hozzáadása után a bejegyzés
ParentCategory
tulajdonsága az adott
kategóriára kell mutasson.
ChildEntriesAddShouldSetBidirection
ChildEntriesRemoveShouldClearBidirection
Mivel ezek a tesztek már nem implementálhatóak egyszer¶en 1-2 sorban, gyelni kellett az átláthatóságra. Az egységtesztek tipikusan 3 részb®l állnak: a tesztelend® és egyéb
38
szükséges objektumok el®készítése, a vizsgált utasítások végrehajtása, végül az eredmények kiértékelése. Bonyolult egységteszteknél osszemosódhatnak ezek a részek, esetleg még ismétl®dhetnek is, és nem mindig egyértelm¶, hogy mi is az éppen tesztelt utasítás a kódban. Ennek megoldására találták ki az
AAA
mintát, ami az angol Arrange, Act és Assert
kifejezések rövidítése. Mindössze annyit tanácsol ez a minta, hogy a kódban jól láthatóan, kommentekkel el kell különíteni ezeket a lépéseket. Az egységtesztjeimet ennek a mintának megfelel®en implementáltam, a 5.4 kódrészletben három példa is látható.
[ TestMethod ] [ ExpectedException ( typeof ( InvalidOperationException ))] public void ChildEntriesAddOnRootShouldThrowInvalidOperationException () { // Arrange Category parent = new Category ( CategoryType . Root , " Parent "); Entry child = new Entry (10.0 m , DateTime . Now );
}
// Act parent . ChildEntries . Add ( child );
[ TestMethod ] public void ChildEntriesAddOnIncomeShouldWork () { // Arrange Category parent = new Category ( CategoryType . Income , " Parent "); Entry child = new Entry (10.0 m , DateTime . Now ); // Act parent . ChildEntries . Add ( child );
}
// Assert Assert . IsTrue ( parent . ChildEntries . Contains ( child ));
[ TestMethod ] public void ChildEntriesAddShouldSetBidirection () { // Arrange Category parent = new Category ( CategoryType . Income , " Parent "); Entry child = new Entry (10.0 m , DateTime . Now ); // Act parent . ChildEntries . Add ( child );
}
// Assert CollectionAssert . Contains ( parent . ChildEntries , child ); Assert . AreSame ( parent , child . ParentCategory );
5.4. ábra. Példa a Category osztály tesztjeire.
Ezeken kívül még sok teszt lett deniálva a a háromhoz vagy az el®z® részben az
Entry
Category
osztályra, de jellegükben ehhez
osztálynál taglalt tesztekhez hasonlítanak.
Egyrészt a kategória osztály összes tulajdonságának validációja, másrészt a többi entitással fenntartott relációk helyessége lett vizsgálva. Az eddig felvázoltakhoz hasonlóan épül fel az összes többi teszt is a perzisztens objektumokra, de ezen a rétegen van még egy osztály, ami teljesen más jelleg¶ és ezért kicsit más fajta tesztekre is volt szükség.
39
5.1.3. RecurrenceRule A
Domain Driven Design
architektúrában a Domain modell nem csak a perzisztens ob-
jektumból áll. Minden olyan fogalom és entitás deniálásáért felel, ami az adott problémakörben szerepel. Ennek a mintának megfelel®en az automatikusan ismétl®d® bejegyzések szabálya is itt van implementálva a
RecurrenceRule
osztályban. Ez az osztály tudja meg-
mondani, hogy egy bizonyos ismétl®dési szabállyal (pl. heti, havi, éves) létrehozott bejegyzésnek mikor lesz a következ® el®fordulása. Ezeknek a szabályoknak az implementálását mindössze 1 statikus metódus végzi:
public static IEnumerable < DateTime > GetRecurrenceDates ( RecurrenceType type , DateTime firstDate );
Enum
Paraméterül meg kell adni, hogy mi az ismétl®dés szabálya (
típus) és hogy mi
volt a bejegyzés els® el®fordulásának dátuma. A metódus ezek alapján visszaadja az els®vel együtt az összes el®fordulás dátumát egy végtelenül iterálható
IEnumerable
példány for-
májában. Ez talán els®re nem t¶nik bonyolultnak, az implementációja is relatív egyszer¶, de a dátumkezelésben van pár kivétel amit szerettem volna helyesen kezelni. Például egy havi rendszeresség¶ ismétl®désnél azt várja a felhasználó, hogy ha 29-én vitte fel a bejegyzést, akkor a következ® bejegyzés minden hónap 29. jön létre, kivéve amikor ennél rövidebb a hónap, ekkor az adott hónap utolsó napján kell létrejönnie a bejegyzésnek. Figyelembe kell venni a hónapok hosszát (28, 30, 31) és a szök®éveket, amikor a február 29 napos. Szerencsére a .NET-ben ezek a szabályok mind implementálva vannak a
DateTime
struktúrában, de semmi sem garantálja, hogy mi azt helyesen használtuk. Ezt talán az az eset szemlélteti a legjobban, amikor a Microsoft Azure platformján világszinten hibák
1
jelentek meg , mert egy SSL tanusítványt frissít® kódban egy fejleszt® nem az
AddYears
metódust használta, hanem egyszer¶en növelte a dátumban az évet. Minden olyan tanusítvány amit február 29-én kellett volna frissíteni hibát okozott, hiszen a következ® évben már csak 28 napos volt a hónap, ez pedig komoly károkat eredményezett. Ezt a metódust lényegében két féle képpen teszteltem. Egyrészt a napi és heti ismétl®déssel szemben az elvárás csak annyi, hogy az egymás után következ® bejegyzések között pontosan egy napnak vagy egy hétnek kell eltelnie. Ebb®l adódóan a tesztem annyi, hogy egy el®re megadott dátumtól számítva el®állítottam az els® 100 ismétl®dés pontos id®pontját, majd a listát végigjárva megbizonyosodtam arról, hogy tényleg 1 vagy 7 nap van az egymás utáni dátumok között. A heti ismétl®désre készített egységteszt a 5.5 kódrészletben látható. Persze ha a
DateTime -ot használtam a dátumok el®állítására, majd a DateTime -ra ha-
gyatkozok az eredmények ellenörzésére során, el®fordulhat, hogy fals pozitív eredményt kapok, ezért ezt máshogy is tesztelnem kellett. A többi ismétl®dési szabálynál, a havi, negyedéves és éves szabályoknál nincs állandó dierencia a dátumok között, ezért itt más jelleg¶ teszteket kellett implementálnom. Alaposan utánajártam a szök®évek és dátumok szabályainak, majd kézzel felvittem a különböz® esetekhez az elvárt eredmények els® né-
1
http://blogs.msdn.com/b/windowsazure/archive/2012/03/09/summary-of-windows-azure-service-
disruption-on-feb-29th-2012.aspx
40
[ TestMethod ] public void GetRecurrenceDatesWithWeekShouldReturnDatesSevenDaysApart () { // Arrange DateTime firstDate = new DateTime (2000 , 1, 1, 14 , 20 , 16); // Act DateTime [] recurrences = RecurrenceRule . GetRecurrenceDates ( RecurrenceType . Week , firstDate ). Take (100). ToArray ();
}
// Assert TimeSpan expectedDifference = TimeSpan . FromDays (7.0); DateTime previous = firstDate ; for ( int i = 1; i < recurrences . Length ; i ++) { DateTime current = recurrences [i ]; Assert . AreEqual ( expectedDifference , current - previous ); previous = current ; }
5.5. ábra. Egy id®szakra vonatkozó összegzések összeadásának tesztelése.
hány elemét. Mivel több szabályhoz is több tesztet meg kellett írnom, az elvárt és aktuális eredmények összehasonlítását végz® kódot kiemeltem egy segédfüggvénybe. A segédfüggvény implementációja és egy példa a használatára a 5.6 kódrészletben látható.
[ TestMethod ] public void GetRecurrenceDatesWithYearShouldWorkInLeapYear () { // Arrange DateTime firstDate = new DateTime (2000 , 2, 29 , 14 , 20 , 16); DateTime [] expectedRecurrences = { firstDate , new DateTime (2001 , 2, 28 , 14 , 20 , 16) , new DateTime (2002 , 2, 28 , 14 , 20 , 16) , new DateTime (2003 , 2, 28 , 14 , 20 , 16) , new DateTime (2004 , 2, 29 , 14 , 20 , 16) , new DateTime (2005 , 2, 28 , 14 , 20 , 16) , new DateTime (2006 , 2, 28 , 14 , 20 , 16) , new DateTime (2007 , 2, 28 , 14 , 20 , 16) , new DateTime (2008 , 2, 29 , 14 , 20 , 16) };
}
// Act & Assert GetRecurrenceDatesAndCompareToExpected ( RecurrenceType . Year , firstDate , expectedRecurrences );
private static void GetRecurrenceDatesAndCompareToExpected ( RecurrenceType recurrenceType , DateTime firstDate , DateTime [] expectedRecurrences ) { // Act DateTime [] recurrences = RecurrenceRule . GetRecurrenceDates ( recurrenceType , firstDate ). Take ( expectedRecurrences . Length ). ToArray ();
}
// Assert CollectionAssert . AreEqual ( expectedRecurrences , recurrences );
5.6. ábra. Az éves ismétl®dési szabály tesztelése szök®éveken keresztül.
41
5.2. Domain Services A következ® réteg amit egységtesztekkel lefedtem a Domain szolgáltatások. Ezek összetett, entitások között átível® m¶veleteket hajtanak végre a
Repository -kat (a perzisztenciát meg-
valósító osztályokat) használva. Például egy bejegyzés létrehozása egy kategóriában nem csak egy egyszer¶ m¶velet, különböz® kimutatások optimalizálása céljából napi szinten összegezve vannak a kategóriák bejegyzései, ezeket az összegzéseket pedig a
Summary
en-
titás tárolja. Egy bejegyzés létrehozása az objektum pélányosításán túl a kategóriához kapcsolásból és a napi összegzések frissítéséb®l, esetleg létrehozásából áll. Mivel egy ilyen szolgáltatás implementálásához szükség van a különböz®
Repository -k használatára is, és a
modell azokhoz nem fér hozzá, a m¶veleteket szolgáltatások formájában valósítottam meg. Összesen 4 m¶veletet valósítanak meg a Domain szolgáltatások, ezek implementálásához pedig mindnek szüksége van legalább egy
Repository -ra,
esetleg egy másik szolgáltatásra.
Itt még egységteszteket szerettem volna írni, ezért fel kellett valahogy oldani ezeket a függ®ségeket. Mivel ezek a tesztek még implementálhatóak Windows osztálykönyvtárban is, két féle megoldás áll rendelkezésre:
•
A különböz®
Repository -k
és szolgáltatások mind rendelkeznek egy-egy saját inter-
fésszel amit implementálnak, a rendszerben a magasabb szint¶ komponensek ezekt®l az interfészekt®l függenek. Ennek köszönhet®en ezek a komponensek kicserélhet®ek teszt célú implementációkkal, amik csak az adott teszthez szükséges el®re meghatározott m¶ködésre képesek.
•
Léteznek erre a problémára különböz® keretrendszerek, például a nyílt forrású
Moq [2],
amikkel nagyon futás id®ben hozhatóak létre teszt célú implementációk. Ezek a keretrendszerek általában kód emittálást vagy valamilyen más futásidej¶ kódgenerálási technológiát használnak, így a teszt kódjában tudjuk meghatározni a teszt objektumuok m¶ködését, anélkül, hogy külön osztályokat kéne deniálni.
Sajnos a Windows Phone platformon a .NET futásidej¶ kódgenerálási képességei nem elérhet®ek, így ott csak az els®, valamivel id®igényesebb megoldás áll rendelkezésünkre. Habár úgy alakult, hogy a Windows Phone osztálykönyvtárakban implementált tesztekben nincs szükség ilyen függ®ségfeloldására, mindkét módszerrel implementáltam teszteket.
5.2.1. SummariesDomainService Ennek a szolgáltatásnak az interfésze mindössze 1 metódusból áll, a feladat elvégzéséhez pedig szüksége van az
ISummaryRepository
egy implementációjára. A metódus feladata,
hogy rekurzívan végigjárja a megadott szül® kategóriáit, majd frissítse a megadott dátumra vonatkozó összefoglalókat, esetleg létrehozza azokat, ha még nem léteztek. A szolgáltatás váza a 5.7 ábrán látható. A konstruktorban látható
ISummaryRepository
komponent®l való függést egy új imple-
mentációval oldottam fel. A tesztek megvalósításához implementáltam az
InMemorySum-
maryRepository -t, ami mindössze egy egyszer¶ List<Summary>-t tart a háttérben, és csak 42
public interface ISummariesDomainService { void UpdateSummaries ( Category category , DateTime date , decimal absoluteChange ); } public class SummariesDomainService : ISummariesDomainService { public SummariesDomainService ( ISummaryRepository summaryRepository ) { }
}
public void UpdateSummaries ( Category category , DateTime date , decimal absoluteChange ) { }
5.7. ábra. Az összefoglalókat kezel® Domain szolgáltatás interfésze és implementéciójának váza.
azon valósítja meg az
ISummaryRepository
által elvárt m¶veleteket. Természetesen szem
el®tt kellett tartani a Liskov-féle helyettesítési elvet, és annak minden szabályát be kellett tartani az implementálás során, hiszen csak így garantálható, hogy a két komponens hibák nélkül tényleg kicserléhet®. A tesztek során ezzel a nem perzisztáló, adatbázistól független implementációval példányosítottam a Domain szolgáltatást, végül a vizsgált utasítás meghívása után megvizsgáltam a teszt
Repository
állapotát. Erre a 5.8 ábrán látható egy
példa.
[ TestMethod ] public void UpdateSummariesShouldCreateNewSummaries () { // Arrange Category root = new Category ( CategoryType . Root , " Root "); Category incomeRoot = new Category ( CategoryType . Income , " Income "); root . ChildCategories . Add ( incomeRoot ); DateTime entryDate = new DateTime (2000 , 2, 13); ISummaryRepository summaryRepository = new InMemorySummaryRepository ( new Summary [0]); ISummariesDomainService summariesService = new SummariesDomainService ( summaryRepository ); // Act summariesService . UpdateSummaries ( incomeRoot , entryDate , 10.0 m ); // Assert Assert . AreEqual (2 , summaryRepository . All (). Count ()); Assert . AreEqual (1 , root . ChildSummaries . Count ); Assert . AreEqual ( entryDate , root . ChildSummaries . Single (). Date ); Assert . AreEqual (1 , root . ChildSummaries . Single (). EntryCount ); Assert . AreEqual (10.0 m , root . ChildSummaries . Single (). AbsoluteAmount );
}
Assert . AreEqual (1 , incomeRoot . ChildSummaries . Count ); Assert . AreEqual ( entryDate , incomeRoot . ChildSummaries . Single (). Date ); Assert . AreEqual (1 , incomeRoot . ChildSummaries . Single (). EntryCount ); Assert . AreEqual (10.0 m , incomeRoot . ChildSummaries . Single (). AbsoluteAmount );
5.8. ábra. Az összefoglalók frissítésének m¶veletét vizsgáló egységteszt. A valós adatbázis helyett egy egyszer¶sített, nem perzisztens Repository implementációt használ.
43
5.2.2. CategoriesDomainService Ez a Domain szolgáltatás két m¶veletet biztosít:
AddEntryToCategory
és
RemoveEntry-
FromParentCategory, amik egy új bejegyzés szabályos eltárolásáért vagy törléséért felel®sek. A ICategoriesDomainService implementációja egy IEntryRepository és egy ISummariesDomainService objektumot használnak, így ezt a két függ®séget fel kell oldani. Ennek megoldására a nyílt forráskodú Moq [2] keretrendszert használtam, de annak m¶ködésére csak felületesen térek ki. A keretrendszer használatát a 5.9 ábrán látható egységteszten keresztül mutatom be.
[ TestMethod ] public void AddEntryToCategoryShouldAddTheEntryToTheRepository () { // Arrange Mock < IEntryRepository > entryRepository = new Mock < IEntryRepository >(); Mock < ISummariesDomainService > summariesDomainService = new Mock < ISummariesDomainService >(); ICategoriesDomainService categoriesService = new CategoriesDomainService ( entryRepository . Object , summariesDomainService . Object ); Category category = new Category ( CategoryType . Income , " Income "); Entry entry = new Entry (10.0 m , DateTime . Now ); // Act categoriesService . AddEntryToCategoryAndUpdateSummaries ( entry , category );
}
// Assert entryRepository . Verify ( mock => mock . Add ( entry ));
5.9. ábra. Egy id®szakra vonatkozó összegzések összeadásának tesztelése.
Mock osztályt példányosítok, amik futás id®ben létrehozzák tesztcélú implementációit. Ezek az implementációk az Object tulaj-
Az els® két sorban két a megadott típusok
donságukon keresztül érhet®ek el, így a vizsgált szolgáltatás ezeket használva példányosítható. Alapértelmezetten ezeknek az implementációknak nincsen viselkedésük, a visszatérés nélküli metódusok nem csinálnak semmit, a visszateérési értékkel rendelkez® metódusok pedig az visszatérés típusának alapértelmezett értékével térnek vissza. Ha szükség van rá, megadható a
Mock
objektumon keresztül, hogy melyik metódus hogyan viselkedjen,
ellenörzésképpen pedig megvizsgálható, hogy mely metódusai lettek meghívva. Az utolsó sorban a
Verify
metódussal azt közlöm a keretrendszernek, hogy a teszt csak akkor számít
sikeresnek, ha az
IEntryRepository Add
metódusa meg lett hívva azzal a konkrét
entry
példánnyal. Ezzel sikerült azt ellen®riznem, hogy a szolgáltatás nem csak a kategóriához köti az új bejegyzést, de el is tárolja azt az adatbázisba.
5.3. Domain Repositories Az egyes
Repository -k
az adatbázism¶veleteket implementálják és abszrahálják el úgy,
mintha egy adatbázistábla csak egy egyszer¶ memóriában tárolt adathalmaz lenne. Az a célja ennek a rétegnek, hogy az adatbázistól függ® komponensek lekérdezései újrahasznosíthatóan és jól tesztelhet®en el legyenek különítve. Mivel ezek a lekérdezések a Windows
44
Phone
Linq2Sql
technológiáját használva lettek implementálva, Windows osztálykönyvtár-
ból nem futtathatóak, tehát szükség van egy telefonon vagy emulátorban futú tesztalkalmazásra. Erre a célra elérhet® a
Windows Phone Unit Test App
projekt sablon, ami a
Visual Studio tesztfuttató eszközével integráltan képes m¶ködni. Ez a projekt az
MSTest
Windows Phone verzióját használja, ami valamivel sz¶kebb funkcionalitással bír. Az els® problémám az az
ExpectedException
MSTest
keretrendszer Windows Phone verziójával, hogy hiányzik
attribútum. Ezzel az attribútummal lehet megjelölni azt, hogy egy
teszt csak akkor sikeres, ha teszt metódus a megadott kivételt dobja. Ugyan ezt a viselke-
try-catch struktúrával lehet megvalósítani: a try blokkban meghívjuk a vizsgált utasításokat, az Assert.Fail() metódust, végül a catch ágban ellen®rizdést az attribútum nélkül egy
zük, hogy helyes kivételt kaptunk-e. Habár a megoldás egyszer¶, a kódot átláthatatlanná teszi, ezért jobbnak láttam kiemelni ezt az egyszer¶ mechanizmust egy külön osztályba, ami a 5.10 ábrán látható.
internal static class ExceptionAssert { public static void Throws < TException >( Action blockToExecute ) where TException : Exception { try { blockToExecute (); Assert . Fail (" Expected exception of type " + typeof ( TException ) + " but no exception was thrown ."); } catch ( Exception ex ) { Assert . IsTrue ( ex . GetType () == typeof ( TException ), " Expected exception of type " + typeof ( TException ) + " but type of " + ex . GetType () + " was thrown instead ."); } } } 5.10. ábra. Az ExpectedException attribútumot helyettesít® segédfüggvény.
A
Repository -k
teszteléséhez egy éles adatbázisra van szükség, így ezek már integráci-
ós teszteknek számítanak. Mivel a
Repository -k
lényegében csak 1-1 adatbázislekérdezést
implementálnak, általában úgy lehet letesztelni ezeket, hogy a teszt elején egy üres adatbázisba feltöltünk tesztadatokat, majd megvizsgáljuk milyen eredményeket ad a lekérdezés. Fontos, hogy a tesztek izoláltan fussanak, vagyis az el®z®leg létrehozott tesztadatokat, vagy az egész adatbázist törölni kell és újra létre kell hozni. Ez egy komplex adatbázisnál egy nagyon lassú m¶velet lehet, de egy olyan egyszer¶ adatbázisnál mint ez, nincs észrevehet® lassulás. A
Linq2Sql
lehet®séget ad arra, hogy megjelöljünk teszt inicializáló és takarító rutinokat,
Repository teszt független legyen egymástól. Deniáltam egy absztrakt ®sosztályt RepositoryTestBase néven, ami a leszármazottjai számára egy Linq2Sql DataContext példányt biztosít, amin keresztül minden tesztben egy frissen ezzekkel pedig megoldható, hogy minden
létrehozott adatbázishoz lehet hozzáférni. Ennek a kódja a 5.11 ábrán látható, m¶ködése magától értet®d®.
45
public abstract class RepositoryTestBase { protected Linq2SqlDataContext DataContext { get ; private set ; } [ TestInitialize ] public void Initialize () { this . DataContext = new Linq2SqlDataContext (" isostore :/ test . sdf "); if ( this . DataContext . DatabaseExists ()) { this . DataContext . DeleteDatabase (); } }
}
this . DataContext . CreateDatabase ();
[ TestCleanup ] public void Cleanup () { this . DataContext . Dispose (); }
5.11. ábra. Az adatbázistesztek izolációját biztosít® ®sosztály.
Az ilyen adatbázisteszteknél még az is probléma tud lenni, hogy néhány teszthez nem elég 1-2 adatsor, hanem az adatok egy komplex struktúrájára van szükség. Ezzel a problémával a
SummaryRepository tesztjeinek
írásakor szembesültem. A
SummaryRepositorymeghatároz egy Tes-
Tests az ®sosztálya által meghatározott inicializálás mellett maga is tInitialize rutint, ahol minden egyes teszt el®tt létrehozhatja a tesztadatokat. Ezt az adat-
struktúrát úgy választottam meg, hogy minden tesztnek érdekes eseteket biztosítson. Az osztálynak err®l a részér®l a 5.12 ábrán látható kódrészlet.
[ TestClass ] public class SummaryRepositoryTests : RepositoryTestBase { private Category rootCategory ; private Category childCategory ; private Summary rootSummary1 ; private Summary rootSummary2 ; private Summary rootSummary3 ; ... [ TestInitialize ] public void InitializeData () { this . childSummary1 = new Summary ( new DateTime (2000 , 1, 10)) { AbsoluteAmount = 10.0 m }; this . childSummary2 = new Summary ( new DateTime (2000 , 1, 11)) { AbsoluteAmount = 10.0 m };
} }
this . childCategory = new Category ( CategoryType . Expense , " child "); this . childCategory . ChildSummaries . Add ( this . childSummary1 ); this . childCategory . ChildSummaries . Add ( this . childSummary2 ); ...
...
5.12. ábra. A közös tesztadatokat inicializáló rutin a SummaryRepository tesztjeihez.
46
Ezzel a többszint¶, nem kevés el®készülettel viszont sikerült elérni, hogy a különböz®
Repository -k
tesztjei a lehet® legegyszer¶bben legyenek. Ha olyan tesztadatokat sikerül
készíteni, ahol minden lényeges eset el®fordul, a tesztek mindössze néhány sorból megvalósíthatóak, ahogy az a 5.13 ábrán látható kódrészletben is látható.
[ TestMethod ] public void SumWithoutSummariesShouldReturnZero () { SummaryRepository summaryRepository = new SummaryRepository ( this . DataContext . Summaries ); decimal sum = summaryRepository . Sum ( this . rootCategory , new DateTime (2000 , 1, 1) , new DateTime (2000 , 1, 2)); Assert . AreEqual (0.0 m , sum ); } 5.13. ábra. Egy id®szakra vonatkozó összegzések összeadásának tesztelése.
47
6. fejezet
Értékelés Az mobil alkalmazáshoz összesen 116 teszt készült el, a tesztek száma és komponensek kódfedettsége (ha mérhet® volt) a következ® táblázatban látható:
Komponens
Tesztelés típusa
Tesztek száma Kódfedettség
Domain.Model
Egységtesztek
58
Domain.Repositories
Integrációs tesztek
23
Domain.Services
Egységtesztek
23
Application.Services
Integrációs tesztek
12
99.1%
100.0%
Az els® tapasztalatom a tesztek megírása kapcsán természetesen az volt, hogy sok id®t igényel. Habár rövidek a tesztmetódusok nagyrészük mindössze 2-5 sor terjedelm¶ és csak kis százalékuk haladta meg a 10 sort nem mindig egyértelm¶ milyen tesztet és hogyan kéne implementálni. Legtöbbször amiatt akadt meg a tesztírás, mert természetellenesnek érz®dött az, ahogyan tesztelni kéne, ez pedig az architektúra hibáját jelzi. A szakdolgozat csak a szoftver tesztelésér®l szól, de emellett magát az alkalmazást is fejlesztenem kellett, és így, hogy a tesztelést a fejlesztéssel párhuzamosan végeztem, az architektúra és a tesztelés is jobb min®ség¶ lett. Ha nem lett volna lehet®ségem módosítani a kódon, a tesztek bonyolultabbak lettek volna és ebb®l adódóan kevésbe alaposak. Az eredmény egy olyan rendszer lett volna, amit nehezebb fenntartani és módosítani. Így, hogy az alkalmazást az igényeknek megfelel®en módosíthattam, folyamatosan úgy változtattam a kódot, hogy a teszteket a lehet® legegyszer¶bben meg lehessen valósítani. Lényegében ez volt az a folyamat, ami miatt nehéznek és id®igényesnek t¶nt a tesztírás, de úgy érzem, hogy már csak a kódszervezés javulása miatt is megérte automatizált teszteket írni.
6.1. Kódfedettség A táblázatban látható 2 kódfedettség irreálisan nagynak t¶nhet. Bonyolult projektekben nem reális kit¶zni még a 80%-os kódfedettséget sem, leginkább azért, mert a munkaigény ezzel kapcsolatban nem lineárisan n®. A Domain modellben én is beleestem abba a tipikus hibába, hogy egyszercsak nem a hibák felderítése volt a cél, hanem a kódfedettség növelése, és ennek köszönhet®en több fölösleges teszt is megszületett. Például voltak egyszer¶
getter
metódusok, amik nem csináltak mást, csak visszatérnek a hozzájuk tartozó privát mez®kkel.
48
Ezek nyilván nem igényelnek nagy gyelmet, nem igazán tudnak elromlani, de sikerült arra is energiát pazarolnom, hogy a tesztek ezeket a
getter -eket
is érintsék, csak azért, hogy
n®jön a kódfedettség. A maradék 0.9% lefedetlen kód az ismétlések dátumait kiszámoló metódusban van, egy
switch-case
struktúrában. Egy
enum
értékei alapján dönti el a számítás módját, ahol min-
den érték használva van, de ilyen esetben is szerencsés elhelyezni egy
default
ágat. Ha
bárki b®vítené az ismétl®dési szabályokat és lefutásra kerülne ez a kód, egy kivétel formájában azonnal értesülünk az elmaradt b®vítésr®l, míg enélkül nehezen észrevehet® hibákat tudnánk okozni a rendszerben. Ez volt az a
default
ág, amit nem tudtam érinteni az egy-
ségtesztjeimmel, és ezen a ponton ébredtem rá, hogy ez a hozzáállás már több kárt okozott mint hasznot. Ezek utólag triviálisnak t¶nnek, de ez is egy olyan tudás, amit hiába írnak le a könyvekben, úgyis a saját hibáján tanulja meg az ember. Ezzel szemben a Domain szolgáltatásoknál a 100%-os kódfedettség eléréséhez nem kellett ebbe a csapdába esnem. Ez leginkább a réteg egyszer¶ségének köszönhet®, mindössze 4 különböz® elemi m¶veleteket biztosítanak. A szolgáltatások egymást is felhasználják a m¶veleteik implementációira, a modell szabályainak betartásában pedig a Domain modellre hagyatkoztak, így ezek az összetett m¶veletek a lehet® legegyszer¶bb sikerült implementálni.
6.2. Tesztelés során felderített hibák A tesztelés során legtöbbször már a teszt megírása el®tt vagy közben észrevettem a hibát, sokszor már csak akkor fejeztem be és indítottam el a tesztet, amikor ki is javítottam azt. Sajnos ezeket a javításokat nem sikerült megfelel®en dokumentálnom. Ezeken kívül körülbelül egy tucat egységteszt volt, amik ®szintén megleptek, amikor sikertelenül futottak le. Ezeket a teszteket csak azért sikerült megírnom, mert elvonatkoztattam az implementációról és kizárólag csak arra koncentráltam mi is a specikációjuk a komponenseknek. Legtöbbször a kivételkezeléssel kapcsolatban találtam hibákat, bizonyos esetekben elfelejtettem dobni vagy más kivételt sikerült eldobnom, mint ami elvárható lett volna, de volt két funkcionális hiba is, amit máskülönben nem vettem volna észre. Az els® egyáltalán nem nevezhet® súlyos hibának, a tesztek azt mutatták, hogy gyökér kategóriához gyökér kategóriát is fel lehetett venni, de második már kritikusnak is nevezhet®. A szolgáltatás, amely azért volt felel®s, hogy kitöröljön az adatbázisból egy bejegyzést az összefoglalók frissítésével együtt, a manuális teszteknél még úgy t¶nt helyesen m¶ködik. Az erre írt egyik egységteszt arra mutatott rá, hogy a szolgáltatás a kategóriából tényleg leveszi a bejegyzést és az összefoglalókat is megfelel®en frissíti, de az adatbázisból nem törli a sort. Egy egyszer¶ sor hiányzott a kódból, ami fölött nagyon könnyen el lehet siklani. A gyakorlatban a felhasználó ezt nem veszi észre, de folyamatosan gy¶lt volna az adatbázisban a szemét, és ezt a hibát lehet, hogy nagyon sokáig nem vettem volna észre.
49
6.3. Élesben felderített hibák Az alkalmazást id®közben ki is tettem a
Windows Store -ba,
így az elérhet®vé vált a világ
nagy részén. Ennyi tesztelés után természetesen rendszeresen ellen®riztem a jelentéseket és reménykedtem, hogy tényleg hibátlan szoftvert sikerült kitenni a piacra, de talán egy hét se volt, és a
Microsoft
már rögzített is néhány hibát. A jelentések alapján összesen két
fajta hibára derült fény, mindkett®t kifejezetten tanulságosnak tartok.
Xaml fájlokban tervezési id®ben használt adatok is szerepeltek. Ezek az adatok csak a Visual Studio és Blend szerkeszt®felületén jelennek Amikor kikerült az alkalmazás a piacra, a
meg, aminek célja a felület elkészítésének egyszer¶sítése. Ezek az adatok természetesen nem jelennek meg a futó alkalmazásban, de a kódjuk benne marad és egy ilyen nem funkcionális kódrészlet okozta a hibát. A hónap áttekint® képerny®jén a bejegyzés lista megjelenítésére példányosítottam
Entry -ket,
amikhez dátumokat is deniálni kellett. Mivel ez egy átme-
neti, csak felülettervezést segít® kód volt, próbáltam gyorsan megcsinálni, és a
DateTime
példányokat szövegekkel inicializáltam. Ezekb®l egyszer¶bb volt másolás-beillesztéssel létrehozni több tucat bejegyzést, és ezzel el is helyeztem a hibát a rendszerben. El®ször egy brazil felhasználónak sikerült belefutnia a hibába, mert ott nap/hónap/év felosztást használnak, és amikor megpróbálta megnyitni a kérdéses oldalt, a háttérben a tervezési idej¶ adatok teljesen fölöslegesen próbáltak példányosodni, és ennek köszönhet®en kivétel lépett fel a rendszerben. Tisztában voltam azzal, hogy a kulturális kérdések sokszor okoznak efféle gondokat és kifejezetten gyeltem rá az alkalmazás más részeinél, de a tesztelés során teljesen megfeledkeztem ezekr®l az adatokról. A másik jelleg¶ hiba, ami a Microsoft jelentéseiben szerepelt, egy rejtélyes jelenség volt. A kivétel részleteiben az látszott, hogy a hiba két oldal navigálása közben lépett fel, mélyen a Silverlight saját kódjában. Feltételezhet® volt, hogy nem a Silverlight-ban van a hiba, de többórás nyomozás után se sikerült megfejteni, hogy melyik kódom okozhat ilyen problémát. A megoldás végül az volt, hogy nem én okoztam a hibát. A prezentációs rétegben a
Windows Phone Toolkit
osztálykönyvtárt használtam az oldalak közötti animált
1 ami
navigáció megvalósítására, és végül a honlapjukon találtam rá egy hibabejelentésre
megmagyarázta a jelenséget. Ha egy elindított navigáció közben egy bizonyos id®pontban kezdeményezett a felhasználó egy új navigálást, ilyen hiba léphetett fel. Ez a két hiba azért tanulságos számomra, mert mindkett® olyan helyr®l származik, amire nem számítottam: egy nem használt kódból és egy küls®, megbízhatónak tartott osztálykönyvtárból.
Összességében az elkészített teszteknek köszönhet®en jobb lett a kód és a rendszer min®sége is. Még mindig nem jelenthetem ki teljes bizonyossággal, hogy az alkalmazás hibátlan lenne, de bizonyos komponensek helyességében magabiztos lehetek.
1
https://silverlight.codeplex.com/workitem/8396
50
Összefoglalás A szakdolgozat célja a szoftvertesztelési lehet®ségek vizsgálata volt Windows Phone platformon egy konkrét alkalmazás tesztelésén keresztül. Ennek keretében 116 teszt készült el, amik a szoftver egyes komponenseiben közel 100%-os kódfedettséget eredményeztek, ráadásul a folyamat maga az alkalmazás kódmin®ségére is pozitív hatással volt.
Az els® fejezetben el®ször összefoglaltam a tesztelési alapelveket és tesztelési technikák alapjait, els®sorban fejleszt®i szemszögb®l. Sorra vettem a legfontosabb szoftvertesztelési technikák statikus/dinamikus, automatizált/manuális és feketedoboz/fehérdoboz kategorizálás szerint, majd a tipikus tesztelési szinteket. Ezután a
SOLID
objektum-orientált
tervezési elveket mutattam be, amik egy könnyen fenntartható és tesztelhet® rendszer megtervezésében segítenek. Ezek alapszint¶ ismerete elengedhetetlen a hatékony teszteléshez. A második fejezetben röviden bemutattam a
Money Manager
alkalmazás funkcióit és
architektúráját, aminek tervezése során a könny¶ tesztelhet®ségre törekedtem. A harmadik fejezetben az alkalmazás tesztelésének megtervezése következett tesztelési szintenként. Architektúrális rétegenként meghatároztam, hogy mire és hogyan érdemes egységteszteket írni, gyelembe véve a komponensek közötti függ®ségeket, és hogy mi az a pont, ahol már túl nagy munka lenne a komponensek izolációját megoldani, és integrációs tesztekre kell hagyatkozni. Ezeken túl a teljesség kedvéért a manuális tesztelésre is kitértem. A negyedik fejezetben a Windows Phone platformon elérhet® tesztelési eszközök korlátairól volt szó, és egy megoldás bemutatásáról. A Windows Phone operációs rendszeren futó alkalmazások képességei jóval korlátozottabbak a Windows alkalmazásokéhoz képest, mert csak a .NET által nyújtott API-t képesek használni. Emiatt a platformon futó tesztfuttató alkalmazás nem képes többek között a kódfedés mérésére se, a fejezetben ennek a problémának a megoldását vizsgáltam meg. Az ötödik fejezetben a platform korlátozásainak megkerülésére bemutatott megoldások használatával mutattam be az alkalmazás egység- és integrációs tesztjeinek implementációját az
MSTest
keretrendszerre támaszkodva. El®ször az architektúrában a legalacsonyabb
szint¶ réteg, a Domain modell egységtesztjeit mutattam be. A függ®ségek izolációjával itt nem kellett foglalkozni, de az
Entry, Category
és
RecurrenceRule
osztályokon keresz-
tül bemutattam milyen jelleg¶ egységteszteket lehet írni egyszer¶ osztályokra. Ezután két Domain szolgáltatás egységtesztjeit mutattam be, amiknek függ®ségeit egyrészt a nyílt forráskódú
Moq keretrendszer használatával, másrészt egy saját teszt objektum implementáci-
51
ójával oldottam meg. Végül az adatbázis lekérdezéseit implementáló
Repository
osztályok
tesztjeinek implementációira tértem ki, amiket már csak a Windows Phone platformon lehetett futtatni. Végül a hatodik fejezetben az alkalmazásom tesztelésének teljes folyamatát értékeltem ki. Összefoglaltam a megírt tesztek számát és az elért kódfedettséget, továbbá összegy¶jtöttem a munka során szerzett tapasztalataimat. Végül összegy¶jtöttem a tesztelés során felderített hibákat, és azokat a problémákat, amikre már csak éles m¶ködés közben derült fény.
Továbbfejlesztési lehet®ségek Grakus felülettel rendelkez® alkalmazásoknál érdemes még GUI tesztelést is alkalmazni. Sajnos a Microsoft a .NET más részeivel szemben, a Windows Phone Silverlight keretrendszeréhez nem készítette el a
UI Automation
[9] rendszerét, így csak egyéb zet®s és
nyílt-forráskódú megoldások léteznek. Az utóbbi id®ben a Microsoft az univerzális alkalmazáfejlesztés megvalósítására törekedett, és ennek keretében a Silverlight helyett a
Runtime
Windows
API-ra (WinRT) koncentrált. A szoftverek, amelyek ezt az API-t használják egy-
szerre Windows 8 és Windows Phone 8.1 platformon is képesek futni. A szakdolgozat írása közben jelent meg a Visual Studio Update 2
2 frissítése, ami GUI tesztelési eszközöket is
telepít a Visual Studioba, így már hivatalos támogatás is létezik erre a feladatra. Sajnos a példa alkalmazás a Windows Phone Silverlight keretrendszerére épül, átírására pedig nem volt lehet®ségem. Ha a mobil platformon szükség lesz GUI tesztelésre, a keretrendszer lehet®ségeire kell már támaszkodni.
2
http://www.visualstudio.com/en-us/news/2014-apr-2-vs.aspx
52
Windows Runtime
Irodalomjegyzék [1]
Guide to the Software Engineering Body of Knowledge.
IEEE,
2014.
URL:
http://www.computer.org/portal/web/swebok. [2] Moq/moq4, Május 2014. URL: https://github.com/Moq/moq4. [3] Eric Evans.
Domain-Driven Design: Tackling Complexity in the Heart of Software.
Addison-Wesley Professional, 2003. [4] William C. Hetzel.
The Complete Guide to Software Testing.
QED Information Sci-
ences, 1988. [5] ISTQB.
Hungarian standard glossary of terms used in Software Testing.
[6] Robert
C.
Martin.
Design
principles
and
design
patterns.
2013.
2000.
URL:
http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf. [7] Steve McConnell.
Code Complete.
[8] Microsoft.
A
Microsoft Press, 2004.
history
of
Windows,
Május
2014.
URL:
http://windows.microsoft.com/en-AU/windows/history#T1=era6. [9] Microsoft.
Ui
automation
fundamentals,
Május
2014.
URL:
http://msdn.microsoft.com/en-us/library/ms753107(v=vs.110).aspx. [10] Richard H. Cobb; Harlan D. Mills. control.
Engineering software under statistical quality
IEEE Software, 7(6):4554, November 19990.
[11] Glenford J. Myers.
The Art of Software Testing.
53
John Wiley & Sons, Inc., 2004.
Függelék F.1. A Domain réteg
F.1.1. ábra. A Category, Entry, RecurrentEntry és Summary entitások viszonya.
F.1.2. ábra. A Settings entitás.
54
F.1.3. ábra. A napi bontású összefoglalókat menedzsel® szolgáltatás.
F.1.4. ábra. Egy bejegyzés hozzáadásának m¶veletét levezényl® szolgáltatás.
F.1.5. ábra. Az ismétl®d® bejegyzéseket megvalósító szolgáltatás.
55