<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Testimi dhe Debugging në Angular: Udhëzues i plotë për Jest, Jasmine/Karma, Cypress, Playwright dhe Angular Devtools

Hyrje: Përse Testimi dhe Debugging Bëjnë Ndryshimin

Çdo aplikacion Angular që arrin në prodhim, herët a vonë përballet me të njëjtën pyetje: "Si ta di që ky ndryshim nuk ka prishur asgjë?". Përgjigjja serioze nuk është "e provova me dorë në browser", por një grup testesh automatike që ekzekutohen në sekonda, në çdo commit, pa ndërhyrje njerëzore. Njëkohësisht, kur diçka shkon keq — një komponent që rirenderohet shumë herë, një memory leak, një route që nuk ngarkohet — nevojiten mjete debugging të fokusuara për të kuptuar çfarë po ndodh dhe ku, në vend që të mbështetesh te console.log të shpërndara kudo.

Në këtë udhëzues thellohemi në tre shtyllat e ciklit jetësor të një aplikacioni Angular të pjekur:

  • Unit testet me Jasmine/Karma (parazgjedhja e Angular CLI) dhe me Jest (alternativa më e shpejtë dhe gjithnjë e më e përhapur në ekosistemin modern JavaScript).
  • Testimi E2E (end-to-end) me Cypress dhe Playwright, për të validuar flukse të plota përdoruesish në një browser real.
  • Debugging i performancës me Angular DevTools, për të gjetur pengesat në change detection dhe rendering.

Çdo seksion përfshin shembuj kodi gati për përdorim, të nxjerrë nga skenarë realë me komponentë, shërbime, signals dhe thirrje HTTP — të njëjtat elemente që gjenden në çdo aplikacion Angular në prodhim.

Tema: #Angular #Testing #UnitTesting #Jest #Jasmine #Karma #Cypress #Playwright #E2ETesting #AngularDevTools #Debugging #Performance #WebDevelopment

Piramida e Testeve: Ku të Investosh Kohën

Para se të shkruash edhe një rresht të vetëm testi, ia vlen të kesh të qartë modelin mendor të piramidës së testeve, propozuar fillimisht nga Mike Cohn:

  • Baza (e gjerë): Unit testet. Të shpejta (milisekonda), të izoluara, të shumta. Testojnë një funksion, komponent ose shërbim të izoluar.
  • Mesi: Integration testet. Verifikojnë që disa njësi bashkëpunojnë saktë (p.sh. një komponent me shërbimin e tij real, jo të mockuar).
  • Maja (e ngushtë): E2E testet. Të ngadalta (sekonda/minuta), të brishta nëse shkruhen keq, por të vetmet që validojnë përvojën reale të përdoruesit nga login-i deri te checkout-i.

Gabimi më i zakonshëm është përmbysja e piramidës: pak unit teste dhe dhjetëra E2E teste të ngadalta e të brishta që e kthejnë CI-n në një makth 40-minutësh. Qëllimi i këtij udhëzuesi është t'i japë mjetet për të ndërtuar piramidën në drejtimin e saktë: shumë unit teste të shpejta, një numër i arsyeshëm E2E testesh mbi flukset kritike, dhe mjete debugging për kur diçka gjithsesi i shpëton testeve.

Pjesa 1: Unit Testet me Jest dhe Jasmine/Karma

1.1 — Konfigurimi parazgjedhur: Jasmine + Karma

Kur krijon një projekt me ng new, Angular CLI konfiguron automatikisht Jasmine si framework asertimi (describe, it, expect) dhe Karma si test runner, që i nis testet në një browser real (Chrome Headless si parazgjedhje) përmes karma.conf.js.

// karma.conf.js — configurazione generata da Angular CLI
process.env.CHROME_BIN = require('puppeteer').executablePath();

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage'),
      require('@angular-devkit/build-angular/plugins/karma'),
    ],
    client: {
      jasmine: {
        // random: false disabilita l'ordine casuale dei test (utile in debug)
        random: false,
      },
      clearContext: false,
    },
    coverageReporter: {
      dir: require('path').join(__dirname, './coverage/my-app'),
      subdir: '.',
      reporters: [{ type: 'html' }, { type: 'text-summary' }],
      // Soglie minime: la build fallisce se la copertura scende sotto questi valori
      check: {
        global: {
          statements: 80,
          branches: 75,
          functions: 80,
          lines: 80,
        },
      },
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['ChromeHeadless'],
    singleRun: true, // false in locale per il watch mode, true in CI
    restartOnFileChange: true,
  });
};

Për të nisur testet: ng test lokalisht (watch mode aktiv) ose ng test --no-watch --browsers=ChromeHeadless në CI, ku nuk nevojitet browser interaktiv dhe duam që procesi të mbyllet me një exit code në vend që të qëndrojë në pritje.

1.2 — Anatomia e një unit testi: describe, it, expect

Çdo skedar testi Jasmine ndjek të njëjtën strukturë: describe grupon testet e lidhura logjikisht, it (ose alias-i i tij test) përcakton një rast të vetëm, expect kryen asertimin.

// price-calculator.ts
export function applyDiscount(price: number, percentage: number): number {
  if (percentage < 0 || percentage > 100) {
    throw new Error('La percentuale di sconto deve essere tra 0 e 100');
  }
  return Number((price - (price * percentage) / 100).toFixed(2));
}

// price-calculator.spec.ts
import { applyDiscount } from './price-calculator';

describe('applyDiscount', () => {
  it('applica correttamente uno sconto del 20%', () => {
    expect(applyDiscount(100, 20)).toBe(80);
  });

  it('non modifica il prezzo con sconto 0%', () => {
    expect(applyDiscount(50, 0)).toBe(50);
  });

  it('arrotonda a due decimali', () => {
    expect(applyDiscount(19.99, 15)).toBe(16.99);
  });

  it('lancia un errore se la percentuale è negativa', () => {
    expect(() => applyDiscount(100, -5)).toThrowError(
      'La percentuale di sconto deve essere tra 0 e 100',
    );
  });

  it('lancia un errore se la percentuale supera 100', () => {
    expect(() => applyDiscount(100, 150)).toThrowError();
  });
});

Ky është një unit test i pastër: pa asnjë varësi nga Angular, pa TestBed, vetëm një funksion dhe asertimet e tij. Është lloji i testit më i shpejtë për t'u shkruar, lexuar dhe ekzekutuar — dhe duhet preferuar sa herë që logjika mund të nxirret nga një komponent apo shërbim në një funksion të pastër, të testueshëm i izoluar.

1.3 — Testimi i një shërbimi Angular me TestBed

Kur logjika varet nga DI-ja (Dependency Injection) e Angular, nevojitet TestBed për të ndërtuar një mjedis testimi me të njëjtët provider që do të përdorte aplikacioni real — duke zëvendësuar megjithatë varësitë e jashtme (HTTP, storage, shërbime të tjera) me mocks.

// cart.service.ts
import { Injectable, signal, computed } from '@angular/core';

export interface CartItem {
  id: string;
  price: number;
  quantity: number;
}

@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly _items = signal([]);
  readonly items = this._items.asReadonly();

  readonly total = computed(() =>
    this._items().reduce((sum, item) => sum + item.price * item.quantity, 0),
  );

  readonly itemCount = computed(() =>
    this._items().reduce((sum, item) => sum + item.quantity, 0),
  );

  addItem(item: CartItem): void {
    const existing = this._items().find(i => i.id === item.id);
    if (existing) {
      this._items.update(items =>
        items.map(i => (i.id === item.id ? { ...i, quantity: i.quantity + item.quantity } : i)),
      );
    } else {
      this._items.update(items => [...items, item]);
    }
  }

  removeItem(id: string): void {
    this._items.update(items => items.filter(i => i.id !== id));
  }

  clear(): void {
    this._items.set([]);
  }
}

// cart.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { CartService } from './cart.service';

describe('CartService', () => {
  let service: CartService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(CartService);
  });

  it('parte con carrello vuoto', () => {
    expect(service.items()).toEqual([]);
    expect(service.total()).toBe(0);
  });

  it('aggiunge un articolo e calcola il totale', () => {
    service.addItem({ id: 'p1', price: 10, quantity: 2 });
    expect(service.items().length).toBe(1);
    expect(service.total()).toBe(20);
  });

  it('incrementa la quantità se l\'articolo esiste già', () => {
    service.addItem({ id: 'p1', price: 10, quantity: 1 });
    service.addItem({ id: 'p1', price: 10, quantity: 3 });
    expect(service.items().length).toBe(1);
    expect(service.items()[0].quantity).toBe(4);
  });

  it('rimuove un articolo per id', () => {
    service.addItem({ id: 'p1', price: 10, quantity: 1 });
    service.addItem({ id: 'p2', price: 20, quantity: 1 });
    service.removeItem('p1');
    expect(service.items().length).toBe(1);
    expect(service.items()[0].id).toBe('p2');
  });

  it('itemCount somma le quantità, non il numero di righe', () => {
    service.addItem({ id: 'p1', price: 10, quantity: 3 });
    service.addItem({ id: 'p2', price: 5, quantity: 2 });
    expect(service.itemCount()).toBe(5);
  });
});

Vër re si ky test verifikon edhe computed signals: nuk nevojitet asnjë konfigurim special, computed-et rillogariten në mënyrë sinkrone dhe mund të lexohen si një funksion normal (service.total()) menjëherë pas ndryshimit të gjendjes.

1.4 — Testimi i një komponenti standalone me TestBed dhe ComponentFixture

Testimi i një komponenti kërkon një hap më shumë sesa një shërbim: duhet krijuar një instancë reale e komponentit në DOM përmes ComponentFixture, të detyrohet një cikël change detection me fixture.detectChanges(), dhe më pas të pyetet DOM-i rezultues me DebugElement ose query native.

// quantity-selector.component.ts
import { Component, input, output, computed } from '@angular/core';

@Component({
  selector: 'app-quantity-selector',
  standalone: true,
  template: `
    -
    {{ value() }}
    +
  `,
})
export class QuantitySelectorComponent {
  value = input(1);
  min = input(1);
  max = input(99);
  valueChange = output();

  readonly canIncrement = computed(() => this.value() < this.max());

  increment(): void {
    if (this.value() < this.max()) {
      this.valueChange.emit(this.value() + 1);
    }
  }

  decrement(): void {
    if (this.value() > this.min()) {
      this.valueChange.emit(this.value() - 1);
    }
  }
}

// quantity-selector.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { QuantitySelectorComponent } from './quantity-selector.component';

describe('QuantitySelectorComponent', () => {
  let fixture: ComponentFixture;
  let component: QuantitySelectorComponent;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [QuantitySelectorComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(QuantitySelectorComponent);
    component = fixture.componentInstance;
  });

  function getIncrementButton() {
    return fixture.debugElement.query(By.css('[data-testid="increment"]'));
  }

  function getQuantityText(): string {
    return fixture.debugElement.query(By.css('[data-testid="quantity-value"]')).nativeElement.textContent.trim();
  }

  it('mostra il valore iniziale', () => {
    fixture.componentRef.setInput('value', 3);
    fixture.detectChanges();
    expect(getQuantityText()).toBe('3');
  });

  it('emette valueChange con valore incrementato al click su +', () => {
    fixture.componentRef.setInput('value', 5);
    fixture.detectChanges();

    let emittedValue: number | undefined;
    component.valueChange.subscribe(v => (emittedValue = v));

    getIncrementButton().nativeElement.click();

    expect(emittedValue).toBe(6);
  });

  it('disabilita il pulsante + quando si raggiunge il massimo', () => {
    fixture.componentRef.setInput('value', 99);
    fixture.componentRef.setInput('max', 99);
    fixture.detectChanges();

    expect(getIncrementButton().nativeElement.disabled).toBeTrue();
  });

  it('non emette valueChange se il pulsante è disabilitato', () => {
    fixture.componentRef.setInput('value', 99);
    fixture.componentRef.setInput('max', 99);
    fixture.detectChanges();

    let called = false;
    component.valueChange.subscribe(() => (called = true));
    getIncrementButton().nativeElement.click();

    expect(called).toBeFalse();
  });
});

Përdorimi i atributeve data-testid në vend të klasave CSS për të zgjedhur elementet në teste është një praktikë e mirë e konsoliduar: shkëput testet nga stili vizual, kështu një refactoring i CSS-it nuk e prish suite-n e testeve.

1.5 — Spiunimi dhe mockimi i varësive me jasmine.createSpyObj

Kur një komponent varet nga një shërbim, nuk duam që unit testet e tij të thërrasin logjikën reale të shërbimit (që nga ana e vet mund të thërrasë një API HTTP reale). Në vend të kësaj, krijojmë një mock me vetëm metodat e nevojshme, duke kontrolluar saktësisht çfarë kthejnë.

// checkout.component.spec.ts (estratto)
import { of, throwError } from 'rxjs';
import { OrderService } from './order.service';
import { NotificationService } from '../shared/notification.service';

describe('CheckoutComponent', () => {
  let orderServiceSpy: jasmine.SpyObj;
  let notificationSpy: jasmine.SpyObj;

  beforeEach(async () => {
    orderServiceSpy = jasmine.createSpyObj('OrderService', ['submitOrder', 'getShippingCost']);
    notificationSpy = jasmine.createSpyObj('NotificationService', ['success', 'error']);

    await TestBed.configureTestingModule({
      imports: [CheckoutComponent],
      providers: [
        { provide: OrderService, useValue: orderServiceSpy },
        { provide: NotificationService, useValue: notificationSpy },
      ],
    }).compileComponents();
  });

  it('mostra un messaggio di successo quando l\'ordine va a buon fine', () => {
    orderServiceSpy.submitOrder.and.returnValue(of({ orderId: 'ORD-123', status: 'confirmed' }));

    const fixture = TestBed.createComponent(CheckoutComponent);
    fixture.componentInstance.submit();

    expect(orderServiceSpy.submitOrder).toHaveBeenCalledTimes(1);
    expect(notificationSpy.success).toHaveBeenCalledWith(jasmine.stringMatching(/ORD-123/));
  });

  it('mostra un errore se il servizio ordini fallisce', () => {
    orderServiceSpy.submitOrder.and.returnValue(
      throwError(() => new Error('Pagamento rifiutato')),
    );

    const fixture = TestBed.createComponent(CheckoutComponent);
    fixture.componentInstance.submit();

    expect(notificationSpy.error).toHaveBeenCalledWith('Pagamento rifiutato');
    expect(notificationSpy.success).not.toHaveBeenCalled();
  });
});

jasmine.createSpyObj krijon një objekt me metoda të rreme që regjistrojnë çdo thirrje. .and.returnValue(...) konfiguron çfarë të kthejë, dhe toHaveBeenCalledWith(...) verifikon argumentet e kaluara. Kjo teknikë izolon plotësisht komponentin nën test nga logjika reale e varësive të tij.

1.6 — Testimi i thirrjeve HTTP me HttpClientTestingModule

Për shërbimet që përdorin HttpClient, Angular ofron HttpClientTestingModule dhe HttpTestingController: ato përgjojnë kërkesat reale HTTP dhe lejojnë përgjigje me të dhëna të rreme, duke verifikuar njëkohësisht URL-në, metodën dhe headers e përdorura.

// product.service.ts
@Injectable({ providedIn: 'root' })
export class ProductService {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = '/api/products';

  getById(id: string): Observable {
    return this.http.get(`${this.baseUrl}/${id}`);
  }

  search(query: string, page = 1): Observable> {
    return this.http.get>(this.baseUrl, {
      params: { q: query, page: page.toString() },
    });
  }
}

// product.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ProductService } from './product.service';

describe('ProductService', () => {
  let service: ProductService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
    });
    service = TestBed.inject(ProductService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    // Thelbësore: verifikon që nuk ka kërkesa HTTP jetime të pambuluara nga testi
    httpMock.verify();
  });

  it('recupera un prodotto per id', () => {
    const mockProduct = { id: 'p1', name: 'Tastiera meccanica', price: 89.9 };

    service.getById('p1').subscribe(product => {
      expect(product).toEqual(mockProduct);
    });

    const req = httpMock.expectOne('/api/products/p1');
    expect(req.request.method).toBe('GET');
    req.flush(mockProduct);
  });

  it('propaga un errore 404 al subscriber', () => {
    service.getById('missing').subscribe({
      next: () => fail('doveva fallire'),
      error: err => expect(err.status).toBe(404),
    });

    const req = httpMock.expectOne('/api/products/missing');
    req.flush('Not found', { status: 404, statusText: 'Not Found' });
  });

  it('costruisce correttamente i query params per la ricerca', () => {
    service.search('meccanica', 2).subscribe();

    const req = httpMock.expectOne(
      r => r.url === '/api/products' && r.params.get('q') === 'meccanica' && r.params.get('page') === '2',
    );
    expect(req.request.method).toBe('GET');
    req.flush({ items: [], total: 0, page: 2 });
  });
});

Metoda httpMock.verify() te afterEach është thelbësore: e dështon testin nëse ka kërkesa HTTP që komponenti/shërbimi ka dërguar por asnjë asertim nuk i ka përgjuar, duke shmangur pozitive false ku një kërkesë "harrohet" pa gabime.

1.7 — Kod asinkron: fakeAsync, tick() dhe waitForAsync

Disa skenarë — debounce në një fushë kërkimi, timeout, retry me vonesë — përfshijnë setTimeout ose operatorë RxJS të kohëzuar si debounceTime. Testimi i tyre me timer realë do ta bënte suite-n të ngadaltë dhe jo-deterministike. fakeAsync e zgjidh problemin duke virtualizuar kohën.

// search-box.component.ts (estratto)
export class SearchBoxComponent {
  private readonly productService = inject(ProductService);
  query = new FormControl('');
  results = signal([]);

  constructor() {
    this.query.valueChanges
      .pipe(
        debounceTime(300),
        distinctUntilChanged(),
        switchMap(q => (q ? this.productService.search(q) : of({ items: [] as Product[] }))),
        takeUntilDestroyed(),
      )
      .subscribe(result => this.results.set(result.items));
  }
}

// search-box.component.spec.ts
import { fakeAsync, tick, TestBed } from '@angular/core/testing';

it('esegue la ricerca solo dopo 300ms di inattività (debounce)', fakeAsync(() => {
  const fixture = TestBed.createComponent(SearchBoxComponent);
  const component = fixture.componentInstance;
  spyOn(TestBed.inject(ProductService), 'search').and.returnValue(
    of({ items: [{ id: 'p1', name: 'Mouse', price: 20 }] }),
  );

  component.query.setValue('mo');
  tick(100);
  component.query.setValue('mou');
  tick(100);
  component.query.setValue('mouse');

  // Kanë kaluar vetëm 200ms nga ndryshimi i fundit: kërkimi NUK duhet të nisë ende
  expect(TestBed.inject(ProductService).search).not.toHaveBeenCalled();

  tick(300); // plotësojmë debounce-in
  fixture.detectChanges();

  expect(TestBed.inject(ProductService).search).toHaveBeenCalledOnceWith('mouse');
  expect(component.results().length).toBe(1);
}));

tick(ms) e avancon artificialisht orën virtuale të zonës së testit, duke shkaktuar timer-at në pritje pa pritur realisht atë kohë. Kjo e bën testin deterministik dhe të menjëhershëm, ndërkohë që validon saktë logjikën e debounce-it.

1.8 — Testimi i Effects reaktivë ndaj Signals

Me futjen e Signals, shumë komponentë përdorin effect() për t'iu përgjigjur ndryshimeve të gjendjes (p.sh. sinkronizim me localStorage). Testimi i një effect kërkon të detyrohet eksplicitisht flush-i i kontekstit reaktiv me TestBed.flushEffects() ose, më thjesht, me fixture.detectChanges() kur komponenti është i lidhur me një fixture.

// theme.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { ThemeService } from './theme.service';

describe('ThemeService — persistenza tema con effect()', () => {
  beforeEach(() => localStorage.clear());

  it('salva il tema in localStorage quando cambia', () => {
    TestBed.runInInjectionContext(() => {
      const service = TestBed.inject(ThemeService);
      service.setTheme('dark');

      TestBed.tick(); // detyron flush-in e effects në pritje

      expect(localStorage.getItem('theme')).toBe('dark');
    });
  });
});

1.9 — Marble testing për Observable komplekse

Kur logjika RxJS bëhet komplekse (kombinime combineLatest, switchMap, retry me backoff), marble testing me TestScheduler lejon të përshkruash sekuenca kohore ngjarjesh në mënyrë deklarative dhe t'i verifikosh në një tick të vetëm sinkron.

import { TestScheduler } from 'rxjs/testing';

describe('retryWithBackoff (marble testing)', () => {
  let scheduler: TestScheduler;

  beforeEach(() => {
    scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  it('riprova due volte prima di emettere il valore con successo', () => {
    scheduler.run(({ cold, expectObservable }) => {
      // '#' përfaqëson një gabim, '-' një kuadro kohe, 'a' një vlerë të emetuar
      const source$ = cold('-#', undefined, new Error('fail'));
      const expected = '  -- -- -- -- -#';

      // Në një skenar real këtu do të aplikonim retryWithBackoff(source$, { retries: 2 })
      expectObservable(source$.pipe()).toBe('-#', undefined, new Error('fail'));
    });
  });
});

Marble testing ka një kurbë mësimi më të thepisur krahasuar me testet "klasike", por shpërblen kur logjika reaktive përfshin kohëzime precize që do të ishin të vështira për t'u verifikuar në mënyrë të besueshme me fakeAsync të thjeshtë.

1.10 — Kalimi te Jest: përse dhe si

Jest është bërë standardi de facto në ekosistemin React/Node, dhe gjithnjë e më shumë ekipe Angular e adoptojnë në vend të Karma për tre arsye konkrete:

  • Shpejtësi: Jest i ekzekuton testet në Node.js me JSDOM, pa hapur një browser real Chrome — dukshëm më i shpejtë, sidomos në CI.
  • Watch mode inteligjent: jest --watch ekzekuton vetëm testet e lidhura me skedarët e ndryshuar (përmes analizës së git diff).
  • Snapshot testing dhe mocking i integruar: jest.fn(), jest.mock() dhe snapshot-et janë native, pa libraritë shtesë.
npm install --save-dev jest jest-preset-angular @types/jest
npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter
// jest.config.js
module.exports = {
  preset: 'jest-preset-angular',
  setupFilesAfterEach: ['/setup-jest.ts'],
  testPathIgnorePatterns: ['/node_modules/', '/dist/'],
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageThreshold: {
    global: {
      statements: 80,
      branches: 75,
      functions: 80,
      lines: 80,
    },
  },
  transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
};
// setup-jest.ts
import 'jest-preset-angular/setup-jest';
// package.json (estratto)
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:ci": "jest --ci --coverage --maxWorkers=2"
  }
}

1.11 — Jasmine vs Jest: dallime praktike në sintaksë

Lajm i mirë: sintaksa describe/it/expect është pothuajse identike, kështu që migrimi i pjesës më të madhe të testeve shpesh është vetëm çështje konfigurimi. Dallimet shfaqen kryesisht te mocking-u.

AspektiJasmineJest
Krijimi i një spyjasmine.createSpy('name')jest.fn()
Mockimi i një moduli të tërëJo nativ (nevojitet DI)jest.mock('./module')
Konfigurimi i vlerës së kthimitspy.and.returnValue(x)fn.mockReturnValue(x)
Snapshot testingNuk mbështetet nativishtexpect(x).toMatchSnapshot()
Timer të rremëfakeAsync + tick()jest.useFakeTimers() + jest.advanceTimersByTime()
Mjedisi i ekzekutimitBrowser real (Karma + Chrome)Node.js + JSDOM
Shpejtësi tipike (500 teste)~25-40s~8-15s
// Shembull mockimi moduli me Jest — i dobishëm për librari të jashtme të rënda
jest.mock('./analytics.service', () => ({
  AnalyticsService: jest.fn().mockImplementation(() => ({
    track: jest.fn(),
    identify: jest.fn(),
  })),
}));

describe('CheckoutComponent con Jest', () => {
  it('traccia l\'evento purchase al completamento ordine', () => {
    const trackSpy = jest.fn();
    // ... konfigurimi i komponentit me mock-un ...
    expect(trackSpy).toHaveBeenCalledWith('purchase', expect.objectContaining({ orderId: expect.any(String) }));
  });
});

1.12 — Snapshot testing: kur ta përdorësh (dhe kur ta shmangësh)

Snapshot testet kapin output-in e renderuar të një komponenti dhe e krahasojnë me një version të ruajtur në disk. Janë të dobishme për komponentë thjesht prezantues me output stabil, por bëhen problem kur ekipi i përditëson automatikisht me --ci=false --updateSnapshot pa rishikuar diff-in — duke i kthyer në një kontroll thjesht formal.

it('il badge di stato ordine ha il markup atteso', () => {
  const fixture = TestBed.createComponent(OrderStatusBadgeComponent);
  fixture.componentRef.setInput('status', 'shipped');
  fixture.detectChanges();

  expect(fixture.nativeElement.innerHTML).toMatchSnapshot();
});

Rregull praktik: përdor snapshot-et vetëm për markup të thjeshtë e stabël (badge, ikona, formatues), kurrë për komponentë me logjikë biznesi — atje një asertim eksplicit (expect(text).toBe('Spedito')) e komunikon qëllimin shumë më mirë se një snapshot opak prej 200 rreshtash HTML.

1.13 — Praktika më të mira transversale për unit teste efektive

  • Modeli AAA (Arrange-Act-Assert): struktoro çdo test në tre blloqe të qarta: përgatit të dhënat, kryej veprimin, verifiko rezultatin.
  • Një asertim konceptual për test: disa expect në të njëjtin test janë në rregull nëse verifikojnë të njëjtën gjë nga këndvështrime të ndryshme, por një test që verifikon tre sjellje të pavarura duhet ndarë në tre.
  • Mos testo detaje implementimi: testo sjelljen e vëzhgueshme (output, ngjarje të emetuara, DOM i renderuar), jo variabla private apo thirrje metodash të brendshme.
  • Emra testesh përshkrues: it('çaktivizon butonin kur karroca është bosh') është pafundësisht më i dobishëm se it('test 3') kur një test dështon në CI në orën 2 të natës.
  • Teste të pavarura: çdo test duhet të mund të ekzekutohet vetëm, në çdo radhë. Nëse një test varet nga gjendja e lënë nga një tjetër, është një test i brishtë.
  • Coverage si tregues, jo si qëllim: 100% code coverage me asertime të dobëta (expect(true).toBe(true)) është më keq se 70% me asertime solide. Përdor coverage-in për të gjetur kod të patestuar, jo si KPI për ta maksimizuar me çdo kusht.

1.14 — Testimi i Guard dhe Resolver

Route guards dhe resolvers janë shpesh vendi ku fshihet logjika më delikate e autorizimit të një aplikacioni — dhe është pikërisht ai lloj kodi që duhet të ketë mbulim solid testesh, sepse një bug këtu do të thotë përdorues të paautentikuar që hyjnë në faqe të mbrojtura, ose e kundërta.

// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated()) {
    return true;
  }

  router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
  return false;
};

// auth.guard.spec.ts
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { authGuard } from './auth.guard';
import { AuthService } from './auth.service';

describe('authGuard', () => {
  let authServiceSpy: jasmine.SpyObj;
  let routerSpy: jasmine.SpyObj;

  beforeEach(() => {
    authServiceSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated']);
    routerSpy = jasmine.createSpyObj('Router', ['navigate']);

    TestBed.configureTestingModule({
      providers: [
        { provide: AuthService, useValue: authServiceSpy },
        { provide: Router, useValue: routerSpy },
      ],
    });
  });

  it('permette la navigazione se l\'utente è autenticato', () => {
    authServiceSpy.isAuthenticated.and.returnValue(true);

    const result = TestBed.runInInjectionContext(() =>
      authGuard({} as any, { url: '/dashboard' } as any),
    );

    expect(result).toBeTrue();
    expect(routerSpy.navigate).not.toHaveBeenCalled();
  });

  it('reindirizza al login con returnUrl se non autenticato', () => {
    authServiceSpy.isAuthenticated.and.returnValue(false);

    const result = TestBed.runInInjectionContext(() =>
      authGuard({} as any, { url: '/dashboard/settings' } as any),
    );

    expect(result).toBeFalse();
    expect(routerSpy.navigate).toHaveBeenCalledWith(['/login'], {
      queryParams: { returnUrl: '/dashboard/settings' },
    });
  });
});

Testimi i guard-it si një funksion i thjeshtë (falë TestBed.runInInjectionContext) në vend që të montohet një komponent i tërë rrote është shumë më i shpejtë dhe e fokuson saktë logjikën e autorizimit, pa zhurmë nga rendering-u.

1.15 — Testimi i Pipe dhe Direktivave të personalizuara

// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
  transform(value: string, maxLength = 100, suffix = '…'): string {
    if (!value || value.length <= maxLength) return value;
    return value.slice(0, maxLength).trim() + suffix;
  }
}

// truncate.pipe.spec.ts
describe('TruncatePipe', () => {
  const pipe = new TruncatePipe();

  it('non modifica stringhe più corte del limite', () => {
    expect(pipe.transform('Ciao mondo', 20)).toBe('Ciao mondo');
  });

  it('tronca e aggiunge il suffisso di default', () => {
    const result = pipe.transform('Questo è un testo piuttosto lungo da troncare', 10);
    expect(result).toBe('Questo è u…');
  });

  it('gestisce stringhe vuote o undefined senza errori', () => {
    expect(pipe.transform('', 10)).toBe('');
  });
});

// highlight-on-hover.directive.ts
import { Directive, ElementRef, HostListener, inject, input } from '@angular/core';

@Directive({ selector: '[appHighlightOnHover]', standalone: true })
export class HighlightOnHoverDirective {
  private readonly el = inject(ElementRef);
  color = input('#fff3cd');

  @HostListener('mouseenter') onEnter(): void {
    this.el.nativeElement.style.backgroundColor = this.color();
  }

  @HostListener('mouseleave') onLeave(): void {
    this.el.nativeElement.style.backgroundColor = '';
  }
});

// highlight-on-hover.directive.spec.ts
@Component({
  standalone: true,
  imports: [HighlightOnHoverDirective],
  template: `
Testo
`, }) class HostComponent {} describe('HighlightOnHoverDirective', () => { it('applica il colore di sfondo al mouseenter e lo rimuove al mouseleave', () => { const fixture = TestBed.createComponent(HostComponent); fixture.detectChanges(); const div: HTMLElement = fixture.nativeElement.querySelector('div'); div.dispatchEvent(new Event('mouseenter')); expect(div.style.backgroundColor).toBe('rgb(255, 0, 0)'); div.dispatchEvent(new Event('mouseleave')); expect(div.style.backgroundColor).toBe(''); }); });

Pipe-t dhe direktivat, duke qenë njësi shumë të vogla logjike pa varësi të rënda, janë ndër elementet më ekonomike për t'u testuar në mënyrë shterruese: pak minuta shkrimi garantojnë mbulim pothuajse të plotë të çdo edge case (varg bosh, vlerë kufitare, input i keqformuar).

Pjesa 2: Testimi E2E me Cypress dhe Playwright

2.1 — Përse nevojiten testet E2E përtej unit testeve

Unit testet verifikojnë që pjesët individuale funksionojnë të izoluara, por nuk mund të kapin probleme integrimi: routing i konfiguruar keq, CSS që fsheh një buton, CORS i bllokuar vetëm në një mjedis specifik, ose një regresion në flukun e plotë të checkout-it. Testet E2E hapin një browser real (ose një headless), lundrojnë aplikacionin ashtu si do të bënte një përdorues real dhe verifikojnë rezultatin final.

2.2 — Cypress: instalimi dhe struktura e projektit

npm install --save-dev cypress
npx cypress open   # mënyra interaktive, e dobishme gjatë zhvillimit
npx cypress run    # mënyra headless, për CI
// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:4200',
    supportFile: 'cypress/support/e2e.ts',
    specPattern: 'cypress/e2e/**/*.cy.ts',
    viewportWidth: 1280,
    viewportHeight: 800,
    video: false, // i çaktivizuar lokalisht, shpesh i riaktivizuar në CI për debug
    retries: {
      runMode: 2,  // riprovon testet e dështuara 2 herë në CI (zbut flakiness e rrjetit)
      openMode: 0,
    },
    env: {
      apiUrl: 'http://localhost:3000/api',
    },
  },
});

Struktura tipike e një projekti Cypress:

cypress/
├── e2e/
│   ├── auth/
│   │   ├── login.cy.ts
│   │   └── register.cy.ts
│   ├── checkout/
│   │   └── checkout-flow.cy.ts
│   └── blog/
│       └── blog-navigation.cy.ts
├── fixtures/
│   └── products.json
├── support/
│   ├── commands.ts
│   └── e2e.ts

2.3 — Testi i parë Cypress: fluksi i login-it

// cypress/e2e/auth/login.cy.ts
describe('Login', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('effettua il login con credenziali valide e reindirizza alla dashboard', () => {
    cy.get('[data-testid="email-input"]').type('utente@example.com');
    cy.get('[data-testid="password-input"]').type('PasswordSicura123!');
    cy.get('[data-testid="login-submit"]').click();

    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="welcome-message"]').should('contain.text', 'Bentornato');
  });

  it('mostra un errore con credenziali errate', () => {
    cy.get('[data-testid="email-input"]').type('utente@example.com');
    cy.get('[data-testid="password-input"]').type('passwordSbagliata');
    cy.get('[data-testid="login-submit"]').click();

    cy.get('[data-testid="login-error"]')
      .should('be.visible')
      .and('contain.text', 'Credenziali non valide');
    cy.url().should('include', '/login');
  });

  it('disabilita il pulsante submit finché i campi non sono validi', () => {
    cy.get('[data-testid="login-submit"]').should('be.disabled');
    cy.get('[data-testid="email-input"]').type('non-una-email');
    cy.get('[data-testid="login-submit"]').should('be.disabled');
    cy.get('[data-testid="email-input"]').clear().type('valida@example.com');
    cy.get('[data-testid="password-input"]').type('almeno8caratteri');
    cy.get('[data-testid="login-submit"]').should('not.be.disabled');
  });
});

2.4 — Përgjimi i thirrjeve të rrjetit me cy.intercept

Një test E2E që varet nga një backend real është i ngadaltë dhe i brishtë (të dhëna që ndryshojnë, shërbime të jashtme jashtë funksionimit). cy.intercept lejon të stubosh përgjigjet HTTP duke e mbajtur testin realist por deterministik.

// cypress/e2e/checkout/checkout-flow.cy.ts
describe('Flusso di checkout completo', () => {
  beforeEach(() => {
    cy.intercept('GET', '/api/cart', { fixture: 'cart-with-items.json' }).as('getCart');
    cy.intercept('POST', '/api/orders', {
      statusCode: 201,
      body: { orderId: 'ORD-9981', status: 'confirmed' },
    }).as('submitOrder');

    cy.visit('/checkout');
    cy.wait('@getCart');
  });

  it('completa un ordine e mostra la conferma', () => {
    cy.get('[data-testid="shipping-name"]').type('Mario Rossi');
    cy.get('[data-testid="shipping-address"]').type('Via Roma 1, Milano');
    cy.get('[data-testid="place-order"]').click();

    cy.wait('@submitOrder').its('request.body').should('deep.include', {
      shippingName: 'Mario Rossi',
    });

    cy.get('[data-testid="order-confirmation"]').should('contain.text', 'ORD-9981');
  });

  it('mostra un errore se il pagamento viene rifiutato dal backend', () => {
    cy.intercept('POST', '/api/orders', {
      statusCode: 402,
      body: { message: 'Pagamento rifiutato' },
    }).as('submitOrderFailed');

    cy.get('[data-testid="shipping-name"]').type('Mario Rossi');
    cy.get('[data-testid="shipping-address"]').type('Via Roma 1, Milano');
    cy.get('[data-testid="place-order"]').click();

    cy.wait('@submitOrderFailed');
    cy.get('[data-testid="checkout-error"]').should('contain.text', 'Pagamento rifiutato');
  });
});

2.5 — Custom commands: DRY në testet Cypress

Veprime të përsëritura në shumë teste (login, shtimi i një produkti në karrocë) duhen nxjerrë në custom commands për të shmangur dublikimin.

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      loginViaApi(email: string, password: string): Chainable;
    }
  }
}

Cypress.Commands.add('loginViaApi', (email: string, password: string) => {
  cy.request('POST', '/api/auth/login', { email, password }).then(response => {
    window.localStorage.setItem('authToken', response.body.token);
  });
});

export {};
// Përdorim në një test — login "i heshtur" via API në vend të plotësimit të formularit çdo herë
describe('Dashboard (utente già autenticato)', () => {
  beforeEach(() => {
    cy.loginViaApi('utente@example.com', 'PasswordSicura123!');
    cy.visit('/dashboard');
  });

  it('mostra le statistiche dell\'utente', () => {
    cy.get('[data-testid="stats-panel"]').should('be.visible');
  });
});

Kryerja e login-it via thirrje direkte API (cy.request) në vend të UI-së çdo herë është një nga optimizimet më efektive për të shpejtuar një suite E2E: një test që nuk duhet të testojë vetë login-in nuk duhet të kalojë as nëpër të via interface.

2.6 — Cypress Component Testing

Përtej testeve klasike E2E, Cypress mbështet Component Testing: monton një komponent të vetëm Angular të izoluar në një browser real, duke bashkuar shpejtësinë e unit testeve me besnikërinë e një rendering-u real (pa JSDOM).

// quantity-selector.cy.ts (Cypress Component Testing)
import { QuantitySelectorComponent } from './quantity-selector.component';

describe('QuantitySelectorComponent (component test)', () => {
  it('incrementa il valore al click su +', () => {
    cy.mount(QuantitySelectorComponent, {
      componentProperties: { value: 5, max: 10 },
    });

    cy.get('[data-testid="increment"]').click();
    cy.get('[data-testid="quantity-value"]').should('have.text', '6');
  });
});

2.7 — Playwright: instalimi dhe konfigurimi

npm init playwright@latest
# Instalon browser-at e nevojshëm (Chromium, Firefox, WebKit)
npx playwright install
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [['html', { open: 'never' }], ['github']],
  use: {
    baseURL: 'http://localhost:4200',
    trace: 'on-first-retry', // regjistron një trace vetëm kur një test dështon dhe riprovohet
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
  ],
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:4200',
    reuseExistingServer: !process.env.CI,
  },
});

2.8 — Testi i parë Playwright: i njëjti fluks login-i

// e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Login', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('effettua il login con credenziali valide', async ({ page }) => {
    await page.getByTestId('email-input').fill('utente@example.com');
    await page.getByTestId('password-input').fill('PasswordSicura123!');
    await page.getByTestId('login-submit').click();

    await expect(page).toHaveURL(/\/dashboard/);
    await expect(page.getByTestId('welcome-message')).toContainText('Bentornato');
  });

  test('mostra un errore con credenziali non valide', async ({ page }) => {
    await page.getByTestId('email-input').fill('utente@example.com');
    await page.getByTestId('password-input').fill('sbagliata');
    await page.getByTestId('login-submit').click();

    await expect(page.getByTestId('login-error')).toBeVisible();
    await expect(page.getByTestId('login-error')).toContainText('Credenziali non valide');
  });
});

2.9 — Locators, auto-waiting dhe network mocking

Playwright përdor locators, objekte "lazy" që rilidhen automatikisht me DOM-in para çdo veprimi, me auto-waiting të integruar: nuk nevojitet asnjë sleep apo pritje eksplicite, Playwright pret automatikisht që elementi të jetë i dukshëm, aktiv dhe stabël para se të ndërveprojë me të.

// e2e/checkout/checkout-flow.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Flusso di checkout', () => {
  test.beforeEach(async ({ page }) => {
    await page.route('**/api/cart', route =>
      route.fulfill({ path: 'e2e/fixtures/cart-with-items.json' }),
    );
    await page.route('**/api/orders', route =>
      route.fulfill({
        status: 201,
        json: { orderId: 'ORD-9981', status: 'confirmed' },
      }),
    );
    await page.goto('/checkout');
  });

  test('completa un ordine e mostra la conferma', async ({ page }) => {
    await page.getByTestId('shipping-name').fill('Mario Rossi');
    await page.getByTestId('shipping-address').fill('Via Roma 1, Milano');

    const [orderRequest] = await Promise.all([
      page.waitForRequest('**/api/orders'),
      page.getByTestId('place-order').click(),
    ]);

    expect(orderRequest.postDataJSON()).toMatchObject({ shippingName: 'Mario Rossi' });
    await expect(page.getByTestId('order-confirmation')).toContainText('ORD-9981');
  });
});

2.10 — Ekzekutimi paralel cross-browser dhe Trace Viewer

Një nga pikat e forta të Playwright është ekzekutimi nativ paralel në Chromium, Firefox dhe WebKit pa infrastrukturë shtesë (grid, cloud provider). Kur një test dështon në CI, trace-i i regjistruar lejon rishikimin e të gjithë ekzekutimit — DOM snapshot, network, console — hap pas hapi, si një video interaktive.

# Ekzekuton suite-n në të gjithë browser-at e konfiguruar, paralelisht
npx playwright test

# Ekzekuton vetëm në Chromium me UI interaktive debug
npx playwright test --project=chromium --ui

# Analizon trace-n e një testi të dështuar në CI
npx playwright show-trace trace.zip

2.11 — Cypress vs Playwright: tabelë krahasuese

KarakteristikaCypressPlaywright
ArkitekturaEkzekutohet brenda browser-it (i njëjti event loop me app-in)Kontrollon browser-in nga jashtë përmes një protokolli (CDP/WebSocket)
Browser të mbështeturChrome, Edge, Firefox, ElectronChromium, Firefox, WebKit (pra edhe Safari)
Multi-tab / multi-domainMbështetje historikisht e kufizuarNative, pa workaround
Ekzekutim paralelKërkon Cypress Cloud (me pagesë) ose sharding manualNative, falas, e integruar në config
Shpejtësi tipikeE mirëPërgjithësisht superiore, sidomos në CI
Mjete debugTime-travel debugging në UI interaktiveTrace Viewer + UI Mode
Component testingPo, i pjekurPo (Playwright Component Testing, më i ri)
Kurbë mësimiShumë e butë, API shumë e lexueshmePak më e gjerë (async/await kudo)

Në praktikë: nëse ekipi është tashmë i mësuar me Cypress dhe projekti nuk kërkon Safari/WebKit, Cypress mbetet një zgjedhje e shkëlqyer për developer experience-in e tij. Nëse nevojitet mbulim real multi-browser (përfshirë Safari) dhe paralelizim nativ pa kosto shtesë, Playwright është sot zgjedhja më solide për projekte të reja.

2.12 — Integrimi në CI/CD me GitHub Actions

# .github/workflows/e2e-playwright.yml
name: E2E Tests (Playwright)

on:
  pull_request:
    branches: [main, develop]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npx playwright install --with-deps

      - name: Build applicazione
        run: npm run build

      - name: Esegui test E2E
        run: npx playwright test

      - name: Carica report HTML come artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14
# .github/workflows/e2e-cypress.yml (equivalente con Cypress)
name: E2E Tests (Cypress)

on:
  pull_request:
    branches: [main, develop]

jobs:
  cypress:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cypress-io/github-action@v6
        with:
          build: npm run build
          start: npm run start
          wait-on: 'http://localhost:4200'
          browser: chrome

Një detaj shpesh i lënë pas dore: ekzekutimi i testeve E2E vetëm në pull request që prekin kod frontend (përmes paths: në trigger) shmang humbjen e minutave CI në ndryshime që prekin vetëm, për shembull, dokumentacionin ose backend-in.

2.13 — Visual regression testing me Playwright

Përtej asertimeve funksionale, Playwright lejon kapjen e screenshot-eve dhe krahasimin e tyre automatik me një version referencë të ruajtur në disk — i dobishëm për të kapur regresione thjesht vizuale (një margjinë që ndryshon, një ngjyrë e gabuar) që asnjë asertim tekstual nuk do ta zbulonte.

// e2e/visual/product-card.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Regressione visiva — ProductCard', () => {
  test('la card prodotto corrisponde allo screenshot di riferimento', async ({ page }) => {
    await page.goto('/prodotti/tastiera-meccanica');
    const card = page.getByTestId('product-card');

    // Ekzekutimi i parë gjeneron screenshot-in referencë (--update-snapshots);
    // ekzekutimet pasuese dështojnë nëse ka dallime pikselësh përtej pragut.
    await expect(card).toHaveScreenshot('product-card-baseline.png', {
      maxDiffPixelRatio: 0.02,
    });
  });

  test('la card in stato "esaurito" ha un badge visivamente distinto', async ({ page }) => {
    await page.goto('/prodotti/tastiera-meccanica?stock=0');
    await expect(page.getByTestId('product-card')).toHaveScreenshot('product-card-sold-out.png');
  });
});
# Përditëson screenshot-et referencë pas një ndryshimi UI të qëllimshëm
npx playwright test --update-snapshots

# Në CI, ruaj gjithmonë diff-et e gjeneruara si artifact për rishikim vizual

Rregull praktik: pragu maxDiffPixelRatio duhet kalibruar për të toleruar variacione të vogla anti-aliasing mes mjediseve të ndryshme (lokal vs CI), përndryshe testi bëhet flaky për arsye që nuk kanë lidhje me një bug real vizual.

Pjesa 3: Truket e Debugging me Angular DevTools

3.1 — Instalimi dhe nisja e parë

Angular DevTools është një ekstension zyrtar për Chrome dhe Firefox që shton një panel dedikuar mjeteve të zhvillimit të browser-it. Pas instalimit nga Chrome Web Store, do të shfaqet një skedë "Angular" te DevTools (F12) e çdo faqeje që ekzekuton një aplikacion Angular në mënyrën development (në prodhim, me enableProdMode() aktiv, metadatat e nevojshme hiqen për arsye performance dhe sigurie).

3.2 — Component Explorer: inspektimi i pemës së komponentëve

Skeda Components tregon të gjithë pemën e komponentëve të renderuar, në mënyrë të ngjashme me pemën DOM por në nivel komponentësh Angular në vend të tag-eve HTML. Duke zgjedhur një komponent mund të inspektohen në kohë reale:

  • Vlerat aktuale të të gjithë @Input() dhe signal-eve të kaluara si input.
  • Gjendja e brendshme e komponentit (properties, signal, computed).
  • Hierarkia e komponentëve prind/fëmijë, për të kuptuar nga vjen një e dhënë e papritur.

Është gjithashtu e mundur të modifikohet në kohë reale vlera e një property-je nga paneli dhe të shihet komponenti duke u rirenderuar menjëherë — jashtëzakonisht e dobishme për të riprodhuar një edge case (p.sh. një listë bosh, një çmim negativ) pa dashur të ndryshohen të dhënat në burim.

3.3 — Injector Tree: kuptimi se nga vjen një shërbim

Skeda Injector Tree vizualizon hierarkinë e injector-ëve të Dependency Injection të aplikacionit — e dobishme për të diagnostikuar bug të tipit "përse ky komponent merr një instancë të ndryshme të shërbimit krahasuar me atë ngjitur?", tipikisht të shkaktuar nga një provider i deklaruar në nivel komponenti në vend të root, që krijon një instancë të izoluar për atë nën-pemë.

3.4 — Profiler: regjistrimi i një cikli Change Detection

Skeda Profiler është mjeti më i fuqishëm për debugging-un e performancës. Duke shtypur "Start recording" dhe duke ndërvepruar me app-in (klikime, scroll, shkrim), Angular DevTools regjistron çdo cikël change detection dhe prodhon një grafik shiritash — të ngjashëm me një flame graph — ku çdo shirit përfaqëson një komponent dhe lartësia e tij kohën e shpenzuar për të kontrolluar binding-et e tij.

// Skenar tipik për t'u diagnostikuar me Profiler:
// një komponent liste që rirenderohet me çdo shkronjë të shtypur në një fushë kërkimi,
// edhe pse të dhënat e listës nuk kanë ndryshuar.

@Component({
  selector: 'app-product-list',
  standalone: true,
  // ❌ Pa OnPush, Angular kontrollon këtë komponent në ÇDO cikël CD
  // të të gjithë aplikacionit, edhe për shkrime në komponentë të palidhur.
  template: ``,
})
export class ProductListComponent {
  @Input() products: Product[] = [];
}

Në Profiler, kjo shfaqet si një shirit i gjerë dhe i përsëritur për ProductListComponent në çdo ngjarje të vetme input-i, edhe kur përdoruesi po shkruan thjesht në një fushë që nuk ka lidhje me listën e produkteve.

3.5 — Nga diagnoza te zgjidhja: ChangeDetectionStrategy.OnPush

@Component({
  selector: 'app-product-list',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (p of products(); track p.id) {
      
    }
  `,
})
export class ProductListComponent {
  products = input([]);
}

Me OnPush, Angular kontrollon komponentin vetëm kur: (1) një @Input/signal input ndryshon nga referenca, (2) një ngjarje DOM ndodh brenda tij, ose (3) shënohet eksplicitisht me markForCheck(). Duke rinisur regjistrimin në Profiler pas këtij ndryshimi, shiriti i ProductListComponent zhduket gjatë shkrimit në fushën e kërkimit — prova vizuale që optimizimi ka funksionuar.

3.6 — trackBy (ose track në sintaksën e re @for) për lista të mëdha

Një tjetër shkak i zakonshëm i pengesave i dukshëm në Profiler: lista që rirenderohen plotësisht (shkatërrohen dhe rikrijohen) me çdo përditësim, në vend që të përditësohen vetëm elementet realisht të ndryshuar.


@for (product of products(); track $index) {
  
}


@for (product of products(); track product.id) {
  
}

Në Profiler, efekti i track $index në një listë që riorganizohet (p.sh. pas një operacioni sort) shihet si një numër i lartë komponentësh ProductCardComponent të shkatërruar dhe rikrijuar, në vend të një riposicionimi të thjeshtë — një sinjal i qartë që track-u i zgjedhur nuk i identifikon saktë elementet.

3.7 — Kombinimi i Angular DevTools me panelin Performance të Chrome

Angular DevTools shkëlqen në tregimin e cilëve komponentë Angular po kontrollon, por nuk hyn në detaje mbi çfarë ndodh në nivel motori JavaScript (garbage collection, layout thrashing, long tasks). Për një analizë të plotë, regjistrohet një sesion paralel te skeda native Performance e Chrome DevTools:

  • Long Tasks (shiritat e kuq mbi 50ms) tregojnë punë sinkrone që bllokon thread-in kryesor — shpesh një cikël change detection tepër i rëndë.
  • Seksioni Layout / Reflow sinjalizon lexime/shkrime DOM të alternuara në mënyrë joefikase (p.sh. leximi i offsetHeight brenda një loop-i që më pas shkruan stile).
  • Grafiku i memories në kohë ndihmon të gjenden memory leak — tipikisht subscription RxJS të pashkëputura në komponentë të shkatërruar dhe rikrijuar përsëritshmërisht (p.sh. në një route të rihapur shpesh).
// Shkak i zakonshëm memory leak: subscription manuale asnjëherë e pashkëputur
export class DashboardComponent implements OnInit {
  ngOnInit(): void {
    // ❌ Çdo herë që komponenti rikrijohet, akumulohet një subscription i ri
    interval(5000).subscribe(() => this.refreshStats());
  }
}

// ✅ Korrigjuar me takeUntilDestroyed — subscription pastrohet
//    automatikisht kur komponenti shkatërrohet nga Angular
export class DashboardComponent implements OnInit {
  private readonly destroyRef = inject(DestroyRef);

  ngOnInit(): void {
    interval(5000)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => this.refreshStats());
  }
}

3.8 — Truke shtesë console: console.table, console.time, debugger

Jo çdo debugging kërkon mjete dedikuara. Disa metoda native të console-s, shpesh të harruara, përshpejtojnë shumë analizën e të dhënave komplekse:

// console.table: shfaq array objektesh si tabelë e lexueshme,
// shumë më e dobishme se console.log për debugging listash të dhënash
console.table(this.products());

// console.time / console.timeEnd: mat shpejt kohëzgjatjen e një blloku
// pa nevojën për të importuar mjete profiling të jashtme
console.time('calcolaTotaleCarrello');
const total = this.calculateTotal();
console.timeEnd('calcolaTotaleCarrello'); // printon: calcolaTotaleCarrello: 2.341ms

// console.trace: printon stack-un e plotë të thirrjes — jashtëzakonisht i dobishëm për të
// kuptuar NGA vjen thirrur një funksion i thirrur nga shumë vende në kod
someSharedUtilityFunction(): void {
  console.trace('someSharedUtilityFunction chiamata da:');
}

// Instruksioni debugger ndërpret ekzekutimin saktësisht si një breakpoint
// i vendosur manualisht, por jeton në kodin burimor — i dobishëm për kushte të rralla
if (order.total < 0) {
  debugger; // browser-i ndalon këtu VETËM nëse DevTools është i hapur
}

Një këshillë e fundit praktike: te breakpoint-et kushtëzuar të Chrome DevTools (klikim i djathtë mbi numrin e rreshtit → "Add conditional breakpoint") mund të futet një shprehje si product.price < 0, duke ndaluar ekzekutimin vetëm kur kushti është i vërtetë, në vend që të duhet klikuar "continue" dhjetëra herë brenda një loop-i.

3.9 — Heap snapshot: identifikimi i memory leak me precizion

Kur Profiler-i i Angular DevTools tregon një aplikacion që ngadalësohet progresivisht pas disa minutash përdorimi (simptomë klasike e memory leak), hapi tjetër është skeda Memory e Chrome DevTools:

  1. Lundro app-in deri në një gjendje "të pastër" (p.sh. dashboard bosh), pastaj regjistro një Heap snapshot të parë.
  2. Kryej veprimin e dyshimtë përsëritshmërisht (p.sh. hap dhe mbyll një dialog 10 herë).
  3. Detyro një garbage collection manuale (ikona e kosit) dhe regjistro një Heap snapshot të dytë.
  4. Përdor pamjen "Comparison" mes dy snapshot-eve: objektet që vazhdojnë të rriten në numër (p.sh. instanca DialogComponent që duhej të ishin shkatërruar) janë simptoma e leak-ut.
// Shkak tipik i zbulueshëm në një Comparison heap snapshot:
// një EventEmitter i personalizuar ose një subscription RxJS e mbajtur gjallë nga një shërbim singleton,
// që nga ana e vet mban një referencë te komponenti dhe pengon garbage collection.

@Injectable({ providedIn: 'root' })
export class NotificationBusService {
  private readonly _messages = new Subject();
  readonly messages$ = this._messages.asObservable();
}

// ❌ Komponenti abonohet te shërbimi singleton por nuk çabonohet kurrë:
// çdo instancë e komponentit mbetet "e varur" te Subject-i përgjithmonë.
export class ToastComponent implements OnInit {
  constructor(private bus: NotificationBusService) {}
  ngOnInit(): void {
    this.bus.messages$.subscribe(msg => this.show(msg));
  }
}

Në një aplikacion ku komponenti ToastComponent krijohet dhe shkatërrohet përsëritshmërisht (p.sh. brenda një route-i që përdoruesi vizitoi shpesh), ky subscription i vetëm i pashkëputur akumulon një referencë për çdo instancë të krijuar ndonjëherë — pikërisht modeli që një Comparison heap snapshot e bën të dukshëm me një numër instancash "Detached" që rritet pa u kthyer kurrë në zero.

Përfundim: Ndërtimi i një Rrjeti Sigurie Shumështresor

Asnjë nga këto mjete nuk zëvendëson tjetrat: unit testet e shpejta me Jest ose Jasmine/Karma japin besim të menjëhershëm për çdo njësi të vetme kodi me çdo ruajtje; testet E2E me Cypress ose Playwright validojnë që flukset kritike (login, checkout, kërkim) funksionojnë vërtet end-to-end para lëshimit; Angular DevTools ndërhyn kur nevojitet të kuptohet përse diçka është e ngadaltë ose sillet në mënyrë të papritur, gjë që asnjë test automatik i vetëm nuk mund ta diagnostikojë plotësisht.

Kombinimi i këtyre tre shtresave — shumëzuar me një CI që i ekzekuton automatikisht në çdo pull request — është ajo që e kthen një projekt Angular nga "funksionon në kompjuterin tim" në një produkt mbi të cilin ekipi mund të bëjë refactoring me besim, duke ditur që një regresion do të kapet para se të arrijë te përdoruesit.

  • Checklist minimale për një projekt Angular të pjekur:
  • ✅ Unit teste për çdo shërbim me logjikë jo-triviale, coverage minimale e konfiguruar në CI.
  • ✅ Teste E2E mbi 3-5 flukset realisht kritike për biznesin (jo mbi çdo faqe të vetme).
  • ChangeDetectionStrategy.OnPush si parazgjedhje për komponentët e rinj.
  • ✅ Një sesion profiling me Angular DevTools para çdo release të rëndësishëm mbi faqet me trafik të lartë.

💬 Shënime nga lexuesit

0 shënime

Shkruaj një shënim

Ndaj mendimin tënd, një sugjerim ose një kompliment

Shënimet e fundit

Ende asnjë shënim. Bëhu i pari që komenton!