Reguli mici, impact mare: 10 principii pe care le urmez pentru a scrie teste mai bune
Test Automation

Reguli mici, impact mare: 10 principii pe care le urmez pentru a scrie teste mai bune

Taia Dimitrova
Creat: 2025-10-24Actualizat: 2025-10-24
12 min citire

TL;DR - Scrierea testelor care rezistă

  • Un test, un scop - Păstrează fiecare test concentrat pe un singur obiectiv.
  • DRY - Nu repeta codul.
  • Refolosește testele - Iterează prin seturi de date când scenariile sunt aceleași.
  • Separă datele de teste - Păstrează logica și datele separate pentru claritate.
  • Fă aserțiunile informative - Scrie mesaje de eșec care explică de ce.
  • Folosește cu grija wait-urile - evită waitForTimeout și folosește condiții inteligente.
  • Păstrează testele independente - Nu le înlănțui, decât dacă e logică E2E.
  • Păstrează-le simple și scurte - Fiecare test ar trebui să spună o poveste clară.
  • Revizuiește testele ca pe cod - Fii consistent, util și colaborativ.
  • Folosește locatori si selectorii stabili și păstrează-i organizați - Dacă sunt setate corect, suita ta va rămâne stabilă și după refactorizări.

Ce definește un test cu adevărat bun?

M-am gândit mult la ce face un test bun. E tool-ul? Framework-ul? Limbajul? Nu chiar, e modul în care îl scrii.

De-a lungul timpului, am realizat că scrierea testelor nu e atât de diferită de scrierea unui cod bun. Ai nevoie de standarde. Reguli. Un pic de disciplină.

Urmarea câtorva principii simple ajută să păstrezi suita de teste curată, clară și stabilă, chiar și după luni (sau ani). De asemenea, menține echipa pe aceeași direcție și îți va mulțumi viitorul Tu când vei deschide codul vechi.

Deci, iată regulile de bază pe care le urmez înainte de fiecare pull request, cele care mențin testele mele consistente, semnificative și mai puțin instabile.

1. Un test, un scop

Fiecare test ar trebui să aibă un motiv clar de a exista. Ar trebui să verifice un singur lucru, să spună o poveste și să eșueze dintr-un singur motiv. Asta nu înseamnă că ai nevoie de un test separat pentru fiecare expect().

Înseamnă doar că totul din interiorul unui test, de la setup la asserturi, ar trebui să servească același scop.

Exemplu (ce să nu faci):

test('user can login and view dashboard', async ({ page }) => {
  // Login
  await page.goto('/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password123');
  await page.click('button[type="submit"]');
  
  // Check dashboard
  await expect(page.locator('.dashboard')).toBeVisible();
  await expect(page.locator('.welcome-message')).toContainText('Welcome');
});

De ce?:

  • Dacă acest test pică, nu vei ști imediat ce e stricat - login sau dashboard?
  • Ai amestecat două fluxuri diferite într-un singur test.

Abordare mai bună:

test('user can login successfully', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password123');
  await page.click('button[type="submit"]');
  
  await expect(page.locator('.user-menu')).toBeVisible();
});
 
test('dashboard displays welcome message', async ({ page }) => {
  // Presupunem că utilizatorul e deja logat via fixture
  await page.goto('/dashboard');
  
  await expect(page.locator('.welcome-message')).toContainText('Welcome');
});

De ce contează:

  • Vezi instant ce a picat fără să citești log-uri.
  • Re-rulezi doar partea picată în loc de un flow întreg.
  • Păstreazi rapoartele curate și semnificative.

2. DRY (Don't Repeat Yourself)

Cu toții am făcut-o, am copiat câteva linii dintr-un test în altul "numai acum, pentru moment". Trei sprint-uri mai târziu, repari același selector stricat în 12 locuri diferite. Așa se răspândește codul instabil.

În schimb, urmează principiul DRY - Don't Repeat Yourself. Dacă folosești aceeași secvență de pași în două sau mai multe teste, extrage-le într-o funcție.

Exemplu (modul dezordonat):

test('create product A', async ({ page }) => {
  await page.goto('/products');
  await page.click('button:has-text("New Product")');
  await page.fill('#name', 'Product A');
  await page.fill('#price', '99.99');
  await page.click('button:has-text("Save")');
  await expect(page.locator('.success-message')).toBeVisible();
});
 
test('create product B', async ({ page }) => {
  await page.goto('/products');
  await page.click('button:has-text("New Product")');
  await page.fill('#name', 'Product B');
  await page.fill('#price', '149.99');
  await page.click('button:has-text("Save")');
  await expect(page.locator('.success-message')).toBeVisible();
});

Modul DRY:

async function createProduct(page: Page, name: string, price: string) {
  await page.goto('/products');
  await page.click('button:has-text("New Product")');
  await page.fill('#name', name);
  await page.fill('#price', price);
  await page.click('button:has-text("Save")');
}
 
test('create product A', async ({ page }) => {
  await createProduct(page, 'Product A', '99.99');
  await expect(page.locator('.success-message')).toBeVisible();
});
 
test('create product B', async ({ page }) => {
  await createProduct(page, 'Product B', '149.99');
  await expect(page.locator('.success-message')).toBeVisible();
});

De ce contează:

  • O singură modificare actualizează toate testele vizate
  • Codul tău arată mai curat și spune o poveste mai clară.
  • Noii membri ai echipei înțeleg flow-ul mai rapid.

Ține minte: nu exagera cu DRY. Folosește-l doar atunci când face testele mai ușor de citit, nu mai complicate.

3. Refolosește testele când are sens

Uneori flow-ul e același, dar datele se schimbă. Poate e nevoie să te loghezi ca diferiți utilizatori, sau să verifici același formular cu mai multe seturi de input.

În loc să scrii cinci teste aproape identice, refolosește aceeași logică și iterează prin setul de date.

Asta e testarea bazată pe date: simplă, elegantă și eficientă.

Fără refolosirea logicii (modul repetitiv):

test('admin can access admin panel', async ({ page }) => {
  await loginAs(page, 'admin@example.com', 'admin123');
  await expect(page.locator('.admin-panel')).toBeVisible();
});
 
test('manager can access admin panel', async ({ page }) => {
  await loginAs(page, 'manager@example.com', 'manager123');
  await expect(page.locator('.admin-panel')).toBeVisible();
});
 
test('supervisor can access admin panel', async ({ page }) => {
  await loginAs(page, 'supervisor@example.com', 'supervisor123');
  await expect(page.locator('.admin-panel')).toBeVisible();
});

Versiune reutilizabilă, bazată pe date:

const adminUsers = [
  { email: 'admin@example.com', password: 'admin123', role: 'admin' },
  { email: 'manager@example.com', password: 'manager123', role: 'manager' },
  { email: 'supervisor@example.com', password: 'supervisor123', role: 'supervisor' },
];
 
for (const user of adminUsers) {
  test(`${user.role} can access admin panel`, async ({ page }) => {
    await loginAs(page, user.email, user.password);
    await expect(page.locator('.admin-panel')).toBeVisible();
  });
}

De ce contează:

  • Păstrează suita de teste simplă și eficientă - o logică, multe puncte de date.
  • Reduce mentenanța - modifici într-un singur un loc, nu cinci.
  • Îmbunătățește acoperirea fără să încarce testele inutil.

Asigură-te doar că numele testului include setul de date (${user.role}) - va face rapoartele tale ușor de citit și depanat.

4. Păstrează datele de test separat

Hardcodarea valorilor în interiorul testului tău ar putea părea inofensivă, până când trebuie să le schimbi în 20 de locuri.

Datele de test sunt primul lucru care devine dezordonat dacă nu sunt gestionate corect. Scopul tău e simplu: separă logica de date.

Ce să eviți: Amestecarea logicii și seturilor de date direct în fișierul tău de test.

test('create product', async ({ page }) => {
  await page.goto('/products');
  await page.fill('#name', 'Premium Widget');
  await page.fill('#price', '99.99');
  await page.fill('#category', 'Electronics');
  await page.fill('#description', 'A high-quality widget for professionals');
  // ... mai multe valori hardcodate
});

sau această variantă:

const productName = 'Premium Widget';
const productPrice = '99.99';
const productCategory = 'Electronics';
 
test('create product', async ({ page }) => {
  await page.goto('/products');
  await page.fill('#name', productName);
  // ...
});

Modul mai curat: Mută datele într-un folder dedicat (de ex. /test-data/) și importă-le.

/tests/ui/products/test-data/products.data.ts

export const PRODUCT_DATA = {
  premium: {
    name: 'Premium Widget',
    price: '99.99',
    category: 'Electronics',
    description: 'A high-quality widget for professionals',
  },
  basic: {
    name: 'Basic Widget',
    price: '29.99',
    category: 'Electronics',
    description: 'An affordable widget for everyday use',
  },
};

/tests/ui/products/create-product.spec.ts

import { PRODUCT_DATA } from './test-data/products.data';
 
test('create premium product', async ({ page }) => {
  await page.goto('/products');
  await page.fill('#name', PRODUCT_DATA.premium.name);
  await page.fill('#price', PRODUCT_DATA.premium.price);
  await page.fill('#category', PRODUCT_DATA.premium.category);
  await page.fill('#description', PRODUCT_DATA.premium.description);
  // ...
});

De ce contează:

  • Face logica testului ușor de citit și menținut.
  • Păstrează fișierele de test scurte și concentrate.
  • Îți permite să refolosești sau să schimbi seturi de date între medii (de ex., staging vs. prod).

Tine minte! Păstrarea seturilor de date în același fișier de test funcționează doar pentru exemple rapide, auto-conținute. Altfel, externalizarea lor păstrează testele mai curate și mai scalabile.

Tratează datele de test ca configurație, nu ca cod.

5. Asigură-te că asserturile sunt informative

Un test care pică ar trebui să te ajute, nu să te lase confuz.

Dacă raportul tău spune:

expect(received).toBeTruthy()

…ai pierdut deja câteva minute încercând să-ți dai seama ce înseamnă.

Un assert bun e ca un mesaj de eroare bine scris, îți spune ce s-a stricat și unde. De aceea adaug întotdeauna context și mesaje personalizate la asserturile mele.

Exemplu (versiunea "fail misterios"):

await expect(page.locator('.alert')).toBeVisible();

Dacă asta pică, ce alertă avem? - Succes? Eroare? Timeout?

Versiune mai bună:

await expect(
  page.locator('.alert'),
  'Success message is not visible after form submission'
).toBeVisible();

De ce contează:

  • Economisești timp când depanezi - nu e nevoie să ghicești.
  • Rapoartele de test devin auto-explicative pentru oricine le citește.
  • Fail-urile sunt mai puțin stresante pentru că știi instant ce a picat și de ce.

Sfat pro: Când scrii mesaje personalizate, gândește-te: "Dacă asta pică, de ce va avea nevoie viitorul Eu să știe pentru a găsi rapid cauza?"

6. Gestionează atent wait-urile

Dacă există un ucigaș silențios al testelor stabile, e așteptarea proastă. Cu toții am fost acolo, testul pică aleatoriu, și cineva adaugă:

await page.waitForTimeout(3000);

...și îl "fixează". Pentru o vreme. Până când nu mai funcționează.

Wait-urile hardcodate sunt ca scotchul - ascund problemele de timing în loc să le rezolve. Playwright (și nu numai) îți oferă opțiuni mai bune, deci folosește-le.

Abordarea cu scotch:

test('submit form', async ({ page }) => {
  await page.goto('/form');
  await page.fill('#email', 'test@example.com');
  await page.click('button[type="submit"]');
  await page.waitForTimeout(3000); // De ce 3 secunde? De ce nu 2? Sau 5?
  await expect(page.locator('.success')).toBeVisible();
});

Mai bine (explicit și inteligent):

test('submit form', async ({ page }) => {
  await page.goto('/form');
  await page.fill('#email', 'test@example.com');
  await page.click('button[type="submit"]');
  
  // Așteaptă condiția specifică, nu un timp arbitrar
  await expect(page.locator('.success')).toBeVisible();
  // Sau dacă trebuie să aștepți un request de rețea:
  await page.waitForResponse(response => 
    response.url().includes('/api/submit') && response.status() === 200
  );
});

Playwright așteaptă automat ca elementele să apară, să devină vizibile, active și chiar stabile. Aproape niciodată nu ai nevoie de timeout-uri manuale, doar când scrii teste sau depanezi.

Dacă chiar ai nevoie să aștepți ceva specific, fă-o cu metode explicite.

De ce contează:

  • Reduce fail-urile instabile, aleatorii.
  • Păstrează execuția testelor rapidă.
  • Oferă încredere în rezultatele automatizării.

Sfat pro: Dacă te regăsești adăugând waitForTimeout, întreabă-te de ce elementul nu era gata. Poate e un apel async, animație sau stare de loading lipseste în aplicație.

7. Păstrează testele independente

Dacă un test care pică cauzează trei altele să pice după el - nu ai o suită de teste, ai o linie de domino.

Fiecare test ar trebui să poată rula singur, în orice ordine, și totuși să treacă. Fără presupuneri. Fără dependențe. Fără "acest test funcționează doar după ce login.spec.ts rulează."

Teste înlănțuite:

// login.spec.ts
test('user can login', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password123');
  await page.click('button[type="submit"]');
});
 
// profile.spec.ts
test('user can view profile', async ({ page }) => {
  // Asta presupune că login.spec.ts a rulat primul!
  await page.goto('/profile');
  await expect(page.locator('.profile-info')).toBeVisible();
});

Asta funcționează doar dacă primul test rulează primul, dar eșuează imediat dacă rulezi profile.spec.ts singur sau în paralel.

Bun (auto-conținut):

// profile.spec.ts
test('user can view profile', async ({ page }) => {
  // Loghează-te mai întâi, independent. Sau fa asta in un beforeEach hook
  await page.login('user@example.com', 'password123');
  
  // Acum testează profilul
  await page.goto('/profile');
  await expect(page.locator('.profile-info')).toBeVisible();
});

Acum acest test poate rula independent, în paralel sau chiar în izolare când depanezi - și totuși trece.

De ce contează:

  • Face suita ta prietenoasă cu paralelizarea și stabilă pentru CI.
  • Simplifică depanarea, fără "efecte secundare" de la testele anterioare.
  • Îmbunătățește viteza testelor (nu trebuie să aștepți alte suite).

Sfat pro: Dacă testele împărtășesc o precondiție comună (precum "utilizatorul e logat"), folosește fixture-uri sau hook-uri beforeEach. Nu te baza niciodată pe rulările anterioare de teste pentru a seta starea.

Dar amintește-ți, înlănțuirea testelor are sens când construiești flow-uri e2e.

Fa-o doar intenționat: nu înlănțui pentru comoditate, înlănțuiește pentru sens.

8. Păstrează testele simple și scurte

Dacă testul tău arată ca un mini roman, cu condiții imbricate, pași fără sfârșit și scroll-uri nesfârșite, probabil face prea mult.

Un test bun e ca o propoziție bună: clar, concret și ușor de citit. Oricine ar trebui să poată să-l înțeleagă în câteva minute.

Prea complex, încearcă să testeze totul:

test('user can manage products', async ({ page }) => {
  // Login
  await page.goto('/login');
  await page.fill('#email', 'admin@example.com');
  await page.fill('#password', 'admin123');
  await page.click('button[type="submit"]');
  
  // Create product
  await page.goto('/products');
  await page.click('button:has-text("New Product")');
  await page.fill('#name', 'Test Product');
  await page.fill('#price', '99.99');
  await page.click('button:has-text("Save")');
  await expect(page.locator('.success-message')).toBeVisible();
  
  // Edit product
  await page.click('text=Test Product');
  await page.fill('#name', 'Updated Product');
  await page.click('button:has-text("Update")');
  await expect(page.locator('.success-message')).toBeVisible();
  
  // Delete product
  await page.click('button:has-text("Delete")');
  await page.click('button:has-text("Confirm")');
  await expect(page.locator('.success-message')).toBeVisible();
});

Acest tip de test ar putea "funcționa," dar e complicat de depanat când ceva se strică. Dacă un pas pică, nu știi care parte e vinovatul - login, creare sau ștergere.

Versiune mai bună: Împarte acel monstru în teste mai mici, semnificative:

test('user can create product', async ({ page }) => {
  await loginAsAdmin(page);
  await page.goto('/products');
  await page.click('button:has-text("New Product")');
  await page.fill('#name', 'Test Product');
  await page.fill('#price', '99.99');
  await page.click('button:has-text("Save")');
  
  await expect(page.locator('.success-message')).toBeVisible();
});
 
test('user can edit product', async ({ page }) => {
  await loginAsAdmin(page);
  const productId = await createTestProduct(page);
  
  await page.goto(`/products/${productId}`);
  await page.fill('#name', 'Updated Product');
  await page.click('button:has-text("Update")');
  
  await expect(page.locator('.success-message')).toBeVisible();
});
 
test('user can delete product', async ({ page }) => {
  await loginAsAdmin(page);
  const productId = await createTestProduct(page);
  
  await page.goto(`/products/${productId}`);
  await page.click('button:has-text("Delete")');
  await page.click('button:has-text("Confirm")');
  
  await expect(page.locator('.success-message')).toBeVisible();
});

Fiecare test spune acum propria poveste — scurt, clar și cuprinzător (ușor de menținut).

De ce contează:

  • Testele mici sunt mai rapide de citit, depanat și revizuit.
  • Fail-urile indică exact unde e problema.
  • Poți refolosi pași între teste și totuși păstra lucrurile curate.

Sfat pro: Dacă testul tău durează mai mult de câteva secunde să-l explici, împarte-l. Și dacă durează mai mult de un minut să-l citești, rescrie-l.

9. Revizuiește testele ca pe cod

Testele de automatizare sunt cod, și ar trebui tratate astfel.

Multe echipe sar peste review-urile corecte pentru script-urile de test pentru că "nu fac parte din produs". Codul de test ineficient poate bloca release-urile și pierde timp, exact ca codul de producție.

Deci revizuiește testele cu aceeași grijă cu care ai revizui o funcționalitate din aplicație.

Ce să cauți într-un review de test:

  • E testul citeț și ușor de urmărit?
  • Există pași duplicați care ar putea fi transformați în helperi?
  • Sunt assert-urile semnificative și clare?
  • E denumirea consistentă și descriptivă?
  • Urmează testul aceeași structură și convenții ca celelalte din suită?

Exemplu de checklist de folosit:

  • ✅ Un test = un scop.
  • ✅ Fără date hardcodate în test
  • ✅ Mesaje de assert clare.
  • ✅ Titluri de test descriptive
  • ✅ Helperi sau fixture-uri folosite unde are sens.
  • ✅ Testul rulează independent și fiabil

De ce contează:

  • Păstrează framework-ul curat și scalabil.
  • Face onboarding-ul mai ușor pentru noii QA.
  • Previne problemele mici (precum așteptări ascunse sau denumiri proaste) să crească în coșmaruri de mentenanță.

Sfat pro: Dacă ești într-o echipă,stabiliti reguli de review pentru teste exact ca standardele de coding. Lucruri precum denumirea, locația datelor, fixture-urile și alte aspecte relevante. Consistența nu e doar despre trecerea testelor, e despre încredere în rezultate.

Și încă un lucru:

Review-urile de cod nu sunt despre a dovedi că cineva greșește. Sunt despre a vă ajuta unul pe altul să scrieți cod mai bun. Persoana care îți revizuiește testul nu te judecă; îți protejează timpul, release-urile și viitorul Tu.

10. Folosește locatori stabili și păstrează-i organizați

Testele instabile încep adesea cu o greșeală simplă - locatori sau selectori ineficienți.

Locatorii sunt fundamentul fiecărui test UI. Fă-i corect, și suită ta va rămâne solidă pentru refactorizări. Fă-i greșit, și îți vei petrece nopțile urmărind fail-uri fantomă.

Ierarhia de locatori pe care o urmez întotdeauna:

  1. Prefer locatorii predefiniți ai Playwright - Folosește metode semantice, built-in precum getByRole, getByText sau getByPlaceholder. Sunt rezilienți, citeți și imită modul în care utilizatorii interacționează efectiv cu pagina.

  2. Adaugă atribute data-testid sau id oriunde e posibil - Când poți influența codul produsului - fă-o. Atributele data-testid (sau id) îți oferă control complet și stabilitate. Asta ar trebui să fie selectorii tăi de bază în orice setup serios de automatizare. Dacă lipsesc, cere echipei tale de devi să le adauge. E un efort mic cu un payoff uriaș.

  3. Folosește CSS sau XPath doar ca fallback temporar - Uneori va trebui să te miști rapid, dar fii clar că acești selectori sunt temporari.

Sfat pro: Păstrează un comentariu "todo" sau referință de ticket astfel încât acești selectori să fie înlocuiți odată ce există hook-uri corecte. Viitorul tu îți va mulțumi.

Organizează locatorii în afara fișierelor de test

Nu le împrăștia prin spec-uri. Păstrează-i în page objects dedicate sau hărți de locatori pentru consistență și actualizări mai ușoare.

// pages/products-page.ts
export const PRODUCTS_PAGE = {
  selectors: {
    newProductButton: 'button:has-text("New Product")',
    productNameInput: '[data-testid="product-name"]',
    productPriceInput: '[data-testid="product-price"]',
    saveButton: '[data-testid="save-product"]',
    successMessage: '[data-testid="success-message"]',
  },
  
  async createProduct(page: Page, name: string, price: string) {
    await page.click(this.selectors.newProductButton);
    await page.fill(this.selectors.productNameInput, name);
    await page.fill(this.selectors.productPriceInput, price);
    await page.click(this.selectors.saveButton);
  },
};

Apoi în testul tău:

import { PRODUCTS_PAGE } from '../pages/products-page';
 
test('create product', async ({ page }) => {
  await page.goto('/products');
  await PRODUCTS_PAGE.createProduct(page, 'Test Product', '99.99');
  await expect(page.locator(PRODUCTS_PAGE.selectors.successMessage)).toBeVisible();
});

De ce contează:

  • Păstrează testele stabile pentru refactorizări UI
  • Încurajează colaborarea între QA și devi
  • Reduce duplicarea locatorilor
  • Face review-urile și debugging-ul mai rapide

Sfat pro: Dacă echipa ta încă adaugă test ID-uri treptat, documentează asta într-un document de "acord" dintre QA–Dev sau în strategia ta de testare.

Afirmație Finală

Scrierea testelor bune nu e doar despre sintaxă sau tool-uri, e despre disciplină, gândire și empatie pentru oricine le citește sau rulează următoarele (chiar dacă persoana aceea e viitorul-tu).

Testele curate, stabile și semnificative nu se întâmplă din pur și simplu.

Se întâmplă pentru că urmezi anumite principii: scrii cu intenție, structurezi cu claritate și revizuiești cu bunătate.

Testele nu sunt acolo doar să "prindă bug-uri". Sunt parte din povestea calității produsului tău. Permite echipei să lucreze rapid, menținând totodată stabilitatea produsului.

Ce urmează?

În următorul capitol din Playwright Chronicles, voi vorbi despre ceva cu care se confruntă fiecare tester mai devreme sau mai târziu: Ce să automatizezi, ce nu, și cum să găsești echilibrul corect.

Pentru că știind ce să nu testezi e la fel de important ca scrierea testului perfect.