30 cele mai bune practici pentru dezvoltarea și testarea de software

Aderarea la orice companie nouă – cu o cultură și practici de programare stabilite – poate fi o experiență descurajantă. Când m-am alăturat echipei Ansible, am decis să scriu practicile și principiile de inginerie software pe care le-am învățat de-a lungul anilor și la care mă străduiesc să lucrez. Aceasta este o listă nedefinitivă și neexhaustivă de principii care ar trebui aplicate cu înțelepciune și flexibilitate.

Pasiunea mea este pentru testare, deoarece cred că bunele practici de testare pot atât să asigure un standard minim de calitate (care din păcate lipsește în multe produse software), cât și să ghideze și să modeleze dezvoltarea în sine. Multe dintre aceste principii se referă la practicile și idealurile de testare. Unele dintre aceste principii sunt specifice Python, dar majoritatea nu sunt. (Pentru dezvoltatorii Python, PEP 8 ar trebui să fie prima oprire pentru stilul de programare și liniile directoare.)

În general, noi, programatorii, suntem o mulțime de oameni cu opinii, iar opiniile puternice sunt adesea un semn de mare pasiune. Ținând cont de acest lucru, nu ezitați să nu fiți de acord cu aceste puncte, iar noi le putem discuta și dezbate în comentarii.

Cele mai bune practici de dezvoltare și testare

1. YAGNI: „You Aint Gonna Need It”. Nu scrieți cod de care credeți că ați putea avea nevoie în viitor, dar nu aveți nevoie încă. Aceasta este codare pentru cazuri de utilizare viitoare imaginare și, în mod inevitabil, codul va deveni cod mort sau va trebui rescris, deoarece cazul de utilizare viitor se dovedește întotdeauna să funcționeze ușor diferit de cum vi l-ați imaginat.

Dacă puneți cod pentru un caz de utilizare viitor, îl voi pune sub semnul întrebării în cadrul unei revizuiri de cod. (Puteți și trebuie să proiectați API-uri, de exemplu, pentru a permite cazuri de utilizare viitoare, dar aceasta este o altă problemă.)

Același lucru este valabil și pentru comentarea codului; dacă un bloc de cod comentat va intra într-o versiune, acesta nu ar trebui să existe. Dacă este cod care poate fi restaurat, faceți un bilet și faceți referire la hash-ul de confirmare pentru ștergerea codului. YAGNI este un element de bază al programării agile. Cea mai bună referință pentru acest lucru este Extreme Programming Explained, de Kent Beck.

2. Testele nu au nevoie de testare. Infrastructura, cadrele și bibliotecile pentru testare au nevoie de teste. Nu testați browserul sau bibliotecile externe decât dacă este cu adevărat necesar. Testați codul pe care îl scrieți, nu codul altora.

3. A treia oară când scrieți aceeași bucată de cod este momentul potrivit pentru a o extrage într-un ajutor de uz general (și a scrie teste pentru el). Funcțiile de ajutor în cadrul unui test nu au nevoie de teste; atunci când le desprindeți și le reutilizați, acestea au nevoie de teste. Până la a treia oară când scrieți un cod similar, aveți tendința de a avea o idee clară despre forma problemei de uz general pe care o rezolvați.

4. Când vine vorba de proiectarea API (API cu interfață externă și API obiect): Lucrurile simple ar trebui să fie simple; lucrurile complexe ar trebui să fie posibile. Proiectați mai întâi pentru cazul simplu, de preferință cu configurație sau parametrizare zero, dacă acest lucru este posibil. Adăugați opțiuni sau metode API suplimentare pentru cazuri de utilizare mai complexe și mai flexibile (pe măsură ce sunt necesare).

5. Eșuați rapid. Verificați intrările și eșuați în cazul intrărilor fără sens sau al stării invalide cât mai devreme posibil, de preferință cu o excepție sau un răspuns de eroare care va face ca problema exactă să fie clară pentru apelant. Permiteți totuși cazuri de utilizare „inovatoare” a codului dvs. (de exemplu, nu faceți verificare de tip pentru validarea intrărilor decât dacă este cu adevărat necesar).

6. Testele unitare testează unitatea de comportament, nu unitatea de implementare. Schimbarea implementării, fără a schimba comportamentul sau fără a fi nevoie să schimbați vreunul dintre testele dvs. este obiectivul, deși nu este întotdeauna posibil. Așadar, acolo unde este posibil, tratați obiectele de testare ca pe niște cutii negre, testând prin API-ul public fără a apela metode private sau a interveni în stare.

Pentru unele scenarii complexe – cum ar fi testarea comportamentului pe o anumită stare complexă pentru a găsi o eroare obscură – acest lucru ar putea să nu fie posibil. Scrierea testelor mai întâi ajută foarte mult în acest sens, deoarece vă obligă să vă gândiți la comportamentul codului dvs. și la modul în care îl veți testa înainte de a-l scrie. Testarea mai întâi încurajează unitățile de cod mai mici și mai modulare, ceea ce înseamnă, în general, un cod mai bun. O bună referință pentru a începe cu abordarea „testare mai întâi” este Test Driven Development by Example, de Kent Beck.

7. Pentru testele unitare (inclusiv testele de infrastructură de testare), toate căile de cod ar trebui testate. O acoperire de 100% este un bun punct de plecare. Nu puteți acoperi toate permutările/combinațiile posibile ale stării (explozie combinatorie), deci acest lucru necesită atenție. Numai dacă există un motiv foarte bun ar trebui să se lase căile de cod netestate. Lipsa de timp nu este un motiv bun și sfârșește prin a costa mai mult timp. Printre posibilele motive bune se numără: cu adevărat imposibil de testat (în orice mod semnificativ), imposibil de atins în practică sau acoperit în altă parte într-un test. Codul fără teste este o responsabilitate. Măsurarea acoperirii și respingerea PR-urilor care reduc procentajul de acoperire este o modalitate de a vă asigura că faceți progrese treptate în direcția corectă.

8. Codul este dușmanul: poate merge prost și are nevoie de întreținere. Scrieți mai puțin cod. Ștergeți codul. Nu scrieți cod de care nu aveți nevoie.

9. Inevitabil, comentariile de cod devin minciuni în timp. În practică, puțini oameni actualizează comentariile atunci când lucrurile se schimbă. Străduiți-vă să vă faceți codul lizibil și autodocumentat prin bune practici de numire și un stil de programare cunoscut.

Codul care nu poate fi făcut evident – lucrând în jurul unui bug obscur sau a unei condiții improbabile, sau a unei optimizări necesare – are nevoie de comentarii. Comentați intenția codului și de ce face ceva, mai degrabă decât ceea ce face. (Apropo, acest punct particular despre faptul că comentariile sunt minciuni este controversat. Eu încă mai cred că este corect, iar Kernighan și Pike, autorii cărții The Practice of Programming, sunt de acord cu mine.)

10. Scrieți în defensivă. Gândiți-vă întotdeauna la ceea ce poate merge prost, la ceea ce se va întâmpla în cazul unei intrări invalide și la ceea ce ar putea eșua, ceea ce vă va ajuta să prindeți multe erori înainte de a se întâmpla.

11. Logica este ușor de testat unitar dacă este fără stare și fără efecte secundare. Împărțiți logica în funcții separate, mai degrabă decât să amestecați logica în cod cu stare și plin de efecte secundare. Separarea codului cu stare și a codului cu efecte secundare în funcții mai mici face ca acestea să fie mai ușor de simulat și de testat unitar fără efecte secundare. (Mai puțină suprasolicitare pentru teste înseamnă teste mai rapide.) Efectele secundare au nevoie de testare, dar testarea lor o singură dată și simularea lor peste tot în rest este, în general, un model bun.

12. Globalele sunt rele. Funcțiile sunt mai bune decât tipurile. Obiectele sunt probabil mai bune decât structurile complexe de date.

13. Utilizarea tipurilor încorporate în Python – și a metodelor acestora – va fi mai rapidă decât scrierea propriilor tipuri (cu excepția cazului în care scrieți în C). Dacă performanța este un aspect de luat în considerare, încercați să vă gândiți cum să folosiți tipurile încorporate standard mai degrabă decât obiectele personalizate.

14. Injectarea dependențelor este un model de codare util pentru a fi clar cu privire la care sunt dependențele dvs. și de unde provin acestea. (Faceți ca obiectele, metodele și așa mai departe să primească dependențele lor ca parametri, mai degrabă decât să instanțieze ele însele obiecte noi). Acest lucru face ca semnăturile API să fie mai complexe, deci este un compromis. Faptul că vă treziți cu o metodă care are nevoie de 10 parametri pentru toate dependențele sale este un semn bun că, oricum, codul dvs. face prea multe. Articolul definitiv despre injectarea dependențelor este „Inversion of Control Containers and the Dependency Injection Pattern”, de Martin Fowler.

15. Cu cât trebuie să faceți mai multe simulări pentru a vă testa codul, cu atât codul dvs. este mai prost. Cu cât mai mult cod trebuie să instanți și să pui la punct pentru a putea testa un anumit comportament, cu atât mai rău este codul tău. Scopul este reprezentat de unități mici testabile, împreună cu teste de integrare și funcționale de nivel superior pentru a testa dacă unitățile cooperează corect.

16. API-urile orientate spre exterior sunt cele în care „proiectarea din față” – și luarea în considerare a viitoarelor cazuri de utilizare – contează cu adevărat. Schimbarea API-urilor este o pacoste pentru noi și pentru utilizatorii noștri, iar crearea unei incompatibilități retroactive este oribilă (deși uneori este imposibil de evitat). Proiectați cu atenție API-urile orientate spre exterior, respectând în continuare principiul „lucrurile simple ar trebui să fie simple”.

17. Dacă o funcție sau o metodă trece de 30 de linii de cod, luați în considerare despărțirea acesteia. O dimensiune maximă bună a unui modul este de aproximativ 500 de linii. Fișierele de testare tind să fie mai lungi de atât.

18. Nu faceți lucrări în constructorii de obiecte, care sunt greu de testat și surprinzătoare. Nu puneți cod în __init__.py (cu excepția importurilor pentru namespacing). __init__.py nu este locul în care programatorii se așteaptă în general să găsească cod, deci este „surprinzător”.

19. DRY (Don’t Repeat Yourself) contează mult mai puțin în teste decât în codul de producție. Lizibilitatea unui fișier de test individual este mai importantă decât mentenabilitatea (ruperea bucăților reutilizabile). Acest lucru se datorează faptului că testele sunt executate și citite individual, mai degrabă decât să fie ele însele parte a unui sistem mai mare. Evident, repetiția excesivă înseamnă că pot fi create componente reutilizabile pentru comoditate, dar este o preocupare mult mai puțin importantă decât în cazul producției.

20. Refaceți ori de câte ori vedeți nevoia și aveți ocazia. Programarea se bazează pe abstracțiuni, iar cu cât abstracțiunile dvs. sunt mai apropiate de domeniul problemei, cu atât codul dvs. este mai ușor de înțeles și de întreținut. Pe măsură ce sistemele se dezvoltă organic, ele trebuie să își schimbe structura pentru cazul de utilizare în expansiune. Sistemele își depășesc abstracțiile și structura, iar faptul de a nu le schimba devine o datorie tehnică care este mai dureroasă (și mai lentă și mai plină de erori) de rezolvat. Includeți costul eliminării datoriei tehnice (refactorizare) în estimările pentru activitatea de elaborare a caracteristicilor. Cu cât lăsați mai mult timp datoria, cu atât mai mare este dobânda pe care o acumulează. O carte excelentă despre refactorizare și testare este Working Effectively with Legacy Code, de Michael Feathers.

21. Faceți codul corect în primul rând și rapid în al doilea rând. Când lucrați la probleme de performanță, faceți întotdeauna profilul înainte de a face corecturi. De obicei, gâtul de gâtuială nu este chiar acolo unde ați crezut că este. Scrierea unui cod obscur pentru că este mai rapid merită doar dacă ați făcut profilul și ați dovedit că într-adevăr merită. Scrierea unui test care exersează codul pe care îl profilați cu o sincronizare în jurul acestuia face mai ușor să știți când ați terminat și poate fi lăsat în suita de teste pentru a preveni regresiile de performanță. (Cu observația obișnuită că adăugarea de cod de temporizare modifică întotdeauna caracteristicile de performanță ale codului, ceea ce face ca munca de performanță să fie una dintre cele mai frustrante sarcini.)

22. Testele unitare mai mici, cu o sferă de cuprindere mai restrânsă, oferă informații mai valoroase atunci când eșuează – ele vă spun în mod specific ce este greșit. Un test care se ridică la jumătate din sistem pentru a testa comportamentul necesită mai multe investigații pentru a determina ce este greșit. În general, un test a cărui execuție durează mai mult de 0,1 secunde nu este un test unitar. Nu există un test unitar lent. Cu teste unitare bine delimitate care testează comportamentul, testele dvs. acționează ca o specificație de facto pentru codul dvs. În mod ideal, dacă cineva dorește să vă înțeleagă codul, ar trebui să poată apela la suita de teste ca „documentație” pentru comportament. O prezentare excelentă privind practicile de testare unitară este Fast Test, Slow Test, de Gary Bernhardt:

23. „Not Invented Here” nu este atât de rău pe cât spune lumea. Dacă noi scriem codul, atunci știm ce face, știm cum să îl întreținem și suntem liberi să îl extindem și să îl modificăm după cum credem de cuviință. Acest lucru respectă principiul YAGNI: avem cod specific pentru cazurile de utilizare de care avem nevoie, mai degrabă decât cod cu scop general care are complexitate pentru lucruri de care nu avem nevoie. Pe de altă parte, codul este dușmanul, iar a deține mai mult cod decât este necesar este rău. Luați în considerare compromisul atunci când introduceți o nouă dependență.

24. Proprietatea partajată a codului este obiectivul; cunoașterea silozată este rea. Cel puțin, acest lucru înseamnă discutarea sau documentarea deciziilor de proiectare și a deciziilor importante de implementare. Revizuirea codului este cel mai prost moment pentru a începe discutarea deciziilor de proiectare, deoarece inerția de a face schimbări radicale după ce codul a fost scris este greu de depășit. (Desigur, este totuși mai bine să subliniezi și să schimbi greșelile de proiectare la momentul revizuirii decât niciodată.)

25. Generatoarele sunt grozave! Sunt în general mai scurte și mai ușor de înțeles decât obiectele cu stare pentru iterație sau execuție repetată. O bună introducere în generatoare este „Generator Tricks for Systems Programmers”, de David Beazley.

26. Să fim ingineri! Să ne gândim la proiectare și să construim sisteme robuste și bine implementate, mai degrabă decât să creștem monștri organici. Totuși, programarea este un act de echilibru. Nu construim întotdeauna o rachetă. Excesul de inginerie (arhitectura de ceapă) este la fel de dureros de lucrat ca și codul subproiectat. Aproape orice lucru scris de Robert Martin merită citit, iar Clean Architecture: A Craftsman’s Guide to Software Structure and Design este o resursă bună pe această temă. Design Patterns este o carte clasică de programare pe care orice inginer ar trebui să o citească.

27. Testele care eșuează intermitent erodează valoarea suitei dvs. de teste, până la punctul în care, în cele din urmă, toată lumea ignoră rezultatele rulării testelor pentru că întotdeauna există ceva care eșuează. Corectarea sau ștergerea testelor care eșuează intermitent este dureroasă, dar merită efortul.

28. În general, în special în teste, așteptați o schimbare specifică, mai degrabă decât să dormiți pentru o perioadă arbitrară de timp. Dormirile voodoo sunt greu de înțeles și vă încetinesc suita de teste.

29. Întotdeauna vedeți că testul dvs. eșuează cel puțin o dată. Puneți o eroare deliberată și asigurați-vă că eșuează, sau rulați testul înainte ca comportamentul testat să fie complet. Altfel, nu știți că testați cu adevărat ceva. Este ușor să scrieți din greșeală teste care de fapt nu testează nimic sau care nu pot eșua niciodată.

30. Și, în final, un punct pentru management: O continuă măcinare de caracteristici este un mod teribil de a dezvolta software. Dacă nu lăsați dezvoltatorii să fie mândri de munca lor, vă asigurați că nu veți obține ce e mai bun de la ei. Faptul că nu abordați datoria tehnică încetinește dezvoltarea și are ca rezultat un produs mai prost și mai plin de erori.

.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *