Az új vállalathoz való csatlakozás – amely már kialakult kultúrával és programozási gyakorlatokkal rendelkezik – ijesztő élmény lehet. Amikor csatlakoztam az Ansible csapatához, úgy döntöttem, hogy összeírom azokat a szoftverfejlesztési gyakorlatokat és elveket, amelyeket az évek során tanultam, és amelyek szerint igyekszem dolgozni. Ez az elvek nem végleges, nem kimerítő listája, amelyeket bölcsen és rugalmasan kell alkalmazni.
A szenvedélyem a tesztelés, mivel úgy vélem, hogy a jó tesztelési gyakorlatok egyrészt biztosíthatják a minimális minőségi szabványt (ami sajnos sok szoftvertermékből hiányzik), másrészt irányíthatják és alakíthatják magát a fejlesztést. Ezen elvek közül sok kapcsolódik a tesztelési gyakorlatokhoz és eszmékhez. Ezen elvek közül néhány Python-specifikus, de a legtöbb nem az. (Python-fejlesztők számára a PEP 8 a programozási stílus és irányelvek első megállóhelye.)
Általánosságban elmondható, hogy mi, programozók nagy véleményűek vagyunk, és az erős vélemények gyakran a nagy szenvedély jelei. Ezt szem előtt tartva, nyugodtan nem érthetsz egyet ezekkel a pontokkal, és a hozzászólásokban megvitathatjuk és megvitathatjuk őket.
A legjobb fejlesztési és tesztelési gyakorlatok
1. YAGNI: “You Aint Gonna Gonna Need It”. Ne írj olyan kódot, amiről úgy gondolod, hogy a jövőben szükséged lehet rá, de még nincs rá szükséged. Ez képzeletbeli jövőbeli felhasználási esetekre való kódolás, és a kód elkerülhetetlenül halott kóddá válik, vagy újraírásra szorul, mert a jövőbeli felhasználási esetről mindig kiderül, hogy kissé másképp működik, mint ahogyan elképzelted.
Ha jövőbeli felhasználási esetre írsz kódot, azt egy kódvizsgálat során megkérdőjelezem. (Az API-kat például úgy lehet és kell megtervezni, hogy lehetővé tegyék a jövőbeli felhasználási eseteket, de ez egy másik kérdés.)
Az ugyanez igaz a kód kikommentálására is; ha egy kikommentált kódblokk bekerül a kiadásba, akkor nem szabadna léteznie. Ha olyan kódról van szó, amelyet vissza lehet állítani, készíts egy jegyet, és hivatkozz a kód törlésének commit hash-jára. A YAGNI az agilis programozás egyik központi eleme. A legjobb referencia ehhez a Kent Beck által írt Extreme Programming Explained.
2. A teszteknek nincs szükségük tesztelésre. A teszteléshez szükséges infrastruktúráknak, keretrendszereknek és könyvtáraknak tesztekre van szükségük. Ne teszteld a böngészőt vagy a külső könyvtárakat, hacsak nem muszáj. Az általad írt kódot teszteld, ne mások kódját.
3. A harmadik alkalom, amikor ugyanazt a kódot írod, a megfelelő alkalom arra, hogy egy általános célú segédprogramba vond ki (és teszteket írj hozzá). A segédfüggvényeknek egy teszten belül nincs szükségük tesztelésre; amikor kitöröd és újrafelhasználod őket, akkor igenis szükségük van tesztekre. A harmadik alkalommal, amikor hasonló kódot írsz, általában már világos elképzelésed van arról, hogy milyen formájú az általános célú probléma, amit megoldasz.
4. Amikor az API tervezéséről van szó (külső szembefordított és objektum API): Az egyszerű dolgok legyenek egyszerűek; az összetett dolgok legyenek lehetségesek. Először az egyszerű esetre tervezz, lehetőleg nulla konfigurációval vagy paraméterezéssel, ha ez lehetséges. Az összetettebb és rugalmasabb felhasználási esetekhez adjunk hozzá opciókat vagy további API-módszereket (amint szükség van rájuk).
5. Gyorsan hibázzon. Ellenőrizze a bemenetet, és a lehető leghamarabb bukjon el értelmetlen bemenet vagy érvénytelen állapot esetén, lehetőleg egy olyan kivétellel vagy hibajelzéssel, amely egyértelművé teszi a hívó számára a pontos problémát. Engedélyezze azonban a kódja “innovatív” felhasználási eseteit (pl. ne végezzen típusellenőrzést a bemenet érvényesítéséhez, hacsak nem feltétlenül szükséges).
6. Az egységtesztek a viselkedés egységére tesztelnek, nem a megvalósítás egységére. Az implementáció megváltoztatása anélkül, hogy a viselkedés megváltozna, vagy a tesztek bármelyikét meg kellene változtatnod, a cél, bár ez nem mindig lehetséges. Tehát ahol lehetséges, kezelje a tesztobjektumokat fekete dobozként, a nyilvános API-n keresztül tesztelve, anélkül, hogy privát metódusokat hívna meg vagy az állapotot piszkálná.
Néhány összetett forgatókönyv esetén – például a viselkedés tesztelése egy adott összetett állapoton egy homályos hiba megtalálása érdekében – ez nem biztos, hogy lehetséges. A tesztek első megírása valóban segít ebben, mivel arra kényszerít, hogy átgondold a kódod viselkedését és azt, hogy hogyan fogod tesztelni, mielőtt megírnád. Az első tesztelés kisebb, modulárisabb kódegységeket ösztönöz, ami általában jobb kódot jelent. Az “először tesztelj” megközelítéssel való kezdéshez jó referencia Kent Beck Test Driven Development by Example című könyve.
7. Az egységteszteknél (beleértve a tesztinfrastruktúra teszteket is) minden kódútvonalat tesztelni kell. A 100%-os lefedettség jó kiindulópont. Az összes lehetséges permutációt/kombinációt nem lehet lefedni (kombinatorikus robbanás), ezért ez megfontolást igényel. Csak nagyon jó ok esetén szabad a kódútvonalakat teszteletlenül hagyni. Az időhiány nem jó ok, és a végén még több időbe kerül. A lehetséges jó okok közé tartozik: valóban nem tesztelhető (bármilyen értelmes módon), a gyakorlatban lehetetlen eltalálni, vagy a teszt máshol fedezi. A tesztek nélküli kód teher. A lefedettség mérése és az olyan PR-ok elutasítása, amelyek csökkentik a lefedettség százalékát, az egyik módja annak, hogy biztosítsuk, hogy fokozatosan haladunk a helyes irányba.
8. A kód az ellenség: elromolhat, és karbantartásra szorul. Írjon kevesebb kódot. Töröljön kódot. Ne írj olyan kódot, amire nincs szükséged.
9. A kódkommentárok idővel elkerülhetetlenül hazugságokká válnak. A gyakorlatban kevesen frissítik a megjegyzéseket, amikor a dolgok változnak. Törekedjen arra, hogy a kódja olvasható és öndokumentáló legyen a jó elnevezési gyakorlatok és az ismert programozási stílus révén.
A kódot, amelyet nem lehet nyilvánvalóvá tenni – egy homályos hiba vagy valószínűtlen állapot megkerülése, vagy egy szükséges optimalizálás -, kommentelni kell. Kommentálja a kód szándékát, és azt, hogy miért csinál valamit, nem pedig azt, hogy mit csinál. (A megjegyzések hazugságával kapcsolatos pont egyébként vitatott. Én még mindig úgy gondolom, hogy helyes, és Kernighan és Pike, a The Practice of Programming (A programozás gyakorlata) című könyv szerzői egyetértenek velem.)
10. Írj védekezően. Mindig gondolj arra, hogy mi romolhat el, mi fog történni érvénytelen bemenet esetén, és mi hibázhat, ami segít sok hibát elkapni, mielőtt bekövetkezne.
11. A logikát könnyű egységtesztelni, ha állapotmentes és mellékhatásmentes. Bontsa a logikát külön függvényekre, ahelyett, hogy a logikát állapotos és mellékhatásokkal teli kódba keveri. Az állapotfüggő és az oldalhatásokat tartalmazó kód kisebb függvényekre való szétválasztása megkönnyíti a mockoutot és az oldalhatások nélküli egységtesztelést. (A kisebb tesztelési terhek gyorsabb tesztelést jelentenek.) Az oldalhatások tesztelésre szorulnak, de az egyszeri tesztelésük és a mocking out mindenhol máshol általában jó minta.
12. A globálok rosszak. A függvények jobbak, mint a típusok. Az objektumok valószínűleg jobbak, mint a komplex adatstruktúrák.
13. A Python beépített típusainak – és módszereiknek – használata gyorsabb lesz, mint saját típusok írása (kivéve, ha C-ben írsz). Ha a teljesítmény szempont, próbáld meg kitalálni, hogyan használhatod a szabványos beépített típusokat a saját objektumok helyett.
14. A függőségi injektálás hasznos kódolási minta ahhoz, hogy tisztában legyél azzal, hogy mik a függőségeid, és honnan származnak. (Az objektumok, metódusok stb. inkább paraméterként kapják meg függőségeiket, minthogy maguk is új objektumokat instanciáljanak.) Ez azonban bonyolultabbá teszi az API-aláírásokat, így ez egy kompromisszum. Ha a végén olyan metódus lesz, amelynek 10 paraméterre van szüksége az összes függőségéhez, az amúgy is jó jel, hogy a kódod túl sokat csinál. A függőségi injektálásról szóló meghatározó cikk Martin Fowler “Inversion of Control Containers and the Dependency Injection Pattern” című írása.
15. Minél többet kell mockolnod a kódod teszteléséhez, annál rosszabb a kódod. Minél több kódot kell instanciálnod és beüzemelned ahhoz, hogy egy adott viselkedést tesztelni tudj, annál rosszabb a kódod. A cél a kis tesztelhető egységek, valamint a magasabb szintű integrációs és funkcionális tesztek, amelyekkel tesztelhetjük, hogy az egységek megfelelően működnek-e együtt.
16. A külső szemléletű API-k azok, ahol a “design up front” – és a jövőbeli felhasználási esetek mérlegelése – igazán számít. Az API-k megváltoztatása fájdalmas számunkra és a felhasználóink számára is, a visszafelé történő inkompatibilitás megteremtése pedig borzalmas (bár néha lehetetlen elkerülni). Tervezzük meg a külső API-kat körültekintően, továbbra is betartva az “egyszerű dolgok legyenek egyszerűek” elvet.
17. Ha egy függvény vagy metódus meghaladja a 30 sornyi kódot, fontoljuk meg a felbontását. A jó maximális modulméret körülbelül 500 sor. A tesztfájlok általában ennél hosszabbak.
18. Ne végezzen munkát objektumkonstruktorokban, amelyek nehezen tesztelhetőek és meglepőek. Ne tegyünk kódot a __init__.py fájlba (kivéve az importálást a névtérbe). A __init__.py nem az a hely, ahol a programozók általában kódot várnak, ezért “meglepő”.”
19. A DRY (Don’t Repeat Yourself) sokkal kevésbé számít a tesztekben, mint a produktív kódban. Az egyes tesztfájlok olvashatósága fontosabb, mint a karbantarthatóság (újrafelhasználható darabok kiszakítása). Ez azért van, mert a teszteket egyenként hajtják végre és olvassák, nem pedig maguk is egy nagyobb rendszer részei. Nyilvánvaló, hogy a túlzott ismétlődés azt jelenti, hogy a kényelem érdekében újrafelhasználható komponensek hozhatók létre, de ez sokkal kevésbé aggályos, mint a termelésben.
20. Refaktoráljon, amikor szükségét látja és lehetősége van rá. A programozás az absztrakciókról szól, és minél közelebb vannak az absztrakcióid a problématerülethez, annál könnyebben érthető és karbantartható a kódod. Ahogy a rendszerek organikusan növekednek, a bővülő felhasználási esetnek megfelelően változtatniuk kell a struktúrájukat. A rendszerek kinövik absztrakcióikat és struktúrájukat, és ha nem változtatnak rajtuk, az olyan technikai adóssággá válik, amelyet még fájdalmasabb (és lassabb és hibásabb) megkerülni. A technikai adósságok felszámolásának (refaktorálás) költségeit is számításba kell venni a funkciókkal kapcsolatos becslésekben. Minél tovább hagyja az adósságot, annál nagyobb kamatot halmoz fel. A refaktorálásról és tesztelésről szóló nagyszerű könyv Michael Feathers: Working Effectively with Legacy Code.
21. Először a kód legyen helyes, és csak utána gyors. Ha teljesítményproblémákon dolgozol, a javítások elvégzése előtt mindig készíts profilokat. Általában a szűk keresztmetszet nem egészen ott van, ahol gondoltad. Csak akkor érdemes homályos kódot írni, mert az gyorsabb, ha profiloztál és bebizonyítottad, hogy valóban megéri. Ha olyan tesztet írsz, amely a profilozás alatt álló kódot időzítéssel körülvéve gyakorolja, könnyebben tudod, mikor végeztél, és a tesztcsomagban hagyhatod, hogy megelőzd a teljesítményregressziót. (Azzal a szokásos megjegyzéssel, hogy az időzítő kód hozzáadása mindig megváltoztatja a kód teljesítményjellemzőit, így a teljesítményre vonatkozó munka az egyik legfrusztrálóbb feladat.)
22. A kisebb, szűkebb hatókörű egységtesztek több értékes információt adnak, ha sikertelenek – konkrétan megmondják, mi a hiba. Egy olyan teszt, amely a fél rendszert felállítja a viselkedés tesztelésére, több vizsgálatot igényel annak megállapításához, hogy mi a baj. Általában az a teszt, amelynek futtatása 0,1 másodpercnél tovább tart, nem egységteszt. Nincs olyan, hogy lassú unit teszt. A viselkedést tesztelő, szűkre szabott egységtesztekkel a tesztek a kód de facto specifikációjaként működnek. Ideális esetben, ha valaki meg akarja érteni a kódodat, akkor a tesztcsomaghoz fordulhat, mint a viselkedés “dokumentációjához”. Az egységtesztelési gyakorlatokról szóló nagyszerű előadás Gary Bernhardt Fast Test, Slow Test című írása:
23. A “Not Invented Here” nem olyan rossz, mint ahogy az emberek mondják. Ha mi írjuk a kódot, akkor tudjuk, mit csinál, tudjuk, hogyan kell karbantartani, és szabadon bővíthetjük és módosíthatjuk, ahogy jónak látjuk. Ez a YAGNI elvét követi: inkább van specifikus kódunk a számunkra szükséges felhasználási esetekre, mint általános célú kódunk, amely olyan dolgokhoz tartalmaz komplexitást, amelyekre nincs szükségünk. Másrészt a kód az ellenség, és a szükségesnél több kód birtoklása rossz. Fontolja meg a kompromisszumot egy új függőség bevezetésekor.
24. A közös kódtulajdonlás a cél; a silózott tudás rossz. Ez legalább a tervezési döntések és a fontos implementációs döntések megvitatását vagy dokumentálását jelenti. A kódellenőrzés a legrosszabb időpont a tervezési döntések megvitatásának megkezdésére, mivel a kód megírása után nehéz leküzdeni azt a tehetetlenséget, hogy átütő változtatásokat hajtsunk végre. (Persze még mindig jobb a tervezési hibákra a felülvizsgálat idején rámutatni és változtatni, mint soha.)
25. A generátorok szuperek! Általában rövidebbek és könnyebben érthetőek, mint az iterációhoz vagy ismételt végrehajtáshoz szükséges állapotú objektumok. A generátorok jó bevezetője David Beazley “Generator Tricks for Systems Programmers” című könyve.
26. Legyünk mérnökök! Gondolkodjunk a tervezésről és építsünk robusztus és jól megvalósított rendszereket, ahelyett, hogy organikus szörnyeket növesztenénk. A programozás azonban egyensúlyozás. Nem mindig rakétahajót építünk. A túlmérnökösködés (hagymaarchitektúra) ugyanolyan fájdalmas munka, mint az alultervezett kód. Robert Martintól szinte bármit érdemes elolvasni, a Clean Architecture: A Craftsman’s Guide to Software Structure and Design egy jó forrás ebben a témában. A Design Patterns egy klasszikus programozási könyv, amelyet minden mérnöknek el kellene olvasnia.
27. Az időszakosan sikertelen tesztek aláássák a tesztcsomagod értékét, egészen addig a pontig, amikor végül mindenki figyelmen kívül hagyja a tesztfuttatás eredményeit, mert mindig van valami, ami nem sikerül. Az időszakosan sikertelen tesztek javítása vagy törlése fájdalmas, de megéri az erőfeszítést.
28. Általában, különösen a tesztekben, inkább várjunk egy konkrét változásra, minthogy tetszőleges ideig aludjunk. A voodoo alvásokat nehéz megérteni, és lelassítják a tesztcsomagot.
29. Mindig lássuk, hogy a tesztünk legalább egyszer megbukik. Tegyél bele egy szándékos hibát, és győződj meg róla, hogy megbukik, vagy futtasd a tesztet, mielőtt a tesztelt viselkedés befejeződik. Ellenkező esetben nem tudhatod, hogy valóban tesztelsz-e valamit. Véletlenül könnyű olyan teszteket írni, amelyek valójában semmit sem tesztelnek, vagy amelyek soha nem bukhatnak meg.
30. És végül egy pont a vezetőségnek: Az állandó funkció-csiszolgatás borzalmas módja a szoftverfejlesztésnek. Ha nem hagyod, hogy a fejlesztők büszkék legyenek a munkájukra, az garantálja, hogy nem hozod ki belőlük a legjobbat. Ha nem foglalkozol a technikai adóssággal, az lelassítja a fejlesztést, és rosszabb, hibásabb terméket eredményez.