Nastupování do nové společnosti – se zavedenou kulturou a programovými postupy – může být skličující zkušenost. Když jsem nastoupil do týmu Ansible, rozhodl jsem se sepsat postupy a zásady softwarového inženýrství, které jsem se během let naučil a podle kterých se snažím pracovat. Jedná se o nedefinitivní a neúplný seznam zásad, které by se měly uplatňovat s moudrostí a flexibilitou.
Moji vášní je testování, protože věřím, že dobré testovací postupy mohou jednak zajistit minimální standard kvality (který bohužel v mnoha softwarových produktech chybí), jednak mohou řídit a formovat samotný vývoj. Mnohé z těchto zásad se týkají testovacích postupů a ideálů. Některé z těchto zásad jsou specifické pro Python, ale většina z nich není. (Pro vývojáře v Pythonu by měl být PEP 8 první zastávkou, pokud jde o styl a zásady programování.)
Všeobecně platí, že my programátoři jsme názorově vyhranění a silné názory jsou často známkou velkého nadšení. S ohledem na to se nebojte s těmito body nesouhlasit a můžeme o nich diskutovat a polemizovat v komentářích.
Osvědčené postupy pro vývoj a testování
1. Proč? YAGNI: „You Aint Gonna Need It“ (Nebudete to potřebovat). Nepište kód, o kterém si myslíte, že byste ho mohli v budoucnu potřebovat, ale zatím ho nepotřebujete. Jedná se o kódování pro imaginární budoucí případy použití a kód se nevyhnutelně stane mrtvým kódem nebo jej bude třeba přepsat, protože se vždy ukáže, že budoucí případ použití funguje trochu jinak, než jak jste si ho představovali.
Pokud vložíte kód pro budoucí případ použití, budu jej při kontrole kódu zpochybňovat. (Můžete a musíte například navrhnout rozhraní API tak, aby umožňovalo budoucí případy použití, ale to je jiná otázka.)
To samé platí pro komentování kódu; pokud se blok komentovaného kódu dostane do verze, neměl by existovat. Pokud se jedná o kód, který může být obnoven, vytvořte lístek a odkažte se na hash revize pro odstranění kódu. YAGNI je základním prvkem agilního programování. Nejlepším odkazem na tuto problematiku je kniha Extreme Programming Explained od Kenta Becka.
2. Testy nepotřebují testování. Infrastruktura, rámce a knihovny pro testování potřebují testy. Netestujte prohlížeč ani externí knihovny, pokud to opravdu nepotřebujete. Testujte kód, který píšete, ne cizí kód.
3. Potřetí, když píšete stejný kus kódu, je ten správný čas extrahovat ho do pomocného kódu pro obecné použití (a napsat pro něj testy). Pomocné funkce v rámci testu nepotřebují testování; když je vyčleníte a znovu použijete, potřebují testy. Při třetím psaní podobného kódu už máte většinou jasnou představu o tom, jaký tvar má problém obecného účelu, který řešíte.
4. Pokud jde o návrh API (externí tvář a objektové API): Jednoduché věci by měly být jednoduché; složité věci by měly být možné. Navrhujte nejprve pro jednoduchý případ, nejlépe s nulovou konfigurací nebo parametrizací, pokud je to možné. Pro složitější a flexibilnější případy použití přidejte možnosti nebo další metody API (pokud jsou potřeba).
5. V případě složitých a flexibilních případů použití přidejte možnosti nebo další metody API. Rychlé selhání. Kontrolujte vstup a selhávejte při nesmyslném vstupu nebo neplatném stavu co nejdříve, nejlépe s výjimkou nebo chybovou odpovědí, která volajícímu objasní přesný problém. Povolte však „inovativní“ případy použití vašeho kódu (tj. neprovádějte kontrolu typu pro ověření vstupu, pokud to opravdu není nutné).
6. Jednotkové testy testujte na jednotku chování, nikoli na jednotku implementace. Změna implementace, aniž byste měnili chování nebo museli měnit některý z testů, je cílem, i když ne vždy je to možné. Pokud je to tedy možné, chovejte se ke svým testovacím objektům jako k černým skříňkám a testujte je prostřednictvím veřejného rozhraní API, aniž byste volali soukromé metody nebo zasahovali do stavu.
U některých složitých scénářů – například při testování chování na konkrétním složitém stavu za účelem nalezení nejasné chyby – to nemusí být možné. Psaní testů jako první v tomto případě opravdu pomáhá, protože vás nutí přemýšlet o chování vašeho kódu a o tom, jak jej budete testovat, ještě předtím, než jej napíšete. Testování jako první podporuje menší a modulárnější jednotky kódu, což obecně znamená lepší kód. Dobrou příručkou, jak začít s přístupem „test first“, je kniha Test Driven Development by Example od Kenta Becka.
7. U jednotkových testů (včetně testů testovací infrastruktury) by měly být otestovány všechny cesty kódu. Dobrým začátkem je 100% pokrytí. Nemůžete pokrýt všechny možné permutace/kombinace stavu (kombinatorická exploze), takže to vyžaduje zvážení. Pouze pokud existuje velmi dobrý důvod, měly by být cesty kódu ponechány netestované. Nedostatek času není dobrým důvodem a ve výsledku stojí více času. Mezi možné dobré důvody patří: skutečně netestovatelné (jakýmkoli smysluplným způsobem), nemožné zasáhnout v praxi nebo pokryté jinde v testu. Kód bez testů je přítěží. Měření pokrytí a odmítání PR, které snižují procento pokrytí, je jedním ze způsobů, jak zajistit postupný pokrok správným směrem.
8. Kód je nepřítel: Může se pokazit a potřebuje údržbu. Pište méně kódu. Odstraňte kód. Nepište kód, který nepotřebujete.
9. Odstraňte kód, který nepotřebujete. Z komentářů ke kódu se časem nevyhnutelně stanou lži. V praxi málokdo komentáře aktualizuje, když se něco změní. Snažte se, aby byl váš kód čitelný a sebedokumentační díky správným postupům při pojmenovávání a známému programátorskému stylu.
Kód, který nemůže být zřejmý – práce s obskurní chybou nebo nepravděpodobnou podmínkou nebo nezbytná optimalizace – je třeba komentovat. Komentujte záměr kódu a proč něco dělá, spíše než co dělá. (Mimochodem, právě tento bod o lživých komentářích je kontroverzní. Stále si však myslím, že je správný, a Kernighan a Pike, autoři knihy The Practice of Programming, se mnou souhlasí)
10. Co se týče komentářů? Pište defenzivně. Vždy přemýšlejte o tom, co se může pokazit, co se stane při neplatném vstupu a co by mohlo selhat, což vám pomůže zachytit mnoho chyb dříve, než k nim dojde.
11. Vždy se snažte psát tak, abyste byli v obraze. Logiku lze snadno unit testovat, pokud je bezstavová a bez vedlejších efektů. Logiku raději rozdělte do samostatných funkcí, než abyste ji míchali do kódu plného stavů a vedlejších efektů. Oddělení stavového kódu a kódu s vedlejšími efekty do menších funkcí usnadňuje jejich mock out a unit testování bez vedlejších efektů. (Menší režie testů znamená rychlejší testy.) Vedlejší efekty je sice potřeba testovat, ale jejich testování jednou a vysmívání všude jinde je obecně dobrý vzor.
12. Globály jsou špatné. Funkce jsou lepší než typy. Objekty jsou pravděpodobně lepší než složité datové struktury.
13. Objekt je lepší než typ. Použití vestavěných typů Pythonu – a jejich metod – bude rychlejší než psaní vlastních typů (pokud nepíšete v jazyce C). Pokud vám záleží na výkonu, snažte se přijít na to, jak používat standardní vestavěné typy spíše než vlastní objekty.
14. V případě, že vám záleží na výkonu, snažte se přijít na to, jak používat standardní vestavěné typy. Vstřikování závislostí je užitečný kódovací vzor, díky kterému máte jasno v tom, jaké jsou vaše závislosti a odkud pocházejí. (Ať objekty, metody apod. přijímají své závislosti jako parametry, místo aby samy instancovaly nové objekty). To však činí signatury API složitějšími, takže jde o kompromis. Skončit s metodou, která potřebuje 10 parametrů pro všechny své závislosti, je každopádně dobré znamení, že váš kód dělá příliš mnoho. Definitivní článek o vstřikování závislostí je „Inversion of Control Containers and the Dependency Injection Pattern“, jehož autorem je Martin Fowler.
15. V případě, že se vám to nelíbí, můžete se obrátit na jiný článek. Čím více musíte při testování kódu vysmívat, tím horší je váš kód. Čím více kódu musíte instancovat a zavádět, abyste mohli otestovat určitou část chování, tím horší je váš kód. Cílem jsou malé testovatelné jednotky spolu s integračními a funkčními testy vyšší úrovně, které otestují, zda tyto jednotky správně spolupracují.
16. Zkuste se podívat, co se děje. U rozhraní API zaměřených na externí uživatele skutečně záleží na „návrhu předem“ – a na zvážení budoucích případů použití. Změna rozhraní API je nepříjemná pro nás i pro naše uživatele a vytváření zpětné nekompatibility je hrozné (i když se jí někdy nelze vyhnout). Navrhujte rozhraní API směřující navenek opatrně a stále se držte zásady „jednoduché věci by měly být jednoduché“.
17. Pokud funkce nebo metoda přesáhne 30 řádků kódu, zvažte její rozdělení. Vhodná maximální velikost modulu je asi 500 řádků. Testovací soubory bývají obvykle delší než tato hodnota.
18. Neprovádějte práci v objektových konstruktorech, které se obtížně testují a překvapují. Nevkládejte kód do souboru __init__.py (kromě importů pro jmenný prostor). __init__.py není místo, kde by programátoři obvykle očekávali, že najdou kód, takže je „překvapivé“
19. V případě, že se v něm objeví kód, je nutné ho použít. Na DRY (Don’t Repeat Yourself) záleží v testech mnohem méně než v produkčním kódu. Čitelnost jednotlivého testovacího souboru je důležitější než udržovatelnost (vyčlenění opakovaně použitelných částí). Je to proto, že testy jsou prováděny a čteny jednotlivě, spíše než že by samy byly součástí většího systému. Nadměrné opakování samozřejmě znamená, že pro pohodlí mohou být vytvořeny opakovaně použitelné součásti, ale je to mnohem menší problém než v případě produkčních testů.
20. Refaktorujte, kdykoli vidíte potřebu a máte možnost. Programování je o abstrakcích, a čím blíže se vaše abstrakce mapují k problémové doméně, tím snadněji se váš kód chápe a udržuje. Jak systémy organicky rostou, musí měnit strukturu pro rozšiřující se případy použití. Systémy přerůstají své abstrakce a strukturu a jejich nezměnění se stává technickým dluhem, na kterém je bolestivější (a pomalejší a chybovější) pracovat. Zahrňte náklady na odstranění technického dluhu (refaktoring) do odhadů prací na funkcích. Čím déle necháte dluh ležet, tím vyšší úroky se kumulují. Skvělou knihou o refaktoringu a testování je Working Effectively with Legacy Code (Efektivní práce se starším kódem) od Michaela Featherse.
21. Udělejte kód nejprve správný a až poté rychlý. Při práci na výkonnostních problémech vždy profilujte před provedením oprav. Úzké místo obvykle není úplně tam, kde jste si mysleli, že je. Psát obskurní kód, protože je rychlejší, se vyplatí pouze v případě, že jste jej vyprofilovali a prokázali, že se to skutečně vyplatí. Napsání testu, který procvičuje profilovaný kód s časováním kolem něj, usnadňuje poznání, kdy jste skončili, a může být ponechán v sadě testů, aby se zabránilo regresím výkonu. (S obvyklou poznámkou, že přidání časovacího kódu vždy změní výkonnostní charakteristiky kódu, čímž se práce s výkonem stává jedním z frustrujících úkolů)
22. V případě, že se vám nepodaří přidat časovací kód, můžete se obrátit na výrobce. Menší a úžeji vymezené jednotkové testy poskytují cennější informace v případě selhání – říkají konkrétně, co je špatně. Test, který stojí na polovině systému a testuje chování, vyžaduje více zkoumání, aby se zjistilo, co je špatně. Obecně platí, že test, jehož spuštění trvá déle než 0,1 sekundy, není jednotkový test. Neexistuje nic takového jako pomalý jednotkový test. S úzce zaměřenými jednotkovými testy testujícími chování fungují vaše testy de facto jako specifikace vašeho kódu. V ideálním případě, pokud chce někdo porozumět vašemu kódu, měl by mít možnost obrátit se na sadu testů jako na „dokumentaci“ chování. Skvělou prezentací o postupech při jednotkovém testování je kniha Fast Test, Slow Test od Garyho Bernhardta:
23. „Not Invented Here“ není tak špatné, jak se říká. Pokud kód napíšeme, pak víme, co dělá, víme, jak ho udržovat, a můžeme ho rozšiřovat a upravovat, jak uznáme za vhodné. To odpovídá principu YAGNI: Máme specifický kód pro případy použití, které potřebujeme, spíše než kód pro obecné účely, který má složitost pro věci, které nepotřebujeme. Na druhou stranu kód je nepřítel a vlastnit více kódu, než je nutné, je špatné. Zvažte tento kompromis při zavádění nové závislosti.
24. Sdílené vlastnictví kódu je cílem; oddělené znalosti jsou špatné. To znamená minimálně diskutovat nebo dokumentovat rozhodnutí o návrhu a důležitá rozhodnutí o implementaci. Kontrola kódu je nejhorší dobou pro zahájení diskuse o rozhodnutích o návrhu, protože setrvačnost provádět rozsáhlé změny po napsání kódu se těžko překonává. (Samozřejmě je stále lepší poukázat na chyby v návrhu a změnit je v době revize než nikdy.)
25. V případě, že se rozhodujete o změnách v kódu, je třeba se na ně zaměřit. Generátory jsou skvělé! Jsou obecně kratší a srozumitelnější než stavové objekty pro iteraci nebo opakované provádění. Dobrým úvodem do generátorů je kniha „Generator Tricks for Systems Programmers“ (Triky s generátory pro systémové programátory) od Davida Beazleyho
26. Buďme inženýry! Přemýšlejme o návrhu a budujme robustní a dobře implementované systémy, místo abychom si pěstovali organická monstra. Programování je však balancování na hraně. Ne vždy stavíme raketovou loď. Přílišné inženýrství (cibulová architektura) je při práci stejně bolestivé jako nedostatečně navržený kód. Téměř cokoli od Roberta Martina stojí za přečtení, a Clean Architecture: A Craftsman’s Guide to Software Structure and Design je dobrým zdrojem informací na toto téma. Design Patterns je klasická kniha o programování, kterou by si měl přečíst každý inženýr.
27. Občasné selhávání testů snižuje hodnotu vaší sady testů, a to až do té míry, že nakonec všichni výsledky testovacích běhů ignorují, protože vždycky něco selže. Oprava nebo odstranění přerušovaně selhávajících testů je sice bolestivá, ale stojí za to.
28. Jaké jsou výsledky testů? Obecně platí, že zejména u testů raději počkejte na konkrétní změnu, než abyste spali libovolně dlouho. Voodoo spánkům je těžké porozumět a zpomalují sadu testů.
29. V případě, že se budete snažit vyřešit problémy, je nutné je vyřešit. Vždy se alespoň jednou přesvědčte, že váš test selhal. Vložte záměrnou chybu a ujistěte se, že selže, nebo spusťte test před dokončením testovaného chování. Jinak nebudete vědět, že skutečně něco testujete. Náhodně napsat testy, které ve skutečnosti nic netestují nebo které nemohou nikdy selhat, je snadné.
30. V případě, že se vám to nepodaří, můžete se rozhodnout, že test bude fungovat. A nakonec bod pro vedení: Neustálé rozmělňování funkcí je příšerný způsob vývoje softwaru. Nedovolíte-li vývojářům, aby byli na svou práci hrdí, zajistíte, že z nich nedostanete to nejlepší. Neřešení technického dluhu zpomaluje vývoj a výsledkem je horší a chybovější produkt.