Oda testelor de integrare
22 martie 2020.
A avut loc pe 17 octombrie 2019, Training360 Verificați hype din spatele întâlnirii sale cu dezvoltatorii. În aceasta am ținut o prelegere intitulată Difficulties of Integration Tests (în Java), deși am discutat mai degrabă pozitivele testelor de integrare.
Atenţie! Următoarea postare conține elemente care vă pot perturba liniștea sufletească. Scopul meu este să subliniez că, uneori, trebuie să punem la îndoială afirmații și elemente de bază, cum ar fi piramida de testare. Prin urmare, în postare este posibil să vă confruntați cu o schimbare a accentului, vi se cere să gestionați acest lucru în loc.
În postare, voi parcurge piramida de testare și conceptele și chiar rezervele mele cu privire la aceasta. Voi lua în considerare apoi o abordare alternativă, care se aplică în special microserviciilor. Între timp, voi da și exemple de testare a unei aplicații simple Spring Boot. Exemplul de proiect este disponibil pe GitHub.
Piramida de testare
Piramida de test a fost introdusă de Mike Cohn în cartea sa Succesing with Agile pentru a vizualiza modul de plasare a diferitelor niveluri de testare.
La cel mai scăzut nivel sunt testele unitare, care testează cea mai mică unitate a unui limbaj de programare dat, în cazul limbajelor orientate obiect, acesta este nivelul clasei. La nivelul mediu se află testele de integrare, care testează deja cooperarea departamentelor. În cele din urmă, nivelul superior este testele end-to-end, cu care testăm întreaga aplicație într-un mediu dat, integrat cu dependențele lor. În plus, nu este o funcționalitate smulsă, ci un proces de afaceri complet de la început până la sfârșit.
Forma piramidei de testare provine din faptul că, de la bază, testele funcționează din ce în ce mai mult, sunt din ce în ce mai consumatoare de resurse pentru întreținere și funcționare, și de aceea merită să scrieți le deplasând în sus.
Din păcate, acest lucru îmi ridică deja mai multe întrebări. Pe de o parte, conceptele nu sunt clar definite, toți ceilalți înseamnă diferit. Nu mai este pe deplin clar ce părți ale unei aplicații sunt numite. Clasele sunt la cel mai scăzut nivel, putem fi de acord asupra acestui lucru, dar care sunt nivelurile superioare? Acestea se numesc module (de exemplu, carte de arhitectură a aplicațiilor Java, OSGi), componente (de exemplu, carte de arhitectură curată, care este foarte opusă, de exemplu, Spring Framework/Java EE, unde o componentă este un bean), pluginuri etc. Chiar și pentru aplicație, se folosesc nume diferite, cum ar fi sistem, serviciu etc. Cartea Clean Architecture and microservices numește aplicația un serviciu și este confuz pentru mine, deoarece Spring Framework apelează și fasole în stratul de logică de afaceri într-o arhitectură pe trei niveluri. Voi folosi clasa (și da, în acest caz interfețele, enumerările, adnotările etc.), modulul, numele aplicațiilor.
În testarea unitară, este clar că dependențele externe trebuie să fie batjocorite. Da, dar o clasă poate folosi o mulțime de elemente în biblioteca de clase Java SE, cum ar fi. șirul, lista etc. Sunt aceste dependențe externe? Evident că nu, deci putem spune că nu ne batem joc de acestea. Ce zici de acest lucru în cazul bibliotecilor externe care implementează structuri de date similare, cum ar fi de ex. colecțiile Guava sau Apache Commons? Și ce zici de propriile noastre clase similare, obiecte de valoare? Cum rămâne cu dependențele externe, de ex. pentru a conecta SLF4J? Poate fi apelat un test unitar la pornirea unui container, de ex. Cadrul de primăvară sau o parte din el? (Testul de unitate Spring Framework este atunci când testați o componentă, dar porniți anumite dispozitive Springes, un container mai mic.) Unde se trasează granița?
În cazul testului de integrare, probabil că întrebarea este mai mică, deoarece pentru practic orice test care include mai multe clase, putem trage markerul testului de integrare. Există un pic de confuzie în numele că testele în care integrăm mai multe aplicații și examinăm interacțiunea lor se mai numesc și teste de integrare.
Există, de asemenea, destul de multe întrebări despre testele E2E. Include doar teste de suprafață? Sau testele API pot fi incluse aici atunci când o altă interfață a aplicației, de ex. Ne adresăm serviciului web REST. Se poate numi test E2E dacă testăm o singură subfuncție prin interfață? Testăm complet izolat de alte aplicații, sau punctul E2E înseamnă să ne integrăm cu alte aplicații?
În plus, astfel de concepte apar ca test de service, test de componentă, test de sistem, ce înseamnă?
Cred că este deja clar din aceasta că problema de bază în acest domeniu este că nu există o terminologie bună, suficient de exactă, alții înțeleg aceleași concepte diferit. Mai mult, proliferarea arhitecturii microserviciilor a confundat acest lucru un pic mai mult, iar terminologia care oricum nu se dezvoltase nu se putea adapta noilor metode.
Testarea automată și instrumentele care o însoțesc (hamul corpului) sunt atât de esențiale încât trebuie să facă parte din arhitectură și să fie astfel proiectate. Toate arhitecturile obișnuite menționează acest lucru, cum ar fi. arhitectură hexagonală, arhitectură de ceapă și arhitectură curată. Cu toate acestea, încă văd că testarea este tratată complet independent de mulți, în multe locuri de către o echipă separată care nici măcar nu primește sprijin pentru a-și face treaba.
Îndoieli cu privire la testarea unitară
În exemple, voi arăta o aplicație (care se evidențiază și ca un microserviciu) care este o aplicație Spring Boot cu trei niveluri care ține evidența datelor orașului. Nu am implementat frontend-ul JavaScript, este disponibil pe API-ul REST. Sub baza de date H2. Cunoașteți coordonatele unui oraș. Folosind algoritmul Haversine, acesta calculează și returnează distanța față de Budapesta. De asemenea, returnează temperatura măsurată în oraș, folosind un serviciu extern (Ora).
Cred că știm cu toții promisiunile testării unitare. Cu toate acestea, pentru discuții suplimentare, merită explorate două abordări ale testării unitare:
- Bazat pe stare: obținem ieșirea așteptată pentru intrarea corespunzătoare
- Bazat pe comportament: a lucrat cu clasele potrivite în mod corect: ne uităm la dependențele batjocorite pentru a vedea dacă au fost invitate corect
Cu toate acestea, criticile aduse testelor unitare încep să apară și în aceste zile. În primul rând, dacă urmăm modelul de a crea o clasă de testare separată pentru fiecare clasă și cel puțin o metodă de testare pentru fiecare metodă publică, testele noastre vor fi fin granulate și dacă vrem să facem o refactorizare mai mare, asta înseamnă o mulțime de cazurile de testare vor fi afectate, ducând la problema testului fragil. De fapt, această metodă este utilizată pentru a testa detaliile implementării.
Luați în considerare următoarea clasă de controlere, pe care utilitatea testului unitar nu este pe deplin clară.
Deoarece are o dependență de serviciu, trebuie înlocuit cu un simulator. Ceea ce putem testa este dacă ceea ce serviciul returnează în mod corespunzător returnează (stare) și îl apelează înapoi la serviciu cu parametrul (comportamentul) corespunzător. Cu toate acestea, amândouă nu sunt necesare, deoarece testează dacă pot apela o metodă. Ce merită testat aici de ex. dacă adnotările sunt plasate corect, sunt disponibile pe o adresă URL bună, parametrii sunt citiți bine, detaliile orașului sunt serializate corect în JSON, codul de stare HTTP este bun etc.
Acest lucru este posibil în Spring Boot folosind @WebMvcTest, care pornește doar stratul controlerului, iar stratul de serviciu trebuie să fie batjocorit și numit și test de unitate, deoarece testează un controler, dar pornește Spring. Prin urmare, pentru mine aparține mai mult stratului de integrare.
Să ne uităm la stratul de logică de afaceri, serviciu. Situația este mai complicată aici.
Ceea ce atrage atenția mai întâi este că are o ramură pe de o parte și, pe de altă parte, colectează date din mai multe surse. Pe de o parte, acesta încarcă coordonatele orașului din baza de date și calculează distanța față de un alt oraș folosind un alt serviciu, HaversineCalculator și, în al treilea rând, obține temperatura utilizând TemperatureGateway. Necesitatea unui test de unitate poate fi apoi explicată.
Și aici merită să scrieți un test unitar pentru cazuri precum:
- Dacă nu găsești acel oraș
- Ce se întâmplă dacă orașul din care măsurăm distanța nu este situat
- Ce se întâmplă dacă apelul de serviciu extern lansează o excepție
Acestea pot fi găsite în exemplu.
Există, de asemenea, întrebări despre testarea unității de strat persistent. În majoritatea cazurilor, acestea sunt apeluri simple către obiectele JDBC adecvate (DataSource, Connection etc.), JdbcTemplate sau EntityManager. Am rezerve dacă merită batjocorite. Pentru Spring Data JPA, tot ce trebuie să faceți este să scrieți interfața și aceasta este implementată de cadrul în sine, deci este interesant modul în care acestea pot fi testate pe unitate. Aici, în cazul JPA, adnotările și interogările vin din nou în prim plan, ceea ce ar fi bine de testat, dar nu este posibil cu testul unitar.
Spring Boot are, de asemenea, o soluție pentru acest lucru cu adnotarea @DataJpaTest, numită și test de unitate, pentru a testa un depozit, dar pe lângă pornirea Spring, pornește și o bază de date încorporată (de exemplu, H2). Prin urmare, pentru mine aparține și stratului de integrare.
Responsabil pentru comunicarea cu alte sisteme, așa-numitul. testarea claselor gateway este din nou discutabilă. Aici, în funcție de protocol, suntem siguri că vom folosi un fel de bibliotecă terță parte, fără de care nu merită neapărat testată.
Să vedem exemplul că Vizualizarea timpului se numește cu jsoup. Construiește o bibliotecă terță parte, precum și o conexiune http și convertește structura de date returnată în propria sa structură.
Dintre acestea, testarea primelor două face cu siguranță parte din testarea integrării.
Aplicația externă la care ne conectăm poate fi ușor deblocată, există mai multe instrumente pentru aceasta, de ex. WireMock sau MockServer. Acestea pot fi rulate ca servere http separate (ambele fiind, de asemenea, integrate cu JUnit, desigur) și puteți specifica ce răspuns la o cerere (de exemplu, html, json etc.) să reveniți. În acest fel, este gestionată și întreaga stivă http. Utilizarea lor nu este utilă doar dacă o dezvoltăm în așa fel încât aplicația aferentă să nu fie gata sau să nu fie disponibilă în timpul dezvoltării, dar și ramurile de eroare pot fi testate foarte bine, de ex. ce se întâmplă dacă aplicația externă nu răspunde sau doar răspunde încet, returnează un răspuns incorect etc. Un caz de testare cu ambele poate fi găsit în exemplul de aplicație.
Îndoieli cu privire la testarea E2E
Testarea E2E face obiectul celor mai multe critici, deoarece este necesară o resursă intensă pentru a fi executată și întreținută. Din această cauză, primim și feedback despre desfășurarea testelor relativ târziu. Prin urmare, mențineți numărul lor scăzut.
Cartea Clean Architecture afirmă că GUI este un strat fragil, care se schimbă adesea, așa că ar trebui să depindem de el cât mai puțin posibil. Pentru multe teste de suprafață, din nou, putem întâlni doar fenomenul Problemă de testare fragilă.
Dacă testele E2E sunt interpretate în sensul că aplicația este legată de o altă aplicație în timpul testelor, atunci provocarea este și mai mare. Acest lucru se datorează faptului că trebuie furnizate aplicații externe în versiunea corectă și în starea potrivită și cu minimul de resurse umane posibile. Imaginați-vă acest lucru pentru până la zeci de microservicii (ceea ce este puțin probabil fără tehnologia de containerizare și orchestrare). Și atunci nici nu am vorbit despre cum să lansăm din diferite aplicații din acest mediu. Și este doar un mediu de testare.
Nu există nicio îndoială cu privire la importanța testării E2E, dar merită menținută cantitatea scăzută. Cu siguranță recomand doar testarea principalelor funcționalități comerciale care „generează bani”. Aș dori să menționez încă o direcție aici. Cei care și-au dat seama cât de dificil sau costisitor este să creezi un astfel de mediu de testare, care este, de altfel, o replică a unui mediu live, au inventat conceptul de testare live. Evident, acest lucru este posibil doar pentru anumite aplicații. Este o condiție prealabilă să aveți o monitorizare profesională și să puteți detecta imediat erorile, precum și să puteți reveni imediat și automat la o versiune anterioară în cazul unei erori. Un concept binecunoscut aici este implementarea Blue-Green, în care versiunile vechi și noi trăiesc în paralel și pot fi reduse în orice moment. La fel ca și versiunea Canary, când noua versiune este adusă la viață doar de un cerc restrâns de utilizatori la un moment dat.
Testarea fagurelui
Spotify recomandă testarea fagurelui în mod special pentru microservicii. Aceasta înseamnă scrierea la maximum a testelor de integrare.
Cartea Clean Architecture sugerează, de asemenea, că nu ar trebui să fim atât de forțați să folosim teste unitare, deoarece testează detaliile implementării și sunt dificil de întreținut.
(Și-a luat numele din faptul că forma sa seamănă cu celulele de splină hexagonale din stup).
Testele de integrare au următoarele avantaje:
- Independent de detaliile implementării, dacă ne bazăm pe API, un refractor intern nu va sparge testele.
- Ele pot fi utilizate pentru a verifica piesele care nu pot fi acoperite de teste unitare, cum ar fi stratul controlerului pentru serializarea JSON, maparea URL-urilor sau stratul depozitului pentru integrarea bazei de date.
- Stratul gateway poate fi testat și prin batjocorirea sistemelor externe. Cu toate acestea, sistemele externe nu trebuie instalate sau integrate.
- Cu cea mai mică cantitate de muncă, obținem cea mai mare acoperire.
- Sunt mai rapide decât testele E2E.
Desigur, există și multe întrebări la aplicarea testelor de integrare. Întrebarea de bază este ce gamă de clase testăm cu testul de integrare. După cum am menționat, poate fi doar controlerul, depozitul, gateway-ul, dar dacă dorim un test semnificativ, acestea sunt deja incluse în testele de integrare.
Următorul pas ar putea fi să batjocorească clasele asociate cu resurse externe. Exemple sunt CityRepository, care este legată de o bază de date, și TemperatureGateway, care este legată de Time. Testul asociat este InMemoryCityIT, care conduce atât clasele CityController, cât și CityService.
Următorul pas este să rulați aplicația cu biblioteca terță parte asigurată de REST, baza de date este un H2 încorporat, iar TemperatureGateway este conectat la un server http încorporat implementat cu WireMock.
Dacă doriți să separați în continuare aplicația dvs. de cadre, lansați aplicația separat, care este conectată la un REST asigurat care rulează într-un proces separat, baza de date a acestuia este o bază de date reală și la un server WireMock care rulează într-un proces separat pentru temperatură. date.
rezumat
Nu există o terminologie precisă, bine stabilită pentru testare, și foarte puține rețete bine stabilite. Pentru o lungă perioadă de timp, am crezut că piramida test nu poate fi greșită, dar și-a arătat punctele slabe. Se pare că, în unele cazuri, testele de integrare încep să preia roluri din testele unitare și, de asemenea, din testele E2E datorită pornirii rapide și a dispozitivelor încorporabile. Testele unitare sunt încă foarte importante, dar le folosim acolo unde chiar are sens, nu este neapărat bine să se realizeze 90% acoperire doar cu testele unitare.
Testarea este foarte importantă, o tratăm ca parte a arhitecturii și o proiectăm cu aceeași minuțiozitate. Dintre căile prezentate, alegeți-o pe cea care se potrivește cel mai bine aplicației dvs. și revedeți periodic decizia noastră. Nu întotdeauna se întâmplă ceilalți și să ne schimbăm dacă simțim că energia investită în testele automate nu dă roade.
Ca fanatic Java, dezvolt, predez, blog, organizez evenimente și particip la conferințe. Am absolvit Universitatea din Debrecen ca matematician în proiectarea programelor, în prezent predând la Training360.
JTechLog este un blog de stil casual despre complexitățile limbajului și platformei Java.
- O dietă echilibrată în timpul sarcinii
- Dacă mă antrenez, pot mânca la fel de mult ca Magazine și Lifestyle Center
- Sensibilitate albă ca zăpada
- Nutriție pentru vârstnici - sfaturi pentru persoanele peste 65 de ani - Kiskegyed
- Măsurarea consumului de căldură - Renovarea panoului plat