modul 00 / de ce automatizăm & ce automatizăm
ce rezolvă automatizarea (și ce nu)
Testarea automată nu înlocuiește testarea manuală — o completează. Dacă ai parcurs cursul de Manual QA, ai deja partea grea: știi ce să testezi. Acest curs te învață să pui acele verificări în cod, ca să ruleze singure, de mii de ori.
problema pe care o rezolvă
Imaginează o aplicație cu 200 de funcționalități. La fiecare modificare de cod ar trebui reverificate toate, ca să te asiguri că n-ai stricat nimic — testare de regresie. Manual, asta înseamnă zile de muncă repetitivă la fiecare livrare. Practic, nimeni nu o face complet, deci bug-urile de regresie ajung în producție.
| Automatizarea e bună la | Omul e mai bun la |
| Repetiție fără greșeli, la nesfârșit | Testare exploratorie, intuiție |
| Regresie rapidă la fiecare commit | Judecăți despre experiență și estetică |
| Rulare pe multe browsere/dispozitive | Situații noi, nespecificate |
| Verificări obiective, măsurabile | „Se simte ciudat aici" |
mit frecvent
„Automatizăm ca să găsim bug-uri." Nu chiar. Testele automate găsesc rar bug-uri noi — ele confirmă că ce funcționa înainte funcționează în continuare. Rolul lor e plasa de siguranță care îți permite să modifici codul fără frică. Bug-urile noi le găsește în continuare testarea exploratorie, făcută de om.
ce câștigi cu adevărat
- Feedback rapid — afli în minute, nu în zile, că o modificare a stricat ceva.
- Curaj de a refactoriza — poți rescrie cod urât pentru că testele te prind dacă greșești.
- Documentație vie — un test bine scris descrie cum ar trebui să se comporte sistemul.
- Timp eliberat — omul face testare exploratorie, nu clickuri repetitive.
de reținut
Automatizarea nu înlocuiește testerul — îi mută munca de la repetiție la gândire. Valoarea ei principală e testarea de regresie: confirmarea rapidă că modificările n-au stricat ce funcționa. Nu te aștepta să găsească bug-uri noi; asta rămâne treaba testării exploratorii. Automatizezi ca să câștigi viteză și curaj, nu ca să scapi de oameni.
modul 00 / de ce automatizăm & ce automatizăm
piramida testelor & ce merită automatizat
Nu toate testele automate sunt egale. Există o ierarhie clasică — piramida testelor — care îți spune câte teste de fiecare fel merită să ai.
cele trei niveluri
| Nivel | Ce verifică | Viteză | Cantitate |
| Unitare (bază) | O funcție/componentă izolată | Milisecunde | Multe |
| Integrare (mijloc) | Mai multe piese împreună (ex: API + bază de date) | Secunde | Moderate |
| E2E (vârf) | Fluxul complet, prin interfață, ca un utilizator real | Zeci de secunde | Puține |
Acest curs se concentrează pe vârful piramidei — testele end-to-end prin interfață, teritoriul clasic al QA-ului. Dar e esențial să înțelegi de ce vârful trebuie să fie îngust.
de ce piramida, și nu invers
Testele E2E sunt cele mai valoroase (verifică ce trăiește utilizatorul) dar și cele mai scumpe: lente, fragile, greu de depanat. Dacă ai sute de teste E2E și puține unitare, obții o „cupă de înghețată răsturnată": o suită care rulează o oră, pică aleatoriu și pe care nimeni n-o mai crede.
✅ Piramidă ❌ Anti-tipar
/E2E\ puține \ E2E / multe
/integr\ \integr/
/unitare_\ multe \_unit_/ puține
ce merită automatizat
| ✅ Automatizează | ❌ Lasă manual |
| Fluxuri critice de business (login, plată, comandă) | Teste care rulează o singură dată |
| Scenarii repetate la fiecare livrare | Zone încă instabile, în schimbare zilnică |
| Verificări cu multe combinații de date | Estetică, „arată bine?" |
| Zone unde bug-urile costă scump | Testare exploratorie |
Regula practică: automatizează întâi fluxul care, dacă se strică, oprește businessul. La un magazin: login → adaugă în coș → finalizează comanda. Zece teste bune pe fluxurile critice valorează mai mult decât o sută pe funcții marginale.
de reținut
Piramida testelor: multe unitare (rapide, ieftine), moderate de integrare, puține E2E (valoroase dar lente și fragile). Inversarea piramidei produce o suită lentă și instabilă în care nimeni nu mai are încredere. Automatizează ce e repetitiv, critic pentru business și stabil; lasă manual ce e explorator, estetic sau încă în schimbare.
modul 01 / localizarea elementelor
strategii de localizare & ierarhia lor
Un test automat trebuie întâi să găsească elementul cu care lucrează. Pare banal, dar alegerea localizatorilor e principala cauză a testelor fragile. E abilitatea care separă o suită stabilă de una care pică săptămânal.
tipuri de localizatori
| Strategie | Exemplu | Stabilitate |
| Rol + nume accesibil | buton cu textul „Trimite" | ⭐ Cea mai bună |
| Etichetă (label) | câmpul etichetat „Email" | ⭐ Foarte bună |
| Text vizibil | linkul „Despre noi" | Bună |
| data-testid | [data-testid="cos-total"] | Bună (dedicat testelor) |
| CSS pe clase de stil | .btn-primary-lg-2 | ⚠️ Fragilă |
| XPath absolut | /html/body/div[3]/div/button | ❌ Foarte fragilă |
de ce rolul și textul sunt cele mai bune
Un localizator bun descrie elementul așa cum îl percepe utilizatorul: „butonul care scrie Trimite". Dacă designerul schimbă clasa CSS sau mută butonul în altă parte a paginii, testul continuă să funcționeze — pentru că butonul e tot butonul „Trimite". Dacă însă textul se schimbă în „Salvează", testul pică — și corect, pentru că s-a schimbat ceva ce vede utilizatorul.
// Playwright — localizatori recomandați
page.getByRole('button', { name: 'Trimite' })
page.getByLabel('Email')
page.getByText('Coșul este gol')
page.getByPlaceholder('Caută produse')
page.getByTestId('cos-total')
bonus: testezi și accesibilitatea
Când localizezi după rol și nume accesibil, testul tău verifică indirect că elementul e corect marcat semantic. Dacă „butonul" e de fapt un <div> fără rol, localizatorul nu-l găsește — și ai descoperit o problemă reală de accesibilitate. Localizatorii buni fac dublă treabă.
XPath absolut — de ce e cel mai rău
/html/body/div[3]/div[2]/button descrie poziția în structură, nu elementul. Orice div adăugat undeva sus în pagină rupe toate testele care-l folosesc. E cea mai frecventă cauză de suite abandonate.
de reținut
Ierarhia localizatorilor: rol+nume accesibil > etichetă > text > data-testid > CSS > XPath. Regula: localizează elementul așa cum îl percepe utilizatorul, nu după poziția lui în DOM sau după clase de stil. Testul trebuie să pice când se schimbă comportamentul, nu când se schimbă culoarea butonului. Aceasta e cea mai importantă decizie pentru stabilitatea suitei.
modul 01 / localizarea elementelor
data-testid, selectori compuși & liste
când folosești data-testid
Uneori nu ai un rol sau un text stabil: un container fără etichetă, un element de layout, o valoare care se schimbă. Atunci adaugi un atribut dedicat testelor:
<div data-testid="cos-total">249,90 lei</div>
await expect(page.getByTestId('cos-total')).toHaveText('249,90 lei');
- Avantaj — e un contract explicit: dezvoltatorii știu că acel atribut e folosit de teste și nu-l șterg la refactorizare.
- Dezavantaj — nu verifică nimic despre ce percepe utilizatorul; e „invizibil" pentru accesibilitate.
- Regula — folosește-l ca a doua opțiune, când localizatorii semantici nu se aplică. Nu-l pune pe tot, reflex.
restrângerea: găsirea într-un context
Într-o pagină cu 20 de produse, „butonul Adaugă în coș" e ambiguu. Soluția e să restrângi căutarea la containerul potrivit:
// întâi găsesc cardul produsului, apoi butonul DIN el
const card = page.getByRole('article').filter({ hasText: 'Tastatură mecanică' });
await card.getByRole('button', { name: 'Adaugă în coș' }).click();
Acest tipar — context → element — e mult mai robust decât un selector lung și complicat care încearcă să exprime totul într-o singură expresie.
elemente multiple
const randuri = page.getByRole('row');
await expect(randuri).toHaveCount(5); // câte sunt
await randuri.first().click(); // primul
await randuri.nth(2).click(); // al treilea (de la 0)
capcană: indexul
nth(2) depinde de ordine. Dacă lista se sortează altfel sau apare un element nou, testul verifică alt lucru decât credeai — și poate trece în continuare, fals liniștitor. Preferă filtrarea după conținut (filter({ hasText: … })) în locul poziției, ori de câte ori se poate.
reguli de igienă
- Nu localiza după text care se schimbă — prețuri, date, numere de comandă.
- Evită clasele generate automat (ex: css-1x2y3z din biblioteci de stil) — se schimbă la fiecare build.
- Un selector lung și complicat e un semnal că ar trebui să restrângi contextul, nu să-l lungești.
de reținut
data-testid e a doua opțiune, pentru când nu există rol/etichetă/text stabil — un contract explicit cu dezvoltatorii. Pentru elemente ambigue, folosește tiparul context → element (găsești containerul, apoi elementul din el) în loc de selectori lungi. Evită localizarea după poziție (nth) și după text volatil sau clase generate — ambele produc teste care mint sau pică fără motiv.
modul 02 / primul test cu Playwright
setup & anatomia unui test
Playwright (Microsoft) e unealta pe care o folosim în acest curs: rapidă, cu așteptări automate, suport pentru toate browserele majore și unelte de depanare excelente. Conceptele se transferă însă la orice framework (Cypress, Selenium).
instalarea
npm init playwright@latest
Instalează framework-ul, browserele și generează un proiect exemplu cu configurare. Structura tipică:
proiect/
tests/ # testele tale
login.spec.ts
playwright.config.ts # configurarea (browsere, URL de bază, retry)
package.json
anatomia unui test
import { test, expect } from '@playwright/test';
test('utilizatorul se poate autentifica', async ({ page }) => {
// 1. ARANJEZ: ajung în starea de start
await page.goto('/login');
// 2. ACȚIONEZ: fac ce ar face utilizatorul
await page.getByLabel('Email').fill('ana@exemplu.ro');
await page.getByLabel('Parolă').fill('parola123');
await page.getByRole('button', { name: 'Autentificare' }).click();
// 3. VERIFIC: rezultatul așteptat
await expect(page.getByRole('heading', { name: 'Panou' })).toBeVisible();
});
Tiparul Arrange → Act → Assert (aranjez → acționez → verific) e universal în testare. Un test care nu are o aserțiune clară la final nu testează nimic — doar execută clickuri.
elementele de bază
| Element | Rol |
| test(nume, fn) | Declară un test; numele descrie comportamentul verificat |
| page | Fila de browser izolată, primită automat pentru fiecare test |
| await | Aproape tot e asincron — o lipsă de await e cauza clasică de teste instabile |
| expect() | Aserțiunea: ce trebuie să fie adevărat |
numele testului contează
Scrie ce comportament verifici, nu ce faci mecanic.
❌ „test1", „click pe buton"
✅ „utilizatorul se poate autentifica cu date valide"
✅ „coșul afișează eroare dacă stocul e insuficient"
Când testul pică în CI la 3 dimineața, numele lui e prima informație pe care o vezi.
de reținut
Un test are trei părți: Arrange (starea de start), Act (acțiunile utilizatorului), Assert (verificarea rezultatului). Fără aserțiune, nu e test. Fiecare test primește un page izolat, iar aproape toate operațiile sunt asincrone — un await uitat e cauza clasică de instabilitate. Numele testului descrie comportamentul, nu mecanica.
modul 02 / primul test cu Playwright
acțiuni, aserțiuni & rularea suitei
acțiuni frecvente
| Acțiune | Cod |
| Navighează | await page.goto('/produse') |
| Click | await locator.click() |
| Scrie text | await locator.fill('text') |
| Selectează din listă | await locator.selectOption('valoare') |
| Bifează | await locator.check() |
| Încarcă fișier | await locator.setInputFiles('poza.png') |
| Tastă | await page.keyboard.press('Enter') |
fill() e preferabil lui „tastează caracter cu caracter": e mai rapid și mai stabil. Folosește tastare reală (pressSequentially) doar dacă testezi ceva care reacționează la fiecare tastă (ex: autocomplete).
aserțiuni („web-first")
await expect(locator).toBeVisible();
await expect(locator).toHaveText('Comandă plasată');
await expect(locator).toContainText('249');
await expect(locator).toHaveValue('ana@exemplu.ro');
await expect(locator).toBeEnabled();
await expect(locator).toHaveCount(3);
await expect(page).toHaveURL(/\/confirmare/);
await expect(page).toHaveTitle(/Coș/);
de ce „web-first" e important
Aceste aserțiuni reîncearcă automat până când condiția devine adevărată (sau expiră timpul). Deci nu trebuie să scrii pauze fixe ca să aștepți încărcarea. E diferența dintre:
❌ await page.waitForTimeout(3000) — lent și tot instabil
✅ await expect(x).toBeVisible() — așteaptă exact cât e nevoie
rularea testelor
npx playwright test # toate, headless
npx playwright test --headed # cu browserul vizibil
npx playwright test login # doar fișierele care conțin „login"
npx playwright test --ui # modul interactiv (excelent la dezvoltare)
npx playwright show-report # raportul HTML după rulare
gruparea & focalizarea
test.describe('Coș de cumpărături', () => {
test('adaugă produs', async ({ page }) => { /* … */ });
test('șterge produs', async ({ page }) => { /* … */ });
});
test.only(…) // rulează DOAR acesta (util local)
test.skip(…) // sare peste
atenție
test.only uitat în cod și trimis în repository face ca în CI să ruleze un singur test, iar suita pare verde deși nu s-a verificat aproape nimic. E o greșeală clasică — merită o verificare automată în CI care să o interzică.
de reținut
Acțiunile imită utilizatorul (click, fill, check), iar aserțiunile web-first (expect(locator).toBeVisible()) reîncearcă automat — de aceea nu ai nevoie de pauze fixe. Rulezi cu npx playwright test (--ui la dezvoltare, show-report după). Și ai grijă la test.only uitat: face suita din CI verde degeaba.
modul 03 / așteptări & teste instabile (flaky)
auto-waiting & tipurile de așteptare
Aplicațiile web sunt asincrone: date care sosesc, animații, elemente care apar după o cerere. Testul rulează mult mai repede decât utilizatorul — deci trebuie să aștepte momentul potrivit. Cum aștepți e diferența dintre o suită de încredere și una care pică aleatoriu.
auto-waiting: ce face unealta pentru tine
Înainte de fiecare acțiune, Playwright verifică automat că elementul e acționabil:
- Există în DOM și e vizibil.
- E stabil (nu se mai mișcă — s-a terminat animația).
- E activat (nu e disabled) și primește evenimente (nu e acoperit de alt element).
Dacă vreo condiție nu e îndeplinită, reîncearcă până la expirarea timpului. De aceea, în majoritatea cazurilor, nu trebuie să scrii nicio așteptare explicită.
ierarhia așteptărilor
| Tip | Exemplu | Verdict |
| Auto-waiting | await btn.click() | ✅ Implicit, cel mai bun |
| Aserțiune web-first | await expect(x).toBeVisible() | ✅ Pentru stări |
| Așteptare de condiție | await page.waitForURL('/succes') | ✅ Când chiar ai nevoie |
| Așteptare de rețea | await page.waitForResponse(…) | ✅ Punctual, pentru cereri anume |
| Pauză fixă | await page.waitForTimeout(3000) | ❌ Ultimul resort |
de ce pauza fixă e cea mai proastă soluție
O pauză de 3 secunde e simultan prea lungă (irosești timp când răspunsul vine în 200ms — înmulțit cu sute de teste) și prea scurtă (când serverul de CI e încărcat și durează 4 secunde, testul pică). Nu rezolvă problema, doar o ascunde până când mașina e mai lentă.
❌ await page.click('#salveaza');
await page.waitForTimeout(2000);
expect(await page.textContent('.mesaj')).toBe('Salvat');
✅ await page.getByRole('button', { name: 'Salvează' }).click();
await expect(page.getByText('Salvat')).toBeVisible();
Varianta corectă e și mai rapidă (nu așteaptă degeaba), și mai stabilă (așteaptă cât e nevoie).
de reținut
Unealta face auto-waiting: verifică singură că elementul e vizibil, stabil, activ și accesibil clicului. Pentru stări folosești aserțiuni web-first, iar pentru condiții anume, așteptări explicite de URL sau de răspuns. Pauza fixă e ultimul resort — e simultan prea lungă și prea scurtă, și transformă suita în ceva lent și nesigur.
modul 03 / așteptări & teste instabile (flaky)
teste flaky: cauze & remedii
Un test flaky (instabil) e cel care trece sau pică aleatoriu, fără ca aplicația să se fi schimbat. E cel mai toxic lucru dintr-o suită automată — nu pentru că pică, ci pentru că distruge încrederea: după câteva alarme false, echipa începe să ignore testele roșii. Iar atunci suita nu mai are nicio valoare.
cauzele principale
| Cauză | Remediu |
| Pauze fixe / așteptări proaste | Aserțiuni web-first, auto-waiting |
| Teste dependente unul de altul | Fiecare test își creează propriile date, izolat |
| Date partajate care se ciocnesc | Date unice per test (email cu marcaj de timp) |
| Localizatori fragili | Rol/etichetă/text în loc de XPath sau clase |
| Dependență de servicii externe | Mock/stub pentru ce nu controlezi |
| Animații și tranziții | Auto-waiting le acoperă; altfel dezactivezi animațiile în mediul de test |
| Ordinea de rulare / paralelizare | Independență totală între teste |
regula de aur a izolării
Un test nu are voie să depindă de altul. Dacă „testul 2" funcționează doar pentru că „testul 1" a creat un cont înainte, atunci: nu poți rula testele în paralel, nu poți rula un singur test izolat, iar o eroare în primul dărâmă în cascadă restul suitei. Fiecare test trebuie să pornească de la zero și să-și creeze singur ce-i trebuie.
retry — unealtă utilă, dar și capcană
// playwright.config.ts
export default {
retries: process.env.CI ? 2 : 0, // reîncearcă doar în CI
};
Retry-ul e util pentru instabilități reale de infrastructură (o rețea care sughiță). Dar e periculos ca obicei: dacă un test trece „la a treia încercare", ai un test flaky mascat, nu un test reparat. Tratează rapoartele de „flaky" ca pe bug-uri de rezolvat, nu ca pe zgomot acceptabil.
disciplina care ține suita sănătoasă
- Zero toleranță — un test flaky se repară sau se scoate din suită; nu se ignoră.
- Rulează în paralel de la început — expune imediat dependențele ascunse între teste.
- Mediu de test curat și predictibil (date proprii, servicii externe simulate).
- Urmărește statistica — care teste au picat cel mai des în ultima lună? Acolo e problema.
de reținut
Un test flaky distruge încrederea în suită — și o suită în care nu ai încredere e mai rea decât niciuna. Cauzele obișnuite: așteptări proaste, dependențe între teste, date partajate, localizatori fragili. Remediul de bază e izolarea totală: fiecare test își creează singur datele și pornește de la zero. Retry ajută la instabilități de infrastructură, dar folosit ca obicei doar maschează problema.
modul 04 / organizarea codului: Page Object Model
Page Object Model
Primele 10 teste le scrii oricum. La 100, structura devine problema principală. Page Object Model (POM) e tiparul standard care ține o suită mentenabilă.
problema fără POM
Dacă localizatorul butonului de autentificare apare în 30 de teste și dezvoltatorii schimbă textul din „Autentificare" în „Intră în cont", trebuie să modifici 30 de fișiere. Cu POM, modifici un singur loc.
ideea POM
Fiecare pagină (sau componentă mare) devine o clasă care încapsulează: localizatorii ei și acțiunile pe care le poate face un utilizator acolo.
// pages/LoginPage.ts
export class LoginPage {
constructor(page) {
this.page = page;
this.email = page.getByLabel('Email');
this.parola = page.getByLabel('Parolă');
this.butonIntra = page.getByRole('button', { name: 'Autentificare' });
this.eroare = page.getByRole('alert');
}
async deschide() {
await this.page.goto('/login');
}
async autentifica(email, parola) {
await this.email.fill(email);
await this.parola.fill(parola);
await this.butonIntra.click();
}
}
testul devine lizibil
test('respinge parola greșită', async ({ page }) => {
const login = new LoginPage(page);
await login.deschide();
await login.autentifica('ana@exemplu.ro', 'gresita');
await expect(login.eroare).toHaveText('Date de autentificare invalide');
});
Testul descrie acum intenția, nu mecanica. Se citește aproape ca limbaj natural — și oricine din echipă înțelege ce verifică, fără să știe detaliile paginii.
regula de separare
Page Object-ul NU conține aserțiuni. El expune localizatori și acțiuni; verificările stau în test. Motivul: aceeași pagină e folosită în teste cu așteptări diferite. Un POM plin de expect-uri devine rigid și greu de refolosit.
greșeli frecvente cu POM
- Metode prea mărunte (clickButonEmail()) — nu aduc valoare; expune direct localizatorul.
- Metode prea mari care fac tot fluxul și ascund ce testezi.
- Aserțiuni înăuntru — vezi mai sus.
- O clasă gigant pentru tot site-ul, în loc de una per pagină/componentă.
de reținut
POM încapsulează localizatorii și acțiunile unei pagini într-o clasă — un singur loc de modificat când UI-ul se schimbă, și teste care se citesc ca intenție, nu ca mecanică. Regula esențială: acțiuni în Page Object, aserțiuni în test. Evită metodele prea mărunte, clasele gigant și verificările ascunse în POM.
modul 04 / organizarea codului: Page Object Model
fixtures, hooks & pregătirea stării
Multe teste au nevoie de aceeași pregătire: un utilizator autentificat, un produs în coș, o bază de date curată. Repetarea acestor pași în fiecare test e risipă — și îi face lenți.
hooks: cod înainte și după
test.describe('Coș', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/produse'); // rulează înaintea FIECĂRUI test
});
test.afterEach(async ({ page }) => {
// curățenie, dacă e nevoie
});
});
| Hook | Când rulează |
| beforeEach | Înaintea fiecărui test — pregătirea comună |
| afterEach | După fiecare test — curățenie |
| beforeAll | O dată, înaintea grupului (atenție: poate crea dependențe) |
| afterAll | O dată, la final |
fixtures: pregătire refolosibilă
Un fixture e o „resursă" pe care testul o cere și o primește gata pregătită. Ai văzut deja unul: page. Poți crea propriile fixtures:
// fixtures.ts
export const test = base.extend({
// un utilizator deja autentificat
paginaAutentificata: async ({ page }, use) => {
const login = new LoginPage(page);
await login.deschide();
await login.autentifica('ana@exemplu.ro', 'parola123');
await use(page); // testul primește pagina autentificată
},
});
// în test:
test('vede comenzile', async ({ paginaAutentificata }) => { /* … */ });
optimizare: autentificare o singură dată
Autentificarea prin interfață, repetată în 100 de teste, adaugă minute la suită. Tiparul profesionist: te autentifici o dată, salvezi starea sesiunii (cookies + localStorage) într-un fișier și o reutilizezi în toate testele:
// o dată, la setup:
await page.context().storageState({ path: 'auth.json' });
// în config, pentru toate testele:
use: { storageState: 'auth.json' }
Testele pornesc direct autentificate, fără să treacă prin formular. Câștig uriaș de viteză — și mai puține puncte de instabilitate.
de reținut
Hooks (beforeEach/afterEach) elimină repetiția pregătirii, iar fixtures îți dau resurse gata pregătite, refolosibile și compozabile. Cea mai valoroasă optimizare practică: autentifică-te o singură dată și refolosește starea sesiunii (storageState) în loc să treci prin formularul de login în fiecare test. Atenție la beforeAll — poate introduce dependențe între teste.
modul 05 / date de test, izolare & paralelizare
strategii pentru datele de test
Datele sunt a doua cauză de instabilitate, după așteptări. Un test care depinde de „contul ana@exemplu.ro care există deja în baza de date" va pica în ziua în care cineva îl șterge — sau când doi dezvoltatori rulează suita simultan.
trei strategii, cu compromisuri diferite
| Strategie | Cum | Compromis |
| Date fixe (seed) | Baza de date e populată cu un set cunoscut înainte de rulare | Rapid, dar testele se pot influența dacă modifică datele |
| Date create de test | Fiecare test își creează ce-i trebuie, prin API | Cel mai izolat; puțin mai lent |
| Date simulate (mock) | Interceptezi cererile și întorci răspunsuri controlate | Foarte rapid și stabil, dar nu testează integrarea reală |
tiparul recomandat: creare prin API
Pregătește starea prin API (rapid și sigur), apoi testează prin interfață doar ce vrei să verifici:
test('poate finaliza comanda', async ({ page, request }) => {
// pregătire prin API — rapid, fără clickuri
const produs = await request.post('/api/produse', {
data: { nume: 'Test-' + Date.now(), pret: 100, stoc: 5 },
});
// verificarea propriu-zisă prin UI
await page.goto('/produse');
// …
});
Nu testa prin interfață ceea ce nu e subiectul testului. Dacă verifici finalizarea comenzii, nu pierde 20 de secunde creând produsul prin panoul de administrare — fă-o prin API.
date unice — obligatoriu
const email = `test-${'{'}Date.now(){'}'}@exemplu.ro`; // unic la fiecare rulare
Datele unice permit rularea în paralel și repetarea suitei fără curățare manuală. Fără ele, al doilea test care încearcă să înregistreze „ana@exemplu.ro" va primi „email deja folosit".
simularea (mock) cererilor
// forțez un scenariu greu de reprodus: serverul cade
await page.route('**/api/produse', route => route.fulfill({
status: 500,
body: JSON.stringify({ eroare: 'Server indisponibil' }),
}));
await page.goto('/produse');
await expect(page.getByText('Ceva n-a mers')).toBeVisible();
Mock-ul e ideal pentru cazuri limită: erori de server, liste goale, răspunsuri lente, servicii externe (plăți) pe care nu vrei să le apelezi real la fiecare rulare.
de reținut
Regula practică: pregătește prin API, verifică prin UI. Folosește date unice (marcaj de timp) ca să poți rula în paralel și repetat. Rezervă mock-ul pentru cazuri limită greu de reprodus (erori de server, servicii externe) — dar nu simula tot, altfel testezi doar simulările tale, nu aplicația reală.
modul 05 / date de test, izolare & paralelizare
izolare & rulare în paralel
de ce paralelizarea e obligatorie
O suită de 200 de teste E2E, rulate secvențial câte 15 secunde, înseamnă 50 de minute. Nimeni nu așteaptă atât la fiecare commit — deci suita ajunge să ruleze doar noaptea, iar feedback-ul rapid dispare. Rulate în paralel pe 5 fire, aceleași teste durează ~10 minute.
cum funcționează
// playwright.config.ts
export default {
fullyParallel: true, // teste paralele, inclusiv în același fișier
workers: process.env.CI ? 4 : undefined,
};
Fiecare worker rulează într-un context de browser izolat: cookies, localStorage și sesiuni separate. Testele nu se văd între ele la nivel de browser — dar împart aceeași bază de date. Acolo apar ciocnirile.
paralelizarea nu creează problemele — le dezvăluie
Dacă suita pică la rularea în paralel, aproape sigur ai dependențe ascunse între teste sau date partajate. Nu dezactiva paralelizarea ca „soluție" — asta doar ascunde defectele de proiectare. Repară izolarea.
lista de verificare a izolării
- Testul își creează datele proprii (nu presupune că există ceva).
- Folosește identificatori unici — email, nume, cod cu marcaj de timp.
- Nu depinde de ordinea rulării.
- Nu modifică o resursă globală de care depind alte teste (ex: setări comune de aplicație).
- Poate rula singur, de zece ori la rând, cu același rezultat.
Ultimul punct e testul decisiv: rulează un singur test de zece ori. Dacă a doua oară pică pentru că prima rulare a lăsat date în urmă, nu e izolat.
sharding — împărțirea pe mai multe mașini
npx playwright test --shard=1/3 # mașina 1 rulează o treime
npx playwright test --shard=2/3
npx playwright test --shard=3/3
Pentru suite mari, împarți testele pe mai multe mașini din CI și rulezi simultan. E următorul nivel după paralelizarea locală — dar funcționează doar dacă testele sunt cu adevărat independente.
de reținut
Paralelizarea transformă o suită de 50 de minute în una de 10 — condiția fiind izolarea totală. Fiecare worker are context de browser separat, dar baza de date e comună, deci datele unice sunt esențiale. Dacă suita pică în paralel, ai o problemă de izolare, nu de paralelizare. Testul decisiv: un test rulat singur, de zece ori la rând, dă mereu același rezultat.
modul 06 / depanare & dovezi
trace, capturi & depanare
Un test care pică în CI la 3 dimineața, pe o mașină la care n-ai acces, e inutil dacă nu-ți spune de ce a picat. De aceea uneltele de diagnostic sunt la fel de importante ca testele.
trace viewer — cea mai puternică unealtă
Un trace înregistrează întreaga execuție: fiecare acțiune, starea DOM la fiecare pas, capturi, cererile de rețea și consola. Îl deschizi după rulare și navighezi înapoi în timp.
// playwright.config.ts
use: {
trace: 'on-first-retry', // înregistrează când un test pică și se reia
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
npx playwright show-trace trace.zip
Vezi exact ce era pe ecran la pasul care a eșuat, ce localizator a căutat, ce cereri de rețea au avut loc. Majoritatea investigațiilor se termină în două minute cu un trace, față de ore de ghicit.
de ce „on-first-retry" e setarea potrivită
Înregistrarea tuturor rulărilor încetinește suita și produce fișiere mari. Cu on-first-retry, testele care trec nu costă nimic, iar cele care pică sunt reluate cu înregistrare completă — ai dovada exact acolo unde ai nevoie de ea.
depanare locală
| Mod | Cum |
| Modul UI | npx playwright test --ui — rulezi pas cu pas, vezi tot, cel mai bun la dezvoltare |
| Debug | npx playwright test --debug — inspector, execuție controlată |
| Headed | --headed — vezi browserul real |
| Pauză în cod | await page.pause() — oprește exact acolo și deschide inspectorul |
| Codegen | npx playwright codegen <url> — înregistrează acțiunile tale ca schelet de cod |
codegen: unealtă bună, obicei prost
Codegen e excelent ca să descoperi rapid localizatori și să pornești un test. Dar codul generat e brut: fără POM, cu localizatori uneori fragili, fără structură. Folosește-l ca punct de plecare, nu ca rezultat final — o suită întreagă generată automat devine imposibil de întreținut.
de reținut
Când un test pică, ai nevoie de dovezi, nu de presupuneri. Configurează trace (on-first-retry), capturi și video la eșec — costul e zero pentru testele care trec. Local, modul UI și page.pause() sunt cele mai eficiente. Codegen e util pentru start și pentru găsirea localizatorilor, dar codul lui trebuie rescris, nu păstrat ca atare.
modul 06 / depanare & dovezi
testare vizuală & aserțiuni pe capturi
Unele defecte nu se văd în DOM: un buton mutat peste alt element, un chenar dispărut, o temă întunecată care s-a stricat. Aici intervine testarea vizuală de regresie.
cum funcționează
await expect(page).toHaveScreenshot('pagina-produs.png');
- Prima rulare — salvează captura ca imagine de referință (baseline).
- Rulările următoare — compară captura curentă cu referința, pixel cu pixel (cu o toleranță configurabilă).
- Dacă diferă peste prag, testul pică și primești o imagine cu diferențele evidențiate.
ce prinde bine
- Regresii de CSS după refactorizări sau actualizări de bibliotecă.
- Elemente suprapuse sau deplasate.
- Diferențe între browsere.
- Teme (light/dark) stricate.
marea problemă: conținutul dinamic
Orice se schimbă între rulări — data curentă, un carusel, o reclamă, o animație, un avatar aleatoriu — face captura să difere fără ca ceva să fie stricat. Rezultatul: teste vizuale flaky, exact lucrul pe care încerci să-l eviți.
await expect(page).toHaveScreenshot('pagina.png', {
mask: [page.getByTestId('data-curenta')], // acoperă zonele dinamice
maxDiffPixelRatio: 0.01, // toleranță mică
animations: 'disabled',
});
reguli practice
- Maschează zonele dinamice (date, nume aleatorii, reclame) sau folosește date fixe.
- Dezactivează animațiile în mediul de test.
- Fixează dimensiunea ferestrei — altfel layout-ul diferă.
- Generează referințele în același mediu în care rulează CI-ul: redarea fonturilor diferă între sisteme de operare, iar referințe făcute local vor pica în CI.
- Aplic-o punctual, pe componente și pagini-cheie, nu pe tot site-ul.
de reținut
Testarea vizuală prinde ce nu se vede în DOM: layout stricat, elemente suprapuse, teme defecte. Costul ei e sensibilitatea la orice conținut dinamic — deci maschează zonele volatile, dezactivează animațiile, fixează dimensiunea ferestrei și generează referințele în același mediu ca CI-ul (fonturile se redau diferit între sisteme). Folosită punctual, e valoroasă; aplicată pe tot, devine o sursă de alarme false.
modul 07 / testarea API
de ce și cum testezi API-ul
Nu tot ce merită testat trece prin interfață. Testele de API verifică serviciile direct — sunt de zeci de ori mai rapide, mult mai stabile și îți spun mai precis unde e problema.
API vs UI — ce alegi
| Test API | Test UI (E2E) |
| Viteză | Milisecunde | Zeci de secunde |
| Stabilitate | Foarte mare | Sensibil la schimbări de UI |
| Ce verifică | Logica de business, validări, permisiuni | Experiența reală a utilizatorului |
| Diagnostic | Precis (știi exact ce endpoint) | Vag („ceva n-a mers pe pagină") |
Regula practică: tot ce poate fi verificat la nivel de API, verifică-l acolo. Rezervă testele UI pentru fluxurile pe care le trăiește utilizatorul. Nu scrie 30 de teste UI pentru 30 de combinații de validare a unui formular — scrie unul UI (formularul afișează erorile) și 29 la nivel de API.
testarea API în Playwright
test('creează un produs valid', async ({ request }) => {
const raspuns = await request.post('/api/produse', {
data: { nume: 'Tastatură', pret: 249.9, stoc: 10 },
});
expect(raspuns.status()).toBe(201);
const corp = await raspuns.json();
expect(corp.nume).toBe('Tastatură');
expect(corp).toHaveProperty('id');
});
ce verifici într-un test de API
- Codul de stare — 200/201 la succes, 400 la date invalide, 401 neautentificat, 403 fără drepturi, 404 inexistent.
- Structura răspunsului — câmpurile așteptate există și au tipul corect.
- Valorile — ce s-a salvat e ce ai trimis.
- Antetele — content-type, cache, antete de securitate.
- Efectele secundare — dacă e cazul, verifici că datele chiar s-au modificat.
cazurile negative — cele mai valoroase
test('respinge prețul negativ', async ({ request }) => {
const r = await request.post('/api/produse', {
data: { nume: 'X', pret: -5 },
});
expect(r.status()).toBe(400);
});
test('refuză accesul fără autentificare', async ({ request }) => {
const r = await request.delete('/api/produse/1');
expect(r.status()).toBe(401);
});
Testele negative (date invalide, lipsa permisiunilor) prind cele mai periculoase defecte — inclusiv găuri de securitate. Un endpoint care acceptă un preț negativ sau permite ștergerea fără autentificare e un bug grav, invizibil în testele „fericite".
de reținut
Testele de API sunt rapide, stabile și precise în diagnostic — mută la acest nivel tot ce nu ține strict de experiența vizuală. Verifici cod de stare, structură, valori, antete. Cele mai valoroase sunt cazurile negative: date invalide, lipsa autentificării, lipsa permisiunilor — acolo se ascund defectele de securitate care nu apar niciodată în scenariile fericite.
modul 07 / testarea API
combinarea API + UI & contracte
tiparul hibrid: cel mai eficient
Cel mai bun raport valoare/cost îl obții combinând nivelurile: pregătești și cureți prin API, verifici prin UI.
test('utilizatorul vede comanda în istoric', async ({ page, request }) => {
// 1. pregătire rapidă prin API (fără 20 de clickuri)
const comanda = await request.post('/api/comenzi', {
data: { produsId: 5, cantitate: 2 },
});
const { numar } = await comanda.json();
// 2. verific prin UI exact ce contează
await page.goto('/cont/comenzi');
await expect(page.getByText(numar)).toBeVisible();
});
Testul rămâne un test de interfață (verifică ce vede utilizatorul), dar durează 3 secunde în loc de 40 și are mult mai puține puncte de eșec.
verificarea încrucișată
Invers: faci acțiunea prin UI și confirmi prin API că datele s-au salvat corect — util când interfața arată succes, dar în spate s-a salvat altceva.
await page.getByRole('button', { name: 'Salvează' }).click();
await expect(page.getByText('Salvat')).toBeVisible();
// confirm în sursa de adevăr
const r = await request.get('/api/profil');
expect((await r.json()).oras).toBe('Chișinău');
testarea de contract
Când front-end-ul și back-end-ul sunt dezvoltate separat, apare o problemă clasică: back-end-ul redenumește un câmp din pret în price, iar interfața afișează brusc gol. Testele de contract verifică forma răspunsului, nu doar valorile:
const corp = await raspuns.json();
expect(corp).toMatchObject({
id: expect.any(Number),
nume: expect.any(String),
pret: expect.any(Number),
});
Astfel prinzi schimbările incompatibile imediat, nu după ce ajung în producție.
autentificarea în testele de API
const raspuns = await request.get('/api/comenzi', {
headers: { Authorization: `Bearer ${'{'}token{'}'}` },
});
Obții token-ul o dată (printr-un apel de login) și îl refolosești — la fel ca storageState pentru UI.
de reținut
Tiparul hibrid — pregătire prin API + verificare prin UI — îți dă teste de zece ori mai rapide, păstrând valoarea testării prin interfață. Poți face și verificare încrucișată (acțiune în UI, confirmare prin API) pentru cazurile în care interfața minte. Iar testele de contract (verificarea formei răspunsului, nu doar a valorilor) prind schimbările incompatibile de API înainte să ajungă în producție.
modul 08 / CI/CD, rapoarte & cross-browser
testele în pipeline-ul de CI
O suită care rulează doar pe calculatorul tău, când îți amintești, are o fracțiune din valoare. Puterea reală apare când testele rulează automat la fiecare modificare de cod.
ce înseamnă CI pentru teste
CI (integrare continuă): la fiecare push sau pull request, o mașină curată descarcă codul, instalează dependențele, rulează suita și raportează rezultatul. Dacă testele pică, modificarea nu se poate integra.
# .github/workflows/teste.yml
name: Teste E2E
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always() # păstrează raportul CHIAR DACĂ testele pică
with:
name: raport
path: playwright-report/
detaliul care se uită mereu
if: always() la pasul de salvare a raportului. Fără el, artefactele se încarcă doar când suita trece — adică fix atunci când nu ai nevoie de ele. Când testele pică, rămâi fără trace, fără capturi, fără raport.
ce mediu testezi
| Strategie | Când |
| Pornești aplicația în CI (webServer) | Cel mai izolat și predictibil |
| Mediu de staging dedicat | Aproape de producție, cu servicii reale |
| Mediu efemer per pull request | Ideal: fiecare modificare testată izolat |
ce rulezi și când
- La fiecare PR — testele critice (smoke), rapid: 5–10 minute maximum.
- La merge în ramura principală — suita completă.
- Noaptea — suita extinsă, pe toate browserele și dispozitivele.
Dacă suita de PR durează 40 de minute, dezvoltatorii vor începe să o ocolească. Feedback-ul rapid e mai valoros decât acoperirea totală la acest nivel — folosește etichete (tag-uri) ca să separi testele critice de restul.
de reținut
Valoarea reală apare când suita rulează automat la fiecare modificare, pe o mașină curată, blocând integrarea dacă pică. Salvează mereu raportul și artefactele (if: always()) — altfel n-ai dovezi exact când ai nevoie. Împarte rularea: smoke rapid la PR (sub 10 minute), suită completă la merge, extinsă noaptea.
modul 08 / CI/CD, rapoarte & cross-browser
rapoarte, cross-browser & mobil
rapoarte utile
| Tip | Pentru cine |
| HTML | Oameni — pași, capturi, trace, filtrare |
| JUnit / XML | Unelte de CI care agregă rezultate |
| JSON | Prelucrare proprie, tablouri de bord |
| Line/list | Consolă, în timpul rulării |
Un raport bun răspunde imediat la: ce a picat, la ce pas, cum arăta pagina atunci. Dacă echipa trebuie să reproducă manual ca să înțeleagă, raportul e insuficient.
testare pe mai multe browsere
// playwright.config.ts
projects: [
{ name: 'chromium', use: devices['Desktop Chrome'] },
{ name: 'firefox', use: devices['Desktop Firefox'] },
{ name: 'webkit', use: devices['Desktop Safari'] },
{ name: 'mobil', use: devices['iPhone 13'] },
]
Aceleași teste rulează pe toate configurațiile definite. Webkit (motorul Safari) e cel mai important de inclus: e singurul motor permis pe iOS, deci acoperă toți utilizatorii de iPhone, și are cele mai multe particularități.
emulare mobilă — ce acoperă și ce nu
- Acoperă: dimensiunea ecranului, densitatea pixelilor, user agent, evenimente de atingere — deci layout responsiv și interacțiuni de bază.
- Nu acoperă: performanța reală a unui telefon, particularitățile browserului nativ, rețele lente, gesturi complexe.
Emularea prinde majoritatea problemelor de layout, dar nu înlocuiește complet testarea pe dispozitive reale pentru aplicații critice.
strategia de acoperire
| Când | Ce rulezi |
| La fiecare PR | Chromium (rapid, prinde majoritatea regresiilor) |
| La merge | Chromium + Webkit |
| Noaptea / înainte de lansare | Toate browserele + configurațiile mobile |
Rularea completă pe toate browserele la fiecare commit triplează timpul pentru un câștig mic — majoritatea regresiilor apar la fel peste tot.
de reținut
Raportul HTML (cu pași, capturi și trace) e pentru oameni; JUnit/JSON pentru unelte. Testarea cross-browser se face definind proiecte — includerea Webkit e cea mai importantă (acoperă iOS, cele mai multe particularități). Emularea mobilă prinde problemele de layout, dar nu performanța reală a dispozitivului. Rulează puțin și des la PR, complet la merge sau noaptea.
modul 09 / strategie, mentenanță · proiect & test final
strategie & mentenanță pe termen lung
Cea mai mare provocare nu e să scrii teste, ci ca suita să fie încă utilă peste doi ani. Majoritatea suitelor automate sunt abandonate — nu pentru că uneltele sunt slabe, ci din cauza deciziilor de proiectare.
de la ce începi
- Fluxurile critice — cele care, dacă se strică, opresc businessul (login, coș, plată). 5–10 teste.
- Regresii recente — bug-uri care au ajuns în producție; scrii un test care le-ar fi prins.
- Zonele fragile — unde se strică lucruri repetat.
- Restul, în funcție de risc și frecvență de utilizare.
acoperirea nu e un scop
„Vrem 80% acoperire cu teste E2E" e un obiectiv prost. O suită de 500 de teste E2E lente și fragile e mai rea decât 40 de teste bine alese, rapide și de încredere. Măsoară riscul acoperit, nu numărul de teste sau procentul de linii.
anti-tipare frecvente
| Anti-tipar | De ce e rău |
| Teste care depind unul de altul | Nu poți paraliza; o eroare dărâmă suita în cascadă |
| Un test uriaș care verifică tot | Când pică, nu știi ce anume s-a stricat |
| Pauze fixe peste tot | Suită lentă și tot instabilă |
| Teste fără aserțiuni | Trec mereu — dau falsă siguranță |
| Localizatori fragili (XPath, clase) | Se strică la orice modificare de UI |
| Teste flaky tolerate | Erodează încrederea până când nimeni nu mai citește rezultatele |
| Suită generată integral cu codegen | Fără structură; imposibil de întreținut |
igiena suitei
- Testele sunt cod de producție — trec prin code review, respectă convenții, se refactorizează.
- Ștergerea e o unealtă — un test care nu mai aduce valoare (sau pică mereu fără motiv) se șterge. O suită mai mică și de încredere bate una mare și ignorată.
- Urmărește durata — dacă suita crește de la 8 la 25 de minute, investighează înainte să devină de neutilizat.
- Testele sunt responsabilitatea echipei, nu doar a unei persoane. Dacă un dezvoltator strică un test, îl repară.
metrici care contează cu adevărat
| Metrică | Ce-ți spune |
| Rata de instabilitate | Câte teste pică fără cauză reală — trebuie să tindă la zero |
| Durata suitei | Dacă depășește pragul, oamenii o ocolesc |
| Bug-uri scăpate în producție | Ce n-a prins suita — sursa următoarelor teste |
| Timp de diagnostic | Cât durează să înțelegi de ce a picat un test |
de reținut
Începe cu fluxurile critice și cu regresiile care au scăpat deja. Nu urmări procente de acoperire — urmărește riscul acoperit. Tratează testele ca cod de producție (review, refactorizare, ștergere când nu mai aduc valoare) și supraveghează instabilitatea și durata: acestea două decid dacă suita rămâne folosită sau devine decor.
modul 09 / strategie, mentenanță · proiect & test final
proiect ghidat: suită E2E completă
Pui tot cursul la lucru construind o suită automată completă pentru o aplicație de tip magazin online — proiectul standard prin care se demonstrează stăpânirea automatizării.
ce construiești
O suită structurată, rulată în CI, care acoperă fluxurile critice ale unui magazin: autentificare, căutare, coș, finalizarea comenzii — plus teste de API și verificări vizuale punctuale.
structura proiectului
suita/
tests/
auth.spec.ts # autentificare (pozitiv + negativ)
cautare.spec.ts # căutare & filtrare
cos.spec.ts # adăugare, cantitate, ștergere
comanda.spec.ts # finalizare (fluxul critic)
api/produse.spec.ts # validări & permisiuni la nivel de API
pages/
LoginPage.ts ProdusPage.ts CosPage.ts CheckoutPage.ts
fixtures/
index.ts # utilizator autentificat, date de test
playwright.config.ts
.github/workflows/teste.yml
planul, pe modulele cursului
- Strategie (mod. 0, 9): alegi fluxurile critice; ~15 teste E2E bine țintite, nu 200.
- Localizatori (mod. 1): toți semantici (rol/etichetă/text); data-testid doar unde e necesar.
- Testele de bază (mod. 2): Arrange-Act-Assert, nume descriptive, aserțiuni web-first.
- Stabilitate (mod. 3): zero pauze fixe; te bazezi pe auto-waiting și aserțiuni care reîncearcă.
- Structură (mod. 4): câte un Page Object per pagină (fără aserțiuni în ele) + fixture pentru utilizator autentificat cu storageState.
- Date & izolare (mod. 5): fiecare test își creează datele prin API, cu identificatori unici; rulare fullyParallel.
- Diagnostic (mod. 6): trace on-first-retry, capturi și video la eșec; o verificare vizuală pe pagina de produs, cu zonele dinamice mascate.
- API (mod. 7): validări și permisiuni testate direct (inclusiv cazuri negative: preț negativ, ștergere fără autentificare).
- CI (mod. 8): rulare la fiecare PR (smoke, Chromium), suită completă la merge (+Webkit), raport salvat cu if: always().
definiția lui „gata"
- Suita rulează în CI la fiecare PR și blochează integrarea când pică.
- Rulează în paralel și trece de zece ori la rând, identic — zero teste flaky.
- Orice test poate rula singur, fără să depindă de altele.
- Când un test pică, trace-ul îți spune în două minute de ce.
- Nicio pauză fixă, niciun XPath absolut, nicio aserțiune în Page Objects.
- Durata la PR: sub 10 minute.
felicitări
Ai parcurs automatizarea testării cap-coadă: de ce și ce automatizăm, localizatori stabili, primul test, așteptări și teste flaky, Page Object Model, date și izolare, depanare cu trace, testare API, CI/CD și strategie de mentenanță — combinate într-o suită completă rulată automat. Ai acum principiul central: o suită mică, rapidă și de încredere valorează mai mult decât una mare și ignorată. Urmează testul final: 20 de întrebări din tot cursul. Prag: 70%. Succes!