Mohimi i përgjegjësisë: çdo shembull do të jetë në Typescript, Javascript ose Angular (por nuk është i rëndësishëm).

Me këtë artikull unë do të doja të grupoja së bashku bazat se si dhe pse shkrimi i testeve në fushën e softuerit është një domosdoshmëri. Nuk do të ketë asnjë ide të shkëlqyer, novatore, vetëm informacion dhe arsyetim për atë që ne "tashmë dimë".

Ne jemi zhvillues dhe e dimë, pak a shumë mirë, se testimi i kodit tonë është diçka që “duhet bërë”.

Por pse duhet bërë?

Ja pse…

1-Zbulimi i hershëm i defekteve

Testet e njësive janë shkruar për të testuar njësitë individuale të kodit, të tilla si funksionet dhe metodat, në izolim. Duke kapur gabime në fillim të ciklit të zhvillimit, ne mund të kursejmë kohë dhe burime që përndryshe do të shpenzoheshin për rregullimin e gabimeve më vonë në ciklin e zhvillimit.

2-Përmirësimi i cilësisë së kodit

Kur shkruajmë teste njësie, jemi të detyruar të mendojmë se si duhet të sillet kodi në skenarë të ndryshëm. Kjo na ndihmon të shkruajmë kodin më të mirë që është më i mirëmbajtur, i zgjerueshëm dhe i lexueshëm.

2.1-Zbatimi i praktikave të mira të projektimit

Kur shkruajmë testet e njësive, jemi të detyruar të mendojmë se si ta izolojmë kodin nën testim nga varësitë e tij. Kjo na ndihmon të dizajnojmë kodin tonë në një mënyrë që të jetë modulare, e zgjerueshme dhe e testueshme.

3-Lehtësimi i rifaktorimit

Rifaktorimi është një praktikë e zakonshme në zhvillimin e softuerit dhe testet e njësive mund të ndihmojnë të sigurohemi që ndryshimet tona në bazën e kodeve nuk sjellin gabime ose regresione. Kur bëjmë ndryshime në kod, mund të ekzekutojmë testet e njësisë për t'u siguruar që sjellja e kodit nuk ka ndryshuar papritur.

4-Rritja e besimit në kod

Duke pasur një grup testesh të njësive që mbulojnë pjesën më të madhe të bazës së kodeve, mund të kemi besim se kodi po funksionon siç është menduar. Kjo mund të na japë besimin për të bëjmë ndryshime në bazën e kodeve pa frikë se mos prishim funksionalitetin ekzistues. Sa shpesh shohim një pjesë kodi që mund të shkruhet më mirë, por nuk e prekim nga frika se mos prishim diçka?

Tl;dr

Gjithçka do të jetë më mirë. 

Epo, por tani që e di që duhet të shkruaj teste, çfarë duhet të di konkretisht?

Ashtu si në çdo gjë në jetë, ne përmirësohemi përmes përvojës, sigurisht, megjithatë, ne mund të ndihmojmë veten duke kuptuar siç duhet bazat teorike, kështu që jam përpjekur të grupoj dhe thjeshtoj parimet teorike më të rëndësishme për të shkruar një kod të mirë të testueshëm.

E nxehet për të shkruar një kod të mirë të testueshëm?

1-Ndarja e shqetësimeve

Kodi duhet të organizohet në një mënyrë që të ndajë shqetësimet, në mënyrë që çdo pjesë e funksionalitetit të jetë përgjegjëse për një gjë. Kjo e bën më të lehtë testimin e secilës pjesë të funksionalitetit në izolim.

2-Injeksion varësie

Kodi duhet të dizajnohet në një mënyrë që të lejojë injektimin e varësive, në mënyrë që ato të zëvendësohen me zbatime të rreme ose të rreme gjatë testimit. Kjo e bën më të lehtë testimin e kodit në izolim dhe siguron që testet të mos varen nga sistemet ose të dhënat e jashtme.

Lidhje me 3 të lira

Kodi duhet të dizajnohet në një mënyrë që të minimizojë lidhjen midis pjesëve të ndryshme të sistemit. Kjo e bën më të lehtë testimin e çdo pjese të funksionalitetit në izolim dhe zvogëlon rrezikun e prishjes së ndryshimeve kur bëni modifikime.

Parimi i 4-Përgjegjësisë së vetme (SRP)

Çdo klasë dhe metodë duhet të ketë një përgjegjësi të vetme, në mënyrë që të kuptohet lehtë se çfarë bën kodi dhe për çfarë është përgjegjës. Kjo e bën më të lehtë shkrimin e testeve të fokusuara që testojnë sjellje specifike.

BONUS

Kodi duhet të hartohet duke pasur parasysh testueshmërinë që në fillim, në vend që të përpiqet të ripërshtatë testet në kodin ekzistues.

Këto janë vetëm parimet që unë personalisht i shoh më të dobishme për t'i mbajtur parasysh. Unë rekomandoj të mësoni më shumë rreth parimeve SOLID dhe modeleve të projektimit që ndihmojnë me bashkimin minimal (p.sh. modeli i kontejnerit, modeli i fasadës, etj.)

Sigurisht, nuk mjafton të shkruhet "kodi i mirë i testueshëm", por është gjithashtu i nevojshëm të shkruash teste të mira! Përsëri, jam përpjekur të grupoj së bashku parimet bazë që më duken të dobishme për t'u mbajtur parasysh për të shkruar teste të mira

Si të shkruani teste të mira?

Testoni sjelljen, jo zbatimin

Përqendrohuni në testimin e sjelljes së kodit, në vend të detajeve të zbatimit. Kjo siguron që testet të mbeten të rëndësishme edhe nëse zbatimi ndryshon.

import { calculateTotalPrice } from './price-calculator';

describe('calculateTotalPrice', ()=> {
    it('calculates the total price of items with tax included', ()=> {
        const items = [
            { name: 'foo', price: 10 },
            { name: 'bar', price: 20 }
        ];
        const taxRate = 0.1;

        const result = calculateTotalPrice(items, taxRate);

        expect(result).toBe(33);
    })

    it('returns 0 for empty items array', ()=> {
        const items = [];
        const taxRate = 0.1;

        const result = calculateTotalPrice(items, taxRate);

        expect(result).toBe(0)
    })
})

Në këtë test, ne po kontrollojmë sjelljen e funksionit të llogaritjes së çmimit total. Ne kemi dhënë një grup specifik të dhënash hyrëse (një grup artikujsh dhe një normë tatimore) dhe presim një rezultat specifik (çmimi total me tatimin e përfshirë). Ne nuk shqetësohemi për detajet e zbatimit se si funksioni arrin në atë rezultat — ne thjesht duam të verifikojmë që ai po sillet siç synohet.

Shkruani teste të izoluara

Çdo test duhet të jetë i izoluar dhe të mos varet nga rezultati i testeve të tjera. Kjo e bën më të lehtë identifikimin e shkakut rrënjësor të një dështimi dhe mirëmbajtjen e testeve me kalimin e kohës.

import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [DataService]
    });

    service = TestBed.inject(DataService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should retrieve a list of items from the server', () => {
    const items = [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' }
    ];

    service.getItems().subscribe(data => {
      expect(data.length).toBe(3);
      expect(data).toEqual(items);
    });

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

  it('should retrieve a single item from the server', () => {
    const item = { id: 1, name: 'Item 1' };

    service.getItem(1).subscribe(data => {
      expect(data).toEqual(item);
    });

    const req = httpMock.expectOne('api/items/1');
    expect(req.request.method).toBe('GET');
    req.flush(item);
  });
});

Në këtë shembull, ne kemi dy teste të izoluara për një DataService. Çdo test është i pavarur nga tjetri dhe teston një sjellje specifike të shërbimit. Hook-i BeforeEach përdoret për të konfiguruar shtratin e provës me varësitë e nevojshme, dhe fiksimi afterEach përdoret për të verifikuar që nuk ka kërkesa HTTP në pritje.

Testi i parë kontrollon nëse metoda getItems kthen listën e pritur të artikujve nga serveri. Testi i dytë kontrollon nëse metoda getItem kthen artikullin e pritur për një ID të caktuar.

Përdor të dhënat e provës

Përdorni të dhëna testimi që janë përfaqësuese të skenarëve të botës reale. Kjo ndihmon për të siguruar që testet të pasqyrojnë me saktësi se si do të sillet kodi në prodhim.

MIRË

const mockUserResponse = {
  id: 1,
  name: 'John Doe',
  email: '[email protected]',
  role: 'admin'
};

KEQ

const aaa = {
  id: 1,
  name: '1',
  email: '1',
  role: '1'
};

Testoni të gjitha shtigjet

Testoni të gjitha shtigjet e mundshme përmes kodit, duke përfshirë rastet e skajeve dhe kushtet e gabimit. Kjo ndihmon për të siguruar që kodi të sillet saktë në të gjitha situatat.

import { calculateTotalPrice } from './price-calculator';

describe('calculateTotalPrice', ()=> {
    it('calculates the total price of items with tax included', ()=> {
        const items = [
            { name: 'foo', price: 10 },
            { name: 'bar', price: 20 }
        ];
        const taxRate = 0.1;

        const result = calculateTotalPrice(items, taxRate);

        expect(result).toBe(33);
    })

    it('returns 0 for empty items array', ()=> {
        const items = [];
        const taxRate = 0.1;

        const result = calculateTotalPrice(items, taxRate);

        expect(result).toBe(0)
    })
})

Në këtë shembull (të treguar tashmë) përveç sjelljes normale, ne testuam edhe rastin kur grupi i artikujve është bosh.

Përdor pohimet

Përdorni pohime për të verifikuar që kodi sillet siç pritej. Kjo siguron që kodi po sillet në mënyrë korrekte dhe ndihmon në identifikimin e gabimeve ose sjelljeve të papritura.

it('should add two numbers togheter', () => {

  // ...

  expect(sum).toEqual(expectedSum);

})

E qartë, e lexueshme, faktike

Përdor përqeshjet

Përdorni tallje për të izoluar kodin nën provë nga varësitë e tij. Kjo ndihmon të sigurohet që testet të fokusohen në kodin që testohet dhe të mos ndikohen nga faktorë të jashtëm.

import { UserListComponent } from './user-list.component';
import { UserService } from './user.service';

describe('UserListComponent', () => {
  let component: UserListComponent;
  let userService: UserService;

  beforeEach(() => {
    userService = {
      getUsers: jest.fn()
    };
    component = new UserListComponent(userService);
  });

  it('should fetch and display the user data', () => {
    const mockUsers = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
      { id: 3, name: 'Charlie' }
    ];
    userService.getUsers.mockReturnValue(of(mockUsers));

    component.ngOnInit();

    expect(userService.getUsers).toHaveBeenCalled();
    expect(component.users).toEqual(mockUsers);
  });
});

Parimi më i rëndësishëm është padyshim i pari; këtu është një shembull i asaj që nënkuptohet me Shkruani teste të izoluara:

Testi i njësisë së izoluar mirë

Le të themi se kemi një shërbim të thjeshtë Calculator që ka një metodë shtimi që mbledh dy numra së bashku:

export class CalculatorService {
  add(a: number, b: number): number {
    return a + b;
  }
}

Një test i njësisë i izoluar mirë për këtë shërbim do të testonte metodën e shtimit në izolim, pa ndonjë varësi të jashtme. Këtu është një shembull se si mund të testojmë metodën e shtimit në izolim:

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

  beforeEach(() => {
    service = new CalculatorService();
  });

  it('should add two numbers together', () => {
    const result = service.add(2, 3);
    expect(result).toBe(5);
  });

  it('should handle negative numbers correctly', () => {
    const result = service.add(-2, 3);
    expect(result).toBe(1);
  });
});

Në këtë shembull, ne krijojmë një instancë të re të CalculatorService përpara çdo testi duke përdorur grepin BeforeEach. Më pas testojmë metodën e shtimit me vlera të ndryshme hyrëse dhe pohojmë se rezultatet janë të sakta duke përdorur funksionin pres.

Ky test është i izoluar mirë sepse teston vetëm metodën e shtimit të CalculatorService dhe nuk mbështetet në ndonjë varësi ose shërbim të jashtëm. Kjo e bën të lehtë për t'u kuptuar dhe mbajtur.

Test njësie e keqe, jo e izoluar

Tani le të imagjinojmë që Shërbimi ynë Calculator ka një varësi nga një shërbim i jashtëm, siç është një shërbim HTTP (kod jo i mirë i testueshëm). Këtu është një shembull i një testi të keq, jo të izoluar të njësisë që teston metodën e shtimit me këtë varësi të jashtme:

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ],
      providers: [ CalculatorService ]
    });

    service = TestBed.inject(CalculatorService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should add two numbers together', () => {
    const num1 = 2;
    const num2 = 3;
    const expectedSum = 5;

    service.add(num1, num2).subscribe(sum => {
      expect(sum).toBe(expectedSum);
    });

    const req = httpMock.expectOne('/api/add');
    expect(req.request.method).toBe('POST');
    req.flush(expectedSum);
  });
});

Në këtë test, ne thërrasim metodën e shtimit në CalculatorService, i cili dërgon një kërkesë HTTP në "/api/add" dhe pret një shumë në përgjigje. Më pas përdorim metodën httpMock.expectOne për të përgjuar kërkesën HTTP dhe përdorim metodën req.flush për të dërguar një përgjigje të rreme me shumën e pritur.

Ky test nuk është i izoluar mirë sepse mbështetet në një shërbim të jashtëm HTTP, i cili mund të shkaktojë dështimin e testit nëse ka probleme me rrjetin ose serverin. Ai gjithashtu e bën testin më kompleks dhe më të vështirë për t'u kuptuar, pasi ne po testojmë shumë gjëra në të njëjtën kohë.

Ka një ndryshim të madh, apo jo?

Për të përfunduar, siç mund të shohim nga shembujt e mësipërm për të shkruar teste të mira duhet të shkruajmë një kod të mirë të testueshëm, nëse mësojmë dhe mësohemi të shkruajmë një kod të mirë të testueshëm, do të jetë shumë më e lehtë të shkruajmë teste të dobishme dhe të lexueshme.

Shpresoj se ky artikull, megjithëse është bazë, mund të shërbejë si një pikënisje për thellimin dhe përmirësimin e bazave të shkrimit të një kodi të mirë të testueshëm.

Faleminderit dhe kodim të lumtur!