Ismerkedés a TDD szépségeivel...
Végre rászántam magam, hogy gyakorlatban is kipróbáljam a Unit Test-ekről olvasottakat, de szokás szerint bizonytalan vagyok, hogy jó úton járok-e. Ehhez kérném a segítségetek!
Ez lenne a unit teszt:ez pedig a tesztelendő kód:Tekintsünk el attól, hogy Pythonban íródott, vegyük úgy, hogy pszeudo kód!
Ha valaki nem ismerné, a
Pár alapelvet felrúgok azzal, hogy "bedrótozom" az
A fenti osztály feladata annyi, hogy visszaadjon egy
Kellőképp primitív feladat a gyakorláshoz, néhány sor az egész, ehhez képest csak azok a tesztek, amiket én ki tudtam találni... Már az is soknak tűnik.
(elfelejtettem ellenőrizni, hogy létezik-e már az adott névhez tartozó példány, így a második kérés új példányt hozott létre)
A 3.-kal meg... nos ott jöttem rá, hogy mennyire nem triviális ez az egész dolog. Ugyanis az
És itt a kérdés: egyáltalán az, amit eddig megírtam, kb. rendben van-e vagy valahol tévúton járok?
A másik: hogyan szokás a teszteket tesztelni, hogy valóban azt tesztelik-e amit...
Ezekre automatizált tesztet írni hülyeség (gondolom én). Marad a manuális tesztelés, de mi van, ha később a teszteken kell módosítani?
■ Ez lenne a unit teszt:
import unittest
import sqlite3
import szemetes
class TestDBFactory(unittest.TestCase):
def test_getInstance_returnValue(self):
# ez itt kicsit borítja a tiszta kód elveket, mert konkrétan azt ellenőrzi,
# amihez semmi köze: valóban egy sqlight3 példányt ad-e vissza a getInstance()?
self.assertIsInstance(szemetes.DBFactory.getInstance(),sqlite3.Connection,
u"Nem sqlite3 példányt ad vissza a getInstance")
def test_getInstance_returnsSameInstance(self):
dbInstance1=szemetes.DBFactory.getInstance()
dbInstance2=szemetes.DBFactory.getInstance()
self.assertEqual(dbInstance1, dbInstance2, u"Azonos adatbázisnév ellenére eltérő instance-t ad vissza!")
def test_getInstance_raisesException(self):
# Hibás fájlnév esetén exceptiont kell kapnom
# A /dev/zero unix alatt (elvileg) csak olvasható, windows alatt meg nem létezik, tehát elvileg
# megfelel a feltételnek
self.assertRaises(sqlite3.OperationalError, szemetes.DBFactory.getInstance,"/dev/zero")
if __name__ == "__main__":
#import sys;sys.argv = ['', 'Test.testName']
unittest.main()
import sqlite3 as db
class DBFactory(object):
databaseObjects = {}
@classmethod
def getInstance(cls,databaseName=':memory:'):
if databaseName not in cls.databaseObjects.keys():
cls.databaseObjects[databaseName]=db.connect(databaseName)
return cls.databaseObjects[databaseName]
Ha valaki nem ismerné, a
self
helyettesíti a PHP-ben használatos $this
-t, a cls
pedig a statikus metódusokban a saját osztályt jelöli.Pár alapelvet felrúgok azzal, hogy "bedrótozom" az
sqlite3
használatát, de nem akartam túlbonyolítani.A fenti osztály feladata annyi, hogy visszaadjon egy
sqlite3.Connection
példányt a paraméterként megadott adatbázishoz. Ha még nem létezik a megadott névhez tartozó példány, akkor létrehoz egy újat, ha már létezik, akkor a létezőt adja vissza. Amennyiben nincs megadva paraméter, akkor egy memóriában létrehozott adatbázist fog használni.Kellőképp primitív feladat a gyakorláshoz, néhány sor az egész, ehhez képest csak azok a tesztek, amiket én ki tudtam találni... Már az is soknak tűnik.
- Vajon megfelelő típusú objektumot kapok-e?
- Ha kétszer lekérem ugyanazzal a paraméterrel, akkor nem hoz-e létre egy új kapcsolatot?
- Valóban hibára fut, ha hibás fájlnevet kap?
(elfelejtettem ellenőrizni, hogy létezik-e már az adott névhez tartozó példány, így a második kérés új példányt hozott létre)
A 3.-kal meg... nos ott jöttem rá, hogy mennyire nem triviális ez az egész dolog. Ugyanis az
assertRaises
-t első menetben rosszul paramétereztem emiatt bármit írtam a paraméterek közé, mindig sikeres volt a teszt. Később jutott eszembe, hogy mi lenne, ha ellenpróbát csinálnék, amivel biztosan failed
lesz az eredmény. Nem az lett... :-)És itt a kérdés: egyáltalán az, amit eddig megírtam, kb. rendben van-e vagy valahol tévúton járok?
A másik: hogyan szokás a teszteket tesztelni, hogy valóban azt tesztelik-e amit...
Ezekre automatizált tesztet írni hülyeség (gondolom én). Marad a manuális tesztelés, de mi van, ha később a teszteken kell módosítani?
Engem már régóta idegesít,
Köszi és igen, nem vagy
Én is elsősorban (ahogy írtam) inkább "pszeudo kódként" használom, de ez legalább olyan, ami le is fut, ha kell. :-)
Miert?
Miért nem(annyira) Python?
Másrészt eddig még nem láttam komoly projektet, ami a pythonos indítás után néhány évvel ne íródott volna át valami egyéb nyelvre. (most nem ugrik be, de valamelyik linuxos admin program is ilyen volt)
Harmadrészt még mindig ott tartok, hogy inkább az elméleti dolgok érdekelnek, egyre kevésbé a gyakorlati megvalósítás (ez utóbbi csak azért van így, mert már biztos vagyok benne, hogy többé nem találok munkát és annyira már nem köt le ez a játék, hogy elég komolyan és mélyen belemásszak)
Az meg már csak apróság, hogy rajtam kívül itt még nem nagyon kérdezősködött senki pythonos témában, ebből feltételezem, hogy a többséget nem igazán érdekli.
Ne vicceljünk
Jó, nem viccelek
És nem haladok, mert már megint OOP elméleten rágódok, ahelyett, hogy csinálnám lesz-ami-lesz... :-(
Erteni
Nem a hozzáértésre céloztam.
VMS-hez, Oracle üzemeltetéshez értettem, mégsem kellettem sehova. Még annyira sem, hogy interjúra behívjanak.
VMS volt a kellemetlenebb, mert ahhoz azért elég ritkán lehet embert találni manapság. A hirdetésben rendszergazdát kerestek. Hónapokkal később derült ki, hogy valójában csoport- vagy osztályvezetőt, aki ért a VMS üzemeltetéshez is...
kulfold
a Rails az Ruby, nem Python
Egyébként (20+ évvel ezelőtti) gyakorlati tapasztalatom van. Amíg procedurálisan is szabad gondolkodni, addig nincs nagy gáz, csak a nyelv rigolyáit kell megtanulni.
Nekem most leginkább az OOP okoz gondokat, mert nehezen olvasok és türelmetlenkedek állandóan.
nem mondod?
http://www.indeed.com/jobanalytics/jobtrends?q=python&l=
http://www.indeed.com/jobanalytics/jobtrends?q=ruby&l=
http://www.indeed.com/jobanalytics/jobtrends?q=php&l=
Bocs, nekem nem tűnt fel. A
A Ruby, mint önálló nyelv, valóban nem olyan elterjedt, de a RoR azt hiszem, lényegesen népszerűbb, mint mondjuk a django.
Mondjuk én a külföldi hirdetéseket eleve nem nézem (öreg vagyok már a költözéshez), a hazaiak közt meg csak elvétve látok ilyesmit. Nem szoktam célzottan keresni, csak az elém kerülő állásajánlatok alapján gondolom így.
Elsőnek szögezzük le, hogy
Alapból ennek úgy kéne mennie, hogy fejben megszületik, hogy mondjuk te akarsz valamit, ami neked szórja az sqlite3 adatbázis kapcsolatokat.
Csinálsz neki egy interface-t, elnevezed SqliteConnectionContainerInterface-nek (vagy ami neked tetszik), ebbe teszel egy metódust,
Szerintem elég logikus, hogy mihez kell itt tesztet írni:
Nyilván minden egyes kivétel típusra, vagy olyan esetre, ami az adott típust kiváltja külön tesztet kell írni (nem string adatbázis név, nem tud kapcsolódni, nincs joga kapcsolódni, stb...). Ezen kívül minden elvárt funkcióra tesztet kell írni (visszaadott érték, adott néven lévő kapcsolat tartása, stb...).
Ami hiányzik nálad annak ellenőrzése, hogy string-e az adatbázis név (mondjuk ezt megteszi helyetted az sqlite connection is, mert gondolom elszáll, ha ilyet adsz neki). A kapcsolat tartását én egy kicsi ciklusban (legalább 3 érték) ellenőrizném 2 érték helyett, de ez is csak apróság. Ha IDE-t használnál, akkor nem tudnád elrontani a sorrendet az assertRaises-nél, az automatikus kiegészítés miatt...
Ha elég logikusan gondolkodsz, és pl az SRP-t szem előtt tartod, akkor nem nagyon fognak változni a tesztjeid, egyébként ha mégis így lenne, akkor őket is refaktorálni kell. Egyébként is kell, mert ki kell emelni belőlük az ismétlődő részeket... A tesztek helyességét nem szokták tesztelni, azt neked kell megmondani ránézésre.
Integrációs?
Lenne pár megjegyzésem - nem értek mindenben egyet azzal, amit leírtál.
Összességében itt a DBFactory elnevezésű osztály egyetlen, "statikus" metódusának működését tesztelném. Ebből az exception tesztelése valóban kilóghat, azt lehet, hogy külön kellene tesztelni annak fényében, amit írsz.ű
A 2. pont nem stimmel (vagy én nem értem, mire gondolsz), mert a metódus kimenete nem egy típus, hanem maga az objektum.
Adatbázis nevének a szintaktikáját nem igazán tudom ellenőrizni egy multiplatform rendszer esetében. Linuxon, megfelelő fájlrendszert használva szinte bármilyen karaktert (ha jól emlékszem, még unicode karaktereket is) tartalmazhat a név, talán a \000-t leszámítva, de Windows-on is meg kell erőltetni magam, ha érvénytelen fájlnevet próbálok kreálni. (leginkább nem létező könyvtárra v. olyanra hivatkozva, amihez nincs jogom hozzáférni)
Ami a hibásan használt asserRaises-t illeti...
Eclipse-t használok PyDev kiegészítéssel és egy apró félreértés miatt nem tudtott közbeszólni a kódkiegészítő sem, hogy hülyeséget csinálok.
Most nincs előttem doksi, de pl. az assertEqual-nak három paramétere van: a két hasonlítandó érték és egy üzenet, ami megjelenik a logban, ha hibás a teszt:
Hogy a későbbiekben mennyire változnak a tesztek... Azt hiszem, épp a Tiszta kódban olvastam, hogy azért azok is változhatnak, nem véletlenül kell ezeket a kódokat is "tisztán" tartani. Itt pl. kapásból feltételezem, hogy most csak azért keletkezett a féloldalnyi teszt, mert ismerkedni próbálok a környezettel. De pl. ha ezt az adatbázis kapcsolatokat előállító osztályt nézem, amint kicsit általánosabbra próbálom alakítani, hogy ne csak sqlite kapcsolatokkal tudjon foglalkozni, máris borulhat az eddig megírt teszt (bejöhet újabb paraméter, lehet, hogy a statikus osztály kevés lesz, példányosítani kell a konstruktornak átadva a szükséges drivert stb.)
Legalábbis én így képzelném.
Persze, változhat a teszt, ha
Persze, változhat a teszt, ha az elvárásai változni a factory-vel kapcsolatban, vagy változik a külső lib, amihez csatolót írsz. Ezért szokták az integrációs teszteket minden ilyen külső lib váltáskor lefuttatni, hogy lássák, hogy lesz e a gond a váltással, illetve ezért szoktak saját adaptereket az ilyen külső libek fölé tenni, hogy csökkentsék a függőséget. Ha pl változik a külső lib, akkor elég az adaptert átírni, ami sokkal kisebb munka, mint minden egyes metódust átírni, ami adatbázist használ...
Ja hát clean code esetében is megírták, hogy max 2 paraméter, aminél még könnyen fejben lehet tartani, hogy melyik melyik, 3 paraméter meg már sok... Én pl assertRaises-nél már map-et adnék át, vagy egy külön objektumot. Js-ben mondjuk ez nem okoznak nagy gondot (object literal), php-ben sem (asszociatív tömb), gondolom python-ban is megoldható valahogy...
Ha magasabb absztrakciós szintekről haladsz lefele, és jól csinálod, akkor úgy változhat meg a teszt, ha kód ismétlődést találsz, és azt emeled ki. Nyilván erre is megvannak az átmeneti megoldások, hogy az ilyest hogyan kell csinálni. A másik, amikor megváltozik a teszt, ha változik az adott osztály funkciója. Mondjuk az ügyfél rájön, hogy egy nagyon picit mást akart, pl egy találati listánál plusz egy filter-t be kell tenni, vagy bármi ilyesmi... Ezek a dolgok az esetek nagy részében nem járnak nagy változással szerintem. Egyébként én is csak próbálgatom a TDD-t, egyelőre csak lassít a munkában...
Úgy hallottam úgy lehet
Sajnos ennek még nem néztem utána, így toolokat se tudok mondani és egy gyors google keresés se adott releváns találatot.
Én valahogy úgy próbálkoztam
Csak az érzékeny lelkemet bántja, hogy tesztelésre írok programot, amit újabb tesztekkel kellene tesztelni, de a tesztet tesztelő teszteket is tesztelni kellene... és ugye az emberi agyban a max. rekurzió mélysége valamivel kisebb, mint PHP-ben v. Pythonban. :-))
A teszteknek az a lényege,
No igen, nem árt, ha az ember
Persze, nem árt, mondjuk ha
Az éles kód teszteli a tesztet.
pp
A példám épphogy nem jó, mert
Elvileg minden if-hez kell
Én is most olvasgatok a code
Tévedések... (offtopik)
SRP - nem tudom, belefutott-e valaki a hibámba az értelmezésével. Ugye ez magyarra fordítva arról szól, hogy "Egy osztálynak egy és csakis egy oka lehet a változásra...".
Magamban elkönyveltem, hogy ennek így kevés értelme van, de aki kitalálta, tudja miről beszél, hát legyen...
Végre "leesett": nem az osztály működésére vonatkozik az a "változás", hanem az elkészült kódhoz a későbbiekben csak "egy" okból szabad hozzányúlni. Ha lehet több ok is az osztály kódjának megváltoztatására, akkor az az osztály nem felel meg az SRP definíciójának.
Továbbra sem teljesen tiszta, mert egy-két példát leszámítva, nehezen tudok elképzelni olyan megvalósítást, aminek a módosítását a későbbiekben ne lehetne több dologgal is indokolni - de itt már jöhet az, hogy nem igazán tudok mit kezdeni az "azonos absztrakciós szint" fogalmával...
SRP
Ez a sok kis osztály jobb, mint egy nagy osztály elve. :)
Mea culpa…
Örömmel látom, hogy nem én
Bennem mindig úgy élt ez az egész, hogy egy felelősségi kör, akkor azon az absztrakciós szintnek nevezett valamin belül egyetlen dologgal kell foglalkoznia. (pl. egy adattároló osztálynak semmi köze a benne tárolt adatok html formátumú megjelenítéséhez)
Így, hogy végre, kezdem felfogni, mit jelent az "egyetlen ok a változásra", kicsit más a leányzó fekvése.
Csak az zavar, hogy nekem még úgy tanították: ismétlődő feladatokat kell szubrutinba, függvénybe tenni. Itt meg már fellép egy olyan igény is, hogy akkor is leválasztok feladatokat az alap programról, ha csak logikailag különülnek el, de nem ismétlődnek.
(mittudomén, egy osztály konstruktorában esetlegesen elvégzendő bonyolultabb műveletek, amit korábbi tanulmányaim alapján simán beraknék a konstruktorba)
Szerintem ezek nem mondanak
Annyiban mondanak neki
Pl. már nem tudom, PL/I v. COBOL fordítónak a köztes, assembly kódját nézegettem: iszonyat mennyiségű művelet volt egy-egy ilyen fv./szubrutin hívása körül. Többféle stack létrehozása, esetenként rendszerkönyvtárakból egy-két hívás stb. majd a saját kódom, ezt követően a stackek felszabadítása, visszatérés után is némi memória rendezgetés.
Mondjuk az is igaz, hogy azoknak a gépeknek a számítási teljesítménye valahol egy 286-os PC környékén lehetett. :-)
Szerintem a python is úgy van
Nyilván ha nincs IO, vagy számításigényes algoritmusokat futtat az ember, akkor érdemes az ilyen dolgokat végiggondolni (de mondjuk pl hd videót konvertálni amúgy sem pythonban illik).
Ja, előre optimalizálni
OK, én ezt értem, hogy
Nekünk még úgy magyarázták, hogy ésszel írjuk a programot, mert ugye:
- megírod valami papírra
- átírod kódlapra (ez egy programnyelvhez igazított beosztású, 80 oszlopos, A4-es papír volt)
- leadod az adatrögzítőknek, akik lyukkártyára lyukasztják.
- elkészül, amikor elkészül, visszakapod.
- leadod futtatásra
- ha épp nem sürgős, akkor lehet, hogy csak három nap múlva jön vissza vagy az eredménytáblázattal, vagy egy féldoboznyi memória dumppal.
Folytassam? ;-)
És akkor még ugye ott volt, hogy a processzor sem volt valami hűde...
Egyébként azon már többször összevitatkoztam az itteni törzsközönséggel, hogy (szerintem) az előre optimalizálás, meg a "nem csinálok olyat, amiről tudom, hogy zabálja az erőforrásokat és tudok hozzá jobb megoldást" azért nem egy kategória.
Általánosságban azért végtelen ideig lehet vitatkozni.
Nyilván az algoritmus bonyolultságára érdemes figyelni, hisz nem mindegy, hogy 2ˆn, nˆ2, vagy C*n lépésből oldja meg a feladatot az ember(n az elemszám, C pedig egy konstans), de hogy ezen belül még optimalizáljunk a kód érthetőségének kárára, szerintem ma már nem elfogadható hozzáállás.
Az egyedül, az elefánttoronyban alkotó "zsenik" kora lejárt. Ma már sokkal fontosabb, hogy csapatban hogyan tudsz dolgozni.
Véleményem szerint aki egyedül dolgozik az is csapatban tolja, mert a fél évvel ezelőtti önmagad már egy tök más ember (ha nem akkor gáz van, mert megálltál a fejlődésben), akivel együtt kell dolgozni, mert nincs olyan, hogy na ez a program kész és használjuk száz évig.
Régen ezt így gondolták, de hibásan. Ez van. Lépjünk ezen túl.
Ha hívnak, hogy baj van, mert lassú a rendszer a leges-legutolsó-utáni dolog (sose fordult elő), hogy azért kéne egy kódhoz hozzányúlni, mert egy plusz függvényhívás van benne.
pp
Nem "egy" feleslegesről
És bizony láttam olyat, hogy java-ban megírt rendszer alá egyre erősebb és erősebb CPU-kat kellett tolni, miközben a diszk alrendszerek változatlanok maradtak...
Pedig csak a userek száma ugrott meg egy picit. :-)
(értsd: a gyakorlati tapasztalataim totál mást mondanak, mint amiket fórumokon olvasok... Pedig nem asztali PC-ken futottak azok a rendszerek, nagyon nem...)
De mindegy, ezt a témát tényleg hagyjuk.
És bizony láttam olyat, hogy
Hát pont, hogy ilyenkor kellett volna belenézni a kódba, és optimalizálni. Gondolom senki nem tette meg, mert egyszerűbb volt vasat tolni alája. Nem értem, hogy ez az egész hogy jön ide... Semmi köze a plusz néhány függvény híváshoz. Senki nem mondja meg hasraütésre, hogy mitől lassú a kód, azért kell profiler. Te maximum gyaníthatod, hogy mi miatt lehet, aztán vagy úgy van, vagy nem (általában nem).
Csak úgy jött ide, hogy
Én meg többször találkoztam olyan esettel, hogy bizony a CPU vagy a CPU is kevés volt.
Csak úgy jött ide, hogy
Ez így nem igaz. Clean code-ban sem azt írták, hogy nem kell foglalkozni vele, hanem azt, hogy először el kell készíteni a programot, és ha már készen van, csak utána optimalizálni, mert akkor derül ki, hogy hol van a szűk keresztmetszet. Itt is csak annyit állítottak, hogy általában az erőforrásokra várás az, ami lassít.