Testarea e vitală în căutarea noastră de a lansa software performant și fără buguri. Pentru a îmbunătăți viteza de dezvoltare și a asigura un produs final de înaltă calitate, se depune un efort considerabil în crearea unui context unde acest software poate fi testat. Sistemele de tip pull sunt de obicei închise, având nivele reduse pentru Controlabilitate si Observabilitate, prin urmare calea spre îmbunătățirea Testabilității se face din mers.
În sistemele tradiționale (de tip push), clientul inițiază comunicarea prin lansarea unei cereri (request) către sistem. Sistemul procesează cererea și livrează un răspuns înapoi clientului.
Într-o arhitectură de tip pull, sistemul nu așteaptă să fie invocat, ci inițiază el însuși conversația prin solicitarea de sarcini de la client. După ce sarcinile sunt îndeplinite, rezultatul este livrat ca răspuns înspre client.
Abordarea de tip pull este regăsită în sistemele distribuite bazate pe evenimente sau cele proiectate să îndeplinească sarcini programate, spre exemplu procesări care durează foarte mult.
În astfel de sisteme, rolul tradițional de client (cel care inițiază comunicarea) este jucat de către sistem, iar beneficiarul rezultatului procesării este un alt sistem. Asemănător unui client tradițional, acest sistem "beneficiar" deține input data și așteaptă output data, iar de cele mai multe ori nu este mai mult decât un Data Source.
Sarcinile de procesare sunt efectuate de către componente numite workers. Ele primesc acces la datele ce trebuie procesate cu ajutorul componentelor agent, capabile să convertească nu doar date, ci și tipuri de comunicare (sincronă <-> asincronă). Progresul procesării este monitorizat de către componente tracker iar toată procesarea este inițiată de către componente trigger.
Acest tip de sistem este reprezentat mai jos, într-o diagramă de componente:
Următoarea diagramă de comunicare prezintă o perspectivă mai bună asupra comunicării între componente:
În principiu avem un sistem închis care a fost proiectat să nu accepte solicitări. La un moment dat, acest sistem trebuie testat.
Deși componentele sistemului pot fi testate în izolare (Unit, Integration), testele End to End pot interacționa doar cu Data Source. Se poate asigura că datele de intrare sunt disponibile și se verifică corectitudinea livrării datelor de ieșire (au conținutul așteptat și schimbul de mesaje a avut loc conform planului).
În stilul BDD, un astfel de scenariu poate arăta ca în următorul fragment:
Given valid <input> available in the Data Source
Given predefined <schedule>
When Data Source is checked
Then the system should have called for <input> according to <schedule>
Then the system should have delivered valid <output>
Execuția unui astfel de test are nevoie de o amplasare ca în următoarea diagramă:
Problema principală este că testele de mai sus pot fi executate doar după ce întregul sistem a fost deja dezvoltat. Dacă sistemul este dezvoltat într-o manieră Agile, se dorește a fi validat cât de timpuriu posibil.
O altă problemă este numărul limitat de variabile care compun scenariul de test (input, schedule, output). În teorie, întregul sistem poate fi testat cu un singur scenariu dacă variabilele sunt jucate corespunzător. Cu așa de puțin control Testabilitatea sistemului are de suferit. Este foarte greu de pus presiune pe sistem în așa fel încât caracteristicile sale invizibile, dar totuși puternice să fie declanșate. Aceste caracteristici sunt de obicei NFR-uri (cerințe non-funcționale), precum Toleranța la erori, Fiabilitatea, Rezistența/Elasticitatea și Robustețea.
Urmărim un mod de a îmbunătăți Testabilitatea și a face atât sistemul cât și dezvoltarea lui cât mai transparente către toate părțile implicate. Un proces transparent, chiar dacă are doar realizări mărunte, face proiectul mai ușor de urmărit din mai multe perspective: de management, aprecierea riscurilor și financiare.
Mai mult potențial pentru Testabilitate aduce mai multe oportunități și pentru membrii echipei. Acesta deschide noi moduri de a fi implicați în asigurarea calității proiectului pe care îl dezvoltă. Dintr-o perspectivă tehnică, acesta devine capabil să acomodeze tot mai multe tehnologii de ultimă oră și invită spre inovație.
Primele componente ce sunt dezvoltate sunt algoritmii de procesare. De obicei, aceștia pornesc ca programe prototip, pentru ca, ulterior, tot mai multe funcționalități sau componente de sprijin să fie adăugate în jurul lor.
Elementul central al acestui sistem constă în componentele (sau micro serviciile) worker. Are sens să le grupăm într-un subsistem și să ne asigurăm că funcționează în armonie cu toate dependențele lor de înlănțuire angajate.
Următorul pas ar fi să ne asigurăm că procesarea este monitorizată corespunzător, iar pentru aceasta adăugăm componentele (sau micro serviciile) tracker. Astfel formăm un alt subsistem.
Ultimul subsistem necesită includerea componentelor (sau micro serviciilor) trigger. Această descompunere în subsisteme este prezentată mai jos:
Din perspectiva BDD, acest sistem este cel mai simplu:
Având pregătite date de intrare predefinite și comandând procesarea pe canale specializate, subsistemul poate fi validat prin felul în care își cere datele de intrare, corectitudinea datelor de ieșire și înregistrarea evenimentelor ce sunt declanșate în timpul procesării.
Următorul fragment BDD ilustrează cum pot fi proiectate cazuri de test pentru acest subsistem:
Given <input> for when the subsystem calls on
<input_req_ch> with <input_req_params>
When processing command is sent on <command_ch> with <command_params>
Then the <input_req_ch> should be called with
<input_req_params>
Then the <output_data_ch> should be called with
<output>
Then the <log_events_ch> should be called with <events>
Acest subsistem prezintă intrări similare cu subsistemul precedent, dar verifică efectele secundare la granițele subsistemelor tracker.
Următorul fragment BDD ilustrează cum pot fi proiectate cazuri de test pentru acest subsistem:
Given <input> for when the subsystem calls on
<input_req_ch> with <input_req_params>
When processing command is sent on <command_ch>
with <command_params>
Then the <input_req_ch> should be called
with <input_req_params>
Then the <output_data_ch> should be called
with <output>
Then calling on <check_events_ch>
with <check_events_params> should return <events>
Ultimul subsistem include componentele de planificare, iar corectitudinea inițiativelor de pull poate fi validată aici, într-o configurare controlată.
Given <input> for when the subsystem calls on
<input_req_ch> with <input_req_params>
When the subsystem is configured with <schedule>
Then the <input_req_ch> should be called with
<input_req_params> according to <schedule>
Then the <output_data_ch> should be called
with <output>
Nu ne vom concentra atenția pe instrumente (cum sunt Cucumber sau Serenity BDD), ci mai degrabă pe abordări de infrastructură.
Luăm în calcul soluții care au potențial să faciliteze integrarea într-un deployment pipeline, astfel încât poate fi luată în calcul o abordare automatizată pentru Continuous Delivery.
Având o Strategie de Testare adaptată specifc pentru proiect ajută la reducerea timpului ciclului (Cycle Time) și îmbunătățește productivitatea doar având mai multă automatizare și mai puțină intervenție umană. În acest fel, putem valida soluția cât mai timpuriu posibil și revalida încontinuu cât de frecvent posibil.
Soluțiile prezentate nu sunt complet dovedite. Este nevoie de câteva proiecte realizate de echipe diferite pentru a aduna destule date și a trage concluziile finale. Avantajele și dezavantajele sunt prezentate așa cum au fost observate până acum în dezvoltarea continuă.
Cea mai evidentă soluție e și cea mai simplă și presupune câte un mediu de testare real pentru fiecare subsistem.
Desfășurarea poate fi efectuată în medii On-Premise sau Cloud, cum sunt AWS EC2 Instances sau ECS, [Azure Virtual Machines](https://azure.microsoft.com/en-us/services/virtual-machines/ ), sau AKS, CloudFoundry sau Heroku Buildpacks, ș.a.m.d.
Această abordare oferă disponibilitate, stabilitate și transparență. Subsistemele sunt prezente întotdeauna ca ținte pentru testele E2E.
Există și unele dezavantaje, cum sunt costul, complexitatea managementului pe măsură ce sistemul crește, și nevoia de personal calificat să opereze toate aceste infrastructuri.
După cum sugerează și numele, aceste medii sunt solicitate doar pe durata testelor, apoi se renunță la ele. Este foarte ușor să realizăm o soluție în medii Cloud, cu ajutorul tehnologiilor Spot și Reserved Instances, dar și să refolosim o infrastructură bazată pe containere pentru un timp, doar să rulăm un set de teste.
Cel mai mare dezavantaj aici e disponibilitatea subsistemelor în afara programului de testare. Poate fi puțin neclar pentru toate părțile implicate ce se întâmplă, ar putea considera aceste sisteme instabile. Pe de alta parte, cu toate că se poate îmbunătăți dramatic costul, complexitatea managementului crește în lipsa unei o soluții automatizate.
Pentru automatizarea creării/distrugerii unui mediu temporar, se pot folosi tehnologii IaC (Infrastracture-as-Code), cum sunt AWS CloudFormation, Azure Resource Manager, Terraform, etc.
De fapt, este cu totul posibil ca ciclul de viață al mediilor să fie integrat chiar în ciclul de viață al testelor E2E. Să luăm Java spre exemplu, unde instrumentele de testare pot fi integrate cu instrumentele de build automation, cum sunt Maven sau Gradle. Un proiect de testare E2E este un proiect de sine stătător, decuplat de restul componentelor care sunt în dezvoltare.
Pentru a executa testele, este suficient să declanșăm buildul acestui proiect E2E, iar testele vor rula ca parte a fazei test
sau verify
. Din acest punct de vedere ele nu sunt diferite de testele Unit sau Integration.
În JUnit, avem la dispoziție instrumente cum sunt @BeforeClass, @AfterClass, @ClassRule pe care le-am putea folosi să ne asigurăm că mediul nostru țintă pentru un subsistem specific e pregătit și rulează înaintea declanșării testelor. La sfârșitul testelor, aceleași instrumente ne pot ajuta să distrugem mediul de test.
În interiorul acestor metode putem face orice e nevoie, cum ar fi:
executarea de comenzi sistem pe serverul de build curent (sau un agent atașat), cum ar fi docker-compose up
și docker-compose down
;
controlul infrastructurii de la distanță, prin kubectl, terraform sau chiar ssh;
Principalul beneficiu al integrării ciclului de viață al mediului în ciclul de viață al buildului este acela că responsabilitatea menținerii mediilor cade în mâinile dezvoltatorilor de teste. În acest caz, chiar putem folosi mecanismele IaC pentru îmbunătățirea Testabilității.
Deoarece sistemele de tip pull sunt văzute din perspectiva testării în calitate de clienți, doar teste End to End la nivelul sistemului s-ar putea dovedi a fi insuficiente. Felul în care descompunem problema ne poate da o indicație pentru felul în care compunem subsistemele, iar în final ne poate ajuta să îmbunătățim testablitatea sistemului.
de Ovidiu Mățan
de Adi Popa
de Ovidiu Mățan