Základy programovania v JavaScripte

Lekcia 11

Milan Herda, 07/2020

Video k tejto lekcii:

Automatizované testovanie

Automatizované testovanie by malo byť neoddeliteľnou súčasťou programovania

Automatizované === nie ručné

Automatizované testy

  • dajú sa spúšťať automaticky
  • počas svojho behu nepotrebujú zásah človeka
  • píšu ich programátori
  • píšu ich testeri

Je veľa firiem, kde sa testy nepíšu, lebo...

  • "tlačí nás čas"
  • "veď sme to preklikali a testeri otestovali"
  • "ak bude chyba, používatelia nahlásia"
  • manažment nevyžaduje
  • programátori nie sú zvyknutí

Testy sa píšu, pretože

  • napísaný test je dôkaz, že kód sme otestovali a že funguje podľa očakávaní
  • beh automatizovaného testu je rýchlejší ako ručné prebehnutie testovacieho scenáru
  • testeri nie sú zaťažení repetitívnymi úlohami
  • programátori môžu robiť odvážnejšie zmeny
  • testy zvyšujú kvalitu napísaného kódu (ľahšie a rýchlejšie úpravy)

Testovacie nástroje pre JavaScript

  • je ich veľa, veľa
  • vhodné nástroje závisia aj od používaného JS frameworku

Ukážeme si:

  • cypress.io
  • mocha
  • chai

Inštalovali sme si na prvej hodine

Cypress je nástroj pre tzv. end-to-end testovanie web stránok.

To znamená, že netestuje váš kód izolovane, ale v kontexte celej aplikácie.

Cypress.io - inštalácia a spustenie

npm install cypress
npx cypress open

Napíšte do príkazového riadku:

Odbočka - lokálny web server

Bolo by vhodné, keby naša stránka s Karlom bolo ozajstná webstránka a bežala na nejakom web serveri.

Cypress k nej tak bude pristupovať cez HTTP protokol, čiže rovnako, akoby k nej pristupoval, keby bola umiestnená na internete.

Nainštalujeme a spustíme si veľmi jednoduchý web server.

Odbočka - lokálny web server

V adresári (priečinok, folder, zložka...), v ktorom máte uloženého Karla (index.html):

- otvorte príkazový riadok

- spustite príkaz:

npm install http-server

Tým sa nainštaluje http server. Teraz ho spustite príkazom:

npx http-server

Odbočka - lokálny web server

*Počas testovania si nezatvárajte okno s týmto terminálom a ani neukončujte beh http serveru.

*Operačný systém alebo firewall antivírusu vás po spustení tohto príkazu môžu obťažovať s otázkou, či naozaj chcete otvoriť port 8080 alebo s podobnou otázkou: áno, chcete to

Odbočka - lokálny web server

Teraz si vyskúšajte do prehliadača napísať jednu z URL adries, ktoré server vypísal.

Tip: namiesto 127.0.0.1 môžete napísať localhost

Cypress

Cypress po spustení cez npx cypress open otvorí webový prehliadač a ukáže vám zoznam testovacích scenárov.

Testovacie scenáre sú súbory s príponou .spec.js v adresári cypress/integration

V týchto súboroch sa nachádza javascriptový kód, ktorým hovoríme cypressu, čo má robiť.

Keď si v cypresse vyberieme nejaký scenár, tak on začne vykonávať príkazy napísané v súbore a podľa toho bude ovládať prehliadač a kontrolovať výsledky.

Cypress.io - otestovanie našej kalkulačky

Vytvoríme súbor cypress/integration/kalkulacka.spec.js

describe('kalkulacka', function () {
    it('by mala dobre počítať', function () {
        // kód testu
    });
});

describe a it

describe a it pochádzajú z frameworku Mocha

describe slúži na logické oddeľovanie skupín testov. Môže v sebe obsahovať viacero it a dokonca aj describe, ak potrebujeme robiť podskupiny.

it zabaľuje jeden test

describe a it

it sa vykonávajú v takom poradí, ako sú zapísané v súbore

Keďže it predstavuje jeden test, tak musí spĺňať požiadavky na test, ktoré sú platné pre všetky automatizované testy:

  • je opakovateľný
  • je nezávislý na iných testoch (a teda aj poradí volania)
  • neovplyvňuje iné testy

Cypress.io - otestovanie našej kalkulačky

Pracujeme v súbore cypress/integration/kalkulacka.spec.js

describe('kalkulacka', function () {
    it('obsahuje formulár pre výpočet druhej mocniny', function () {
        cy.visit('http://localhost:8080/kalkulacka.html');
      
        cy.contains('Druhá mocnina');
        cy.get('#form-power');   
    });
});

Príkazy cy.* pochádzajú od cypressu a slúžia na manipuláciu so stránkou.

  • cy.visit otvorí URL a skontroluje statusový kód (očakáva 200) a typ dokumentu (text/html)
  • cy.contains - nájde DOM element obsahujúci daný text
  • cy.get - nájde DOM element podľa CSS selektoru

Cypress.io - otestovanie našej kalkulačky

cy.* príkazov je veľa. Kompletný zoznam je v dokumentácii:

Ak nejaký cy príkaz spadne alebo nenájde daný element, tak sa to považuje za padnutý test.

Práve bežiaci it test teda neprejde, zruší sa jeho vykonávanie a pokračuje sa na ďalší it.

Tieto príkazy je možné reťaziť (tj. vieme za nimi napísať bodku a pokračovať ďalším príkazom).

Úloha:

Napíšte druhý test (it), ktorý skontroluje, či je na stránke formulár pre zisťovanie väčšieho čísla.

Riešenie

Pracujeme v súbore cypress/integration/kalkulacka.spec.js

describe('kalkulacka', function () {
    it('obsahuje formulár pre výpočet druhej mocniny', function () {
        cy.visit('http://localhost:8080/kalkulacka.html');
      
        cy.contains('Druhá mocnina');
        cy.get('#form-power');   
    });
  
    it('obsahuje formulár pre zisťovanie väčšieho čísla', function () {
        cy.visit('http://localhost:8080/kalkulacka.html');
      
        cy.contains('Zisti väčšie');
        cy.get('#form-max');
    });
});

Interakcie na stránke

Cypress nám umožňuje s elementami na stránke aj interagovať (klikať na ne, písať do nich, vyberať si zo selectboxov a pod.)

Robí sa to pomocou špeciálnych commandov, tu je k tomu dokumentácia:

V spojení s reťazením príkazov tak vieme napr. najskôr pohľadať element a potom do neho písať

cy.get('#form-search > input[name=search]').type('praca bratislava');

Úloha

Napíšte ďalší test, ktorý overí, že formulár pre druhú mocninu počíta správne.

Budete potrebovať skontrolovať, že vypočítaný výsledok je taký, ako očakávate.

To urobíte tak, že sa pozriete, čo sa zapísalo do elementu s id power-result

K tomu budete potrebovať dva zreťazené cy príkazy:

  • get - na získanie elementu
  • should - na porovnanie hodnoty

Úloha

Napíšte ďalší test, ktorý overí, že formulár pre druhú mocninu počíta správne.

Príkaz should očakáva jeden alebo dva argumenty:

  1. aserciu (tvrdenie) vo forme reťazca
  2. prípadnú hodnotu, ktorú chceme mať potvrdenú

Asercie (asserty) si cypress vypožičal z knižnice chai.

cy.get('#selektor').should('not.be.empty');
cy.get('#selektor').should('be.string', 'ahoj svet');
cy.get('#selektor').should('have.string', 'ahoj');

Riešenie

Napíšte ďalší test, ktorý overí že formulár pre druhú mocninu počíta správne.

it('formulár pre druhú mocninu funguje', function () {
    cy.visit('http://localhost:8080/kalkulacka.html');
  
    cy.get('#form-power input').type(2);
    cy.get('#form-power button').click();
    cy.get('#result-power').should('be.text', 4);
});

Pracujeme v súbore cypress/integration/kalkulacka.spec.js

describe('kalkulacka', function () {
    it('obsahuje formulár pre výpočet druhej mocniny', function () {
        cy.visit('http://localhost:8080/kalkulacka.html');
      
        cy.contains('Druhá mocnina');
        cy.get('#form-power');   
    });
  
    it('obsahuje formulár pre zisťovanie väčšieho čísla', function () {
        cy.visit('http://localhost:8080/kalkulacka.html');
      
        cy.contains('Zisti väčšie');
        cy.get('#form-max');
    });
  
    it('formulár pre druhú mocninu funguje', function () {
        cy.visit('http://localhost:8080/kalkulacka.html');
  
        cy.get('#form-power input').type(2);
        cy.get('#form-power button').click();
        cy.get('#result-power').should('be.text', 4);
    });
});

Toto sa nám opakuje v každom teste, nevieme to nejako dať vykonať automaticky?

Mocha poskytuje tzv. hooky

Pomocou hooku vieme dať vykonávať náš vlastný kód v určitých momentoch, kedy mocha prechádza cez testy.

Tieto hooky sú štyri:

  • before - vykonáva sa raz a to skôr, než sa spustí prvý test v description bloku
  • after - vykonáva sa raz a to po poslednom teste v description bloku
  • beforeEach - vykonáva sa pred každým testom v description bloku
  • afterEach - vykonáva sa po každom teste v description bloku

Mocha poskytuje tzv. hooky

Hooky sa používajú tak, že vo vnútri description bloku zavoláte funkciu nazvanú podľa vybraného hooku a ako argument jej odovzdáte anonymnú funkiu, v ktorej je váš kód

description('dôležitý test', function () {
    beforeEach(function () {
        console.log('Tento kód sa vykoná pred každým testom');
    });
  
    it('...', function () {/* ... */});
    it('...', function () {/* ... */});
    it('...', function () {/* ... */});
});

Úloha

Upravte doterajší kód tak, aby bol cy.visit vykonaný pred každým testom.

Riešenie

Upravte doterajší kód tak, aby bol cy.visit vykonaný pred každým testom.

describe('kalkulacka', function () {
    beforeEach(function () {
        cy.visit('http://localhost:8080/kalkulacka.html');
    });
  
    it('obsahuje formulár pre výpočet druhej mocniny', function () {
        cy.contains('Druhá mocnina');
        cy.get('#form-power');   
    });
  
    it('obsahuje formulár pre zisťovanie väčšieho čísla', function () {
        cy.contains('Zisti väčšie');
        cy.get('#form-max');
    });
  
    it('formulár pre druhú mocninu funguje', function () {
        cy.get('#form-power input').type(2);
        cy.get('#form-power button').click();
        cy.get('#result-power').should('be.text', 4);
    });
});

Teória

Testy by mali otestovať svoju testovanú jednotku zo všetkých strán, vyskúšať všetky možné aj nemožné vstupy a situácie a rôzne okrajové podmienky.

Keby sme  formulár chceli otestovať pre reálnu aplikáciu, tak testami musíme pokryť tieto situácie:

  • nie je zadané číslo a človek klikne na tlačidlo
  • je zadaná 0
  • je zadané záporné číslo
  • je zadané desatinné číslo
  • nie je zadané číslo, ale namiesto neho reťazec
  • musíme overiť, či existuje miesto, kde sa zobrazuje výsledok
  • vieme formulár odoslať klávesou enter?
  • a čokoľvek ďalšie a zmysluplné vám napadne

Úloha na doma

Napíšte test na niektorú z vymenovaných situácií.

Ak sa stránka nebude správať tak, ako by ste očakávali, upravte kód starajúci sa o výpočet alebo zobrazenie výsledku.

  • nie je zadané číslo a človek klikne na tlačidlo
  • je zadaná 0
  • je zadané záporné číslo
  • je zadané desatinné číslo
  • nie je zadané číslo, ale namiesto neho reťazec
  • musíme overiť, či existuje miesto, kde sa zobrazuje výsledok
  • vieme formulár odoslať klávesou enter?

Úloha na teraz

Napíšte test, ktorý otestuje základnú funkcionalitu formuláru pre zistenie väčšieho z dvoch čísel.

Riešenie

Pracujeme v súbore cypress/integration/kalkulacka.spec.js

it('formulár pre zistenie väčšieho čísla funguje', function () {
    cy.get('input[name=first_number]').type(2);
    cy.get('input[name=second_number]').type(5);
    cy.get('#form-max button').click();
    cy.get('#result-max').should('be.text', 5);
});

Testovanie Karla

Ukážeme si, ako otestovať ovládanie Karla pomocou klávesnice

Už vieme, ako písať text do formulárových inputov

cy.get('form input').type('hello world');

Pomocou tohto istého mechanizmu vieme "písať" aj špeciálne klávesy. Stačí, ak ich názov uvedieme v krútených zátvorkách.

Tu je zoznam kláves, ktoré cypress podporuje: https://docs.cypress.io/api/commands/type.html#Arguments

Testovanie Karla

Už potrebujeme iba element, do ktorého budeme "písať".

Karel si svoje listenery zavesil na element document

 document.addEventListener('keydown', function (event) {
     switch (event.code) {
         case 'ArrowUp':
           // ...
     }
 });

Ten ale podľa dokumentácie cypress-u použiť nemôžeme a musíme použiť body

Neviem, prečo. Moc to nevysvetľujú a document naozaj to nefunguje :)

cy.get('body').type('{uparrow}');

Testovanie Karla

Vytvoríme nový spec súbor cypress/integration.karel.spec.js

describe('karel', function () {
    beforeEach(function () {
        cy.visit('http://localhost:8080');
    });
  
    it('začína na pozícii [19,0] otočený na sever', function () {
        // ...
    });
});

Úloha

Pracujeme v súbore cypress/integration.karel.spec.js

describe('karel', function () {
    beforeEach(function () {
        cy.visit('http://localhost:8080');
    });
  
    it('začína na pozícii [19,0] otočený na sever', function () {
        // ...
    });
});

Doplňte telo úvodného testu

Riešenie

Pracujeme v súbore cypress/integration.karel.spec.js

describe('karel', function () {
    beforeEach(function () {
        cy.visit('http://localhost:8080');
    });
  
    it('začína na pozícii [19,0] otočený na sever', function () {
        cy.get('#row').should('be.text', 19);
        cy.get('#col').should('be.text', 0);
        cy.get('#direction').should('be.text', 'sever');
    });
});

Doplňte telo úvodného testu

Využijeme dáta v informačnom paneli pod tlačidlami

Úloha

Napíšte test na overenie fungovania stlačenia pravej šípky

Riešenie

Napíšte test na overenie fungovania stlačenia pravej šípky

describe('karel', function () {
    beforeEach(function () {
        cy.visit('http://localhost:8080');
    });
  
    it('začína na pozícii [19,0] otočený na sever', function () {
        // ...
    });
  
    it('šípka doprava otočí karla', function () {
        cy.get('body').type('{rightarrow}');
        cy.get('#row').should('be.text', 19);
        cy.get('#col').should('be.text', 0);
        cy.get('#direction').should('be.text', 'východ');

        cy.get('body').type('{rightarrow}');
        cy.get('#direction').should('be.text', 'juh');

        cy.get('body').type('{rightarrow}');
        cy.get('#direction').should('be.text', 'západ');

        cy.get('body').type('{rightarrow}');
        cy.get('#direction').should('be.text', 'sever');
    });
});

Pracujeme v súbore cypress/integration.karel.spec.js

Úloha

Napíšte test na overenie fungovania stlačenia ľavej šípky

Riešenie

Napíšte test na overenie fungovania stlačenia ľavej šípky

describe('karel', function () {
    // ...
    
    it('šípka doľava otočí karla', function () {
        cy.get('body').type('{leftarrow}');
        cy.get('#row').should('be.text', 19);
        cy.get('#col').should('be.text', 0);
        cy.get('#direction').should('be.text', 'západ');

        cy.get('body').type('{leftarrow}');
        cy.get('#direction').should('be.text', 'juh');

        cy.get('body').type('{leftarrow}');
        cy.get('#direction').should('be.text', 'východ');

        cy.get('body').type('{leftarrow}');
        cy.get('#direction').should('be.text', 'sever');
    });
});

Pracujeme v súbore cypress/integration.karel.spec.js

Úloha

Napíšte test na overenie fungovania stlačenia šípky hore

Riešenie

describe('karel', function () {
    // ...
    
    it('šípka hore posunie karla dopredu', function () {
        cy.get('body').type('{uparrow}');

        cy.get('#row').should('be.text', 18);
        cy.get('#col').should('be.text', 0);
        cy.get('#direction').should('be.text', 'sever');

        cy.get('body').type('{rightarrow}');
        cy.get('body').type('{uparrow}');
        cy.get('#row').should('be.text', 18);
        cy.get('#col').should('be.text', 1);
        cy.get('#direction').should('be.text', 'východ');

        cy.get('body').type('{rightarrow}');
        cy.get('body').type('{uparrow}');
        cy.get('#row').should('be.text', 19);
        cy.get('#col').should('be.text', 1);
        cy.get('#direction').should('be.text', 'juh');

        cy.get('body').type('{rightarrow}');
        cy.get('body').type('{uparrow}');
        cy.get('#row').should('be.text', 19);
        cy.get('#col').should('be.text', 0);
        cy.get('#direction').should('be.text', 'západ');
    });
});

Úloha

Napíšte test na overenie fungovania stlačenia medzerovníka

Ako sa dostaneme k informácii, či je bunka označená alebo nie?

Bunka je označená, ak jej element td má triedu znacka.

cy.get('správny td element').should('have.class', 'znacka');

Len sa potrebujeme dostať k správnemu elementu

Trošku si pomôžeme

Celkom by pomohlo, keby sme každú bunku vedeli jednoznačne identifikovať.

Na to vieme využiť číslo riadku a číslo stĺpca, lebo každá bunka má túto kombináciu unikátnu.

Upravíme v index.html generovanie buniek tak, aby každé td obsahovala data-cy-position atribút s číslom riadku a stĺpca

Ak potrebujeme v teste pristúpiť k nejakému HTML elementu a tento nie je jednoznačne identifikovateľný cez id alebo nejaké klasické atribúty, aby sme danému elementu dali atribút nazvaný data-cy*

<!-- bunka bez značky tak bude vyzerať takto: -->
<td data-cy-position="10:4">
  
<!-- a takto bunka so značkou: -->
<td data-cy-position="10:4" class="znacka">
 const vygenerujBunku = function (riadok, stlpec) {
    let bunka = '<td data-cy-position="' + riadok + ':' + stlpec + '">';

    if (mapa.bunky[stlpec][riadok].jeOznacene) {
        bunka = '<td data-cy-position="' + riadok + ':' + stlpec + '" class="znacka">';
    }

    if (karel.riadok === riadok && karel.stlpec === stlpec) {
        bunka = bunka + vygenerujKarla();
    }

    bunka = bunka + '</td>';

    return bunka;
};

Upravíme funkciu vygenerujBunku v súbore index.html

it('medzerovník položí značku na prázdne miesto', function () {
    cy.get('body').type(' ');
    cy.get('[data-cy-position="19:0"]').should('have.class', 'znacka');
});

it('medzerovník zdvihne značku z označeného miesta', function () {
    cy.get('body').type(' ');
    cy.get('body').type(' ');
    cy.get('[data-cy-position="19:0"]').should('not.have.class', 'znacka');
});

Riešenie

Záver

Kedy spúšťať testy?

Pri vývoji spúšťame priebežne testy aspoň pre nový kód.

Po dokončení tasku/väčšieho celku spustíme všetky testy.

Nespomaľuje písanie testov čas vývoja?

Keď sa s testami začína a všetci sa ich učia, tak áno

Ale

  • funkctionalitu treba aj tak otestovať
  • počas vývoja sa testuje niekoľkokrát to isté - prečo si to neautomatizovať?
  • ako dlho trvá fixovanie bugov, ktoré sa našli v produkcii?
  • koľko peňazí stojí chyba nájdená v produkcii?

Písanie testov vedie k tvorbe kvalitnejšieho kódu

Existujúce testy znižujú množstvo chýb v kóde

Vývoj ide rýchlejšie, keď je existujúci kód kvalitnejší a bez chýb

Ďakujem za pozornosť

Všetok kód z dnešnej lekcie: