Cât de mult mock-uiți teste unitare?

Am răsfoit recent această carte. Tl;dr conține strategii bune cu ajutorul cărora poți refactoriza cod legaly bazându-te pe teste unitare.

Sunt multe de învațat dar subiectul acestei postări vine din următoarea afirmație: testele unitare trebuie să ruleze rapid, altfel nu sunt teste-unitare.

Ideea ar fi că testele trebuie să termine în căteva minuțele maxim, astfel ele devin un tool foarte puternic pentru dezvoltator. Ideal ar fi dacă ar rula atât de rapid cât să poată fi pornite după fiecare save executat pe un fișier.

Din acest motiv folosirea mock-urilor este esențială. Întrebarea este cât de mult mock-uim?

Pe unul dintre proiectele la care lucrez testele unitare (vreo 2000) rulează în ~20 minute pe un laptop cu 8gb ram (cu 16gb ram totul se termină în 10-15 minute). Enorm de mult aș spune. Cauza: testele unitare chiar folosesc dependințele proiectului pentru a rula: crează/șterge useri și alte obiecte în baza de date, pune task-uri în queue-ul de rabbitmq, lansează task-uri asyncrone (Celery), trimite mail-uri, generează fișiere, scrie și citește pe disk.

Cei care au scris aceste teste motivează prin faptul că vor să fie siguri că toate acele dependințe funcționează.

Eu unul aș mock-uii cât mai mult. Django (framework-ul folosit) este testat deja de comunitate, nu consider necesare testele unitare pentru validatori de form-uri sau ORM. Pentru restul dependințelor un healt-check API este perfect.

Nici nu înțeleg de ce trebuie să trimită mail-uri. Este suficient să se știe că a fost invocată metoda care trimite mail, dacă acel API/server are probleme asta este altă mâncare de pește.

Păreri? testăm totul la sânge sau doar logica proprie a aplicației?

Eu aș spune că alea nu sunt teste unitare ci de integrare. Dependențele (se presupune că) au propriile teste unitare.

5 Likes

@iamntz are dreptate, astea sunt integration tests. Acum ar trebui sa faci un proiect nou pentru unit tests deoarece vrei sa le rulezi separat si sa le poti identifica usor.

Legat de teste de integrare, noi folosimo baza de date dedicata.
Fara a o polua pe cea principala, a aplicatiei.
Business logic trebuie testat. In opinia mea este partea de valoare adaugata a aplicatiei. Pt unit teste, noi mockuim baza de date si conexiunea.
Sper sa te ajute ce am scris.

Eu le numesc “unitare” pentru ca așa au fost botezate dar da, sunt de integrare mai mult.

@thewindev eu mă gândeam să le rescriu dar pană la urmă au și ele un rost și le pot păstra și așa. Merci de idee!

@Cosmin_Popescu partea mișto este că atunci când testele astea sunt pornite se creează automat o bază de date dummy după schema din proiect. La final de teste baza este ștearsă automat.

OTOH, Mai bine nu ai teste de integrare decât să le ai pe baza de date reală :smiley:

1 Like

Am alta baza de date pt teste de integrare :smiley:

1 Like

In Laravel folosesc pentru teste o baza de date SQLite stocata in memorie.
N-am avut nici o problema pana acum.

In cazul unit tests se mockuieste totul si se pot executa in paralel ca nu sunt dependinte sau un state de care sa se tina cont.

Pentru mine castigul cel mai mare cu acest tip de teste este ca obliga dezvoltatorul sa gandeasca bucata de cod de la inceput testabila a.k.a. SOLID. Daca nu se respecta principiile respective scrierea de unit teste devine un exercitiu super frustrant.

Un alt avantaj este ca poti testa situatii care apar foarte rar in productie, de exemplu sa simulezi ca o conexiune la api merge ok si a doua sa dea fail, sau sa dea timeout etc… aceste tipuri de situatii poti testa foarte bine cu mocks si mai putin cu teste de integrare (code coverage > 90%).

2 Likes

E o intreaga dezbatere referitoare la limita care defineste “unit testing” (eu aud prima data “teste unitare”; “unitar”, desi are aceeasi radacina cred ca inseamna altceva; daca ar fi sa traducem poate ar fi “testare pe unitati/unitate”?)

In proiectul nostru noi apelam foarte mult un API extern (OpenStack) si atunci nu avem ce testa acolo cu unit testing.

Avem cateva locuri unde putem face unit testing pur sange, cum ar fi calcule legate de billing. Dar acestea sunt foarte putine.

Si atunci facem de fapt mult integration test. Pe noi ne intereseaza sa testam cu diverse versiuni de OpenStack si sunt relevante pentru noi testele reale pe un API real cu o instalare de OpenStack. Astfel ne asiguram ca functioneaza codul nostru cu diverse versiuni si deployment-uri de OpenStack.

Pe backend (Django Rest Framework) avem in total 625 de teste (unit + integration) si ruleaza tot in Gitlab in 7 minute. Cu tot setup: containerul docker pe care il face Gitlab, instalat pachete Python (aici facem ceva cache), migrari etc.

Mai ruleaza automat si testele de coding style (flake8), dar astea sunt rapide.

La fiecare commit pe branch de development ruleaza testele si, optional, se pot buildui si descarca pachete rpm si deb cu aplicatia:

Pe master avem teste sumare acum, se fac pachetele automat si se face automat deploy pe staging (de fapt e un server de dev, e impropriu numit staging).

Mockuim destul de putin:

  • unde ne concetram pe partea de billing si nu putem testa izolat de API, dar nici nu ne intereseaza API call-uri catre OpenStack
  • trimitere email-uri
  • ceva setari supra scrise
  • datetime-ul pentru un bug legat de miezul noptii si time zone
  • am mai mocuit de curand logging.error pentru ca am vrut output cat mai curat dupa teste ca sa poti vedea mai usor cand sunt probleme cu adevarat. Si cand fortezi diverse situatii extreme e normal sa apara erori in log. Am facut o mica clasa callable si am putut ascunde doar erorile care sunt asteptate, plus le-am transformat in teste avand asertiuni ca (1) s-au logat erorile asteptate si (2) nu au fost alte erori pe care nu le asteptam

Testele pe frontend iau 20 de minute din pacate (cu toata instalatia Gitlab). 32 unit tests + 128 teste e2e cu Selenium.

Mai lucram din cand in cand sa reducem durata de rulare prin diverse optimizati si mai mutam teste e2e in integration pe backend. Testele e2e pe frontend fiind cele mai lente.

Baza de date se face de la zero pentru fiecare test. Se ocupa Django de asta.
Din pacate nu putem acum testat cu baza de date MySQL in RAM (ca avem ceva field-uri Text cu lungime nedefinita, dar rezolvabil daca modificam schema) iar cu SQLite nu merge acum sa testam pentru ca sunt 3 procese (teste, celery si un daemon care primeste notificari pe RabbitMQ de la OpenStack) care acceseaza acelasi fisier SQLite si crapa. Mai avem de mesterit aici la optimizari.

Ce vreau sa facem este sa avem mai multe niveluri de testare:

  • sa ruleze automate de la fiecare commit intr-o versiune de setup mai lite si mai rapid; de ex. sa nu mai generam cheie de licenta, poate refolosim DB si/sau reusim sa folosim MySQL in memory sau SQL Lite
  • sa avem alte teste pe master complete si in setup de productie (MySQL real, cheie licenta etc.) acestea dureaza mult si atunci sa nu fie blockere pentru un merge request; sa le rulam automat dupa un cron (noaptea si eventual la niste ore fixe ziua) plus declansate manual inainte de release-uri

Nu cred ca au mare importanta delimitarile puriste, mai important e:

  • sa (tinzi sa) ai suficiente teste ca sa iti validezi aplicatia
  • sa fie deterministice testele; asta e o provocare cand rulezi teste de integrare cu un API extern, plus isi mai baga coada un race condition (pe care inca nu l-ai rezolvat)
  • sa ruleze rapid testele

De exemplu, inteleg ca un test care trece prin ORM nu e un unit test pur, dar eu personal nu vreau sa imi bat capul cu mock pe ORM pentru ca nu e rasplatit efortul cu mare lucru.

Evident, e bine sa incerci sa iti faci codul testabil din start si sa fie functii izolate de alte componente si sa foloseasca dependency injection daca se poate.

5 Likes

E o abordare tricky IMO. Nu ai paritate intre mediul de Dev/test și producție cu bazele astea de date. Cumva te limitezi și la dialectul SQL și feature-urile comune celor două.
Noi folosim un MySQL într-un docker la munca pentru testele de integrare. E împărțit de toată suita in schimb așa că trebuie avut grija cu setup și teardown scripts.

2 Likes

Am inceput sa citesc cartea lui Roy Osherove - The Art of Unit Testing, si in primul capitol se discuta definitia unui unit test.
Postez aici cateva din chestiile pe care le-am subliniat:

  • A unit test should have the following properties:
  • It should be automated and repeatable.
  • It should be easy to implement.
  • It should be relevant tomorrow.
  • Anyone should be able to run it at the push of a button.
  • It should run quickly.
  • It should be consistent in its results (it always returns the same result if you don’t change anything between runs).
  • It should have full control of the unit under test.
  • It should be fully isolated (runs independently of other tests).
  • When it fails, it should be easy to detect what was expected and determine how to pinpoint the problem.
2 Likes

+1 testat totul la sange. Desi dureaza mult mai mult, macar ai o oarecare certitudine ca totul functioneaza cum trebuie.

Inclusiv funționalitățile framework-ului sau plugin-urilor? Astea se presupun a fi deja testate de comunitate.

E prea extrem sa faci asta, chiar daca nu ar fi testate deja de comunitate. Am facut si eu la inceput greseala sa cred ca TDD inseamna sa ai coverage 100%, dar nu-i asa.
Ian Cooper explica asta destul de bine(printre altele): https://dev.tube/video/EZ05e7EMOLM

Cand trebuie sa decid daca mock-ui sau folosesc o instanta a unei dependinte pe bune, regula mea de baza este ca daca testul ruleaza in < 5ms, e OK.
De aici rezulta ca cele mai multe teste folosesc mock pentru dependinte externe (3rd party staff like DB, email, etc) si folosesc instante reale pentru dependinte marunte. De exemplu, daca vreau sa testez clasa “Product”, care depinde de clasa “Maintenance”, si pot crea super usor o clasa “Maintenance”, atunci creez o astfel de clasa. Daca testul ruleaza sub 5 ms, sunt OK.
Lucrand mult cu Gerard Meszaros ( https://www.amazon.com/xUnit-Test-Patterns-Refactoring-Addison-Wesley-ebook/dp/B004X1D36K/ref=sr_1_1?keywords=gerard+meszaros&qid=1553750646&s=gateway&sr=8-1 ), am invatat ca utilizarea framework-urilor de mocking este bine sa fie evitat pe cat posibil. Ele intr-adevar sunt utile, dar in 80% din cazuri poti pur si simplu crea micile dependinte. Daca ai nevoie sa faci dependinte “Fake”, poti crea o clasa ce extinde dependinta si suprascrii metodele implicate in test.
Folosirea unui framework de mocking este util cand vrei sa verifici method calls fara sa ai posibilitatea de a crea usor acele dependinte.
Ca si referinta, la Syneto pe proiectul “cel mare” avem vreo 9000 de test unitare care ruleaza in aprox 1.5-2 minute.

3 Likes