Autorius Tema: [Pamoka] PHPUnit testavimas arba kaip rašyti stabiliai veikiantį kodą  (Skaityta 2374 kartus)

Neprisijungęs vitalikaz

  • Dalyvis
  • **
  • Įrašai: 895
  • Karma: +59/-2
  • Tikėk kuo tiki
    • Žiūrėti profilį
    • blast.lt
Turėjau truputį laisvo laiko, sugalvojau parašyti straipsniuką :)

Įvadas
Visi programuotojai nori, kad jų rašytas kodas veiktų kuo geriau, greičiau ir stabiliau. Tam, kad įsitikinti, jog kodas daro būtent tai, ko iš jo tikimąsi, naudojamos skirtingos priemonės. Pastebėjau, kad dažnai tai būna paprastas aplikacijos (arba web puslapio) paleidimas ir vizualus patikrinimas, ar išvedama informacija, kuri ir turi būti išvesta, ar neišmetama jokių klaidų, ar veikia aprašyta logika ir pan. Todėl sugalvojau parašyti šį trumpą ir potencialiai neiformatyvų straipsnį, kuris papasakos apie alternatyvų ir automatizuotą tokio testavimo vykdymą - Unit testavimą naudojant PHP.

Kas yra Unit Testing?
Tai toks programavimo procesas, kuris leidžia patikrinti ar atskiras kodo gabalas (modulis, klasė ar kt.) veikia korektiškai ir atlieka tą darbą, kurio iš jo tikimąsi. Idėja tame, kad aprašynėti testavimo atvejus kiekvienai netrivialiai funkcijai arba klasės metodui. Tai leidžia žaibišku greičiu patikrinti, ar naujas sistemoje padarytas pakeitimas nesudarė naujų klaidų kitose sistemos vietose, o taip pat supaprastina tokių atsiradusių klaidų suradimą ir taisymą.

Kokia iš to nauda?
Tokio testavimo būdo tikslas yra parodyti, kad atskiros sistemos dalys veikia taip, kaip reikia. Jeigu tvarkingai ir sąžiningai rašyti tokius testus, tai vėliau nuo naujai atsiradusių sistemos reikalavimų galima bus nebėgti, bijant kažką sugadinti, o atvirksčiai - juos skatinti, siekiant tobulinti sistemą. Taip galima atlikinėti bet kokius sistemos pakeitimus, bet kokiam žingsnyje patikrinant atskirų sistemos dalių veikimą ir įsitikinti, kad ji funkcionuoja taip, kaip reikia.
Į tokius testus galima žiūrėti kaip į "gyva sistemos dokumentaciją". Žmonės, kurie nežino kaip naudoti tavo parašytą kodą, iš testų gali paimti pavyzdžius.



PHPUnit su NetBeans
PHPUnit - tai karkasas, supaprastinantis unit-testų vykdymą pasinaudojant PHP interpretatoriumi. Šiame straipsnyje apžvelgsiu kaip šią biblioteką galima pritaikyti praktiškai. Padaroma prielaida, kad skaitytojas jau moka dirbti su NetBeans IDE, nes straipsnio eigoje rašysime ir testuosime kodą būtent NetBeans'e 7.1.2. (tą pačią praktiką galima pritaikyti ir kitiems IDE - dauguma jų palaiko Unit Testingą ar bent jau turi atitinkamus išplėtimus). Taip pat skaitytojas žino, kas yra objektinis programavimas.

Instaliacija
NetBeans turi būti instaliuotas su PHP išplėtimu.
Pradėkime nuo to, kad reikia suinstaliuoti phpunit karkasą. Ubuntu Linux'e tai galima padaryti šiomis standartinėmis komandomis (jeigu nėra, prieš vykdant šias komandas reikia suinstaliuoti dar ir pear'ą):
$ pear channel-discover pear.phpunit.de
$ pear install phpunit/PHPUnit
(kaip šitą padaryti windozei skaityti, pavyzdžiui, čia )
Jeigu kažkas nesigauna, daugiau straipsnių apie PHPUnit instaliaciją ir nustatymą NetBeans aplinkai galima rasti čia.

Nustatymas
Sukuriame naują NetBeans projektą. NetBeans IDE pasirinkęs Tools > Options > PHP > tab Unit Testing įsitikink, kad teisingai nustatytas path'as iki PHPUnit skripto (Linux tai standartiškai bus '/usr/bin/phpunit').
Projekto direktorijoje sukuriame atskirą folderį testams saugoti. Pavadinkime šią direktoriją `test`. Dabar mūsų projekto struktūra atrodo maždaug šitaip:



Kodo rašymas
Norime sukurti elementarią klasę, kuri saugos informaciją apie žmogų ir galės atlikti kelis veiksmus su jo duomenimis. Projekte sukuriame naują PHP failą, pavadinimu Human.class.php. Jo vidus gali atrodyti, pavyzdžiui, taip:
<?php
/**
 * @author vitalikaz
 * @desc Klasė, kuri bus ištestuota su PHPUnit 
 */
class Human {
    private 
$name;
    private 
$surname;
    private 
$birth_time;
    
    function 
__construct($name$surname$birth_date null) {
        
$this->name $name;
        
$this->surname $surname;
        
// tikriname, ar ivesta data
        
$birth_time strtotime($birth_date);
        
// jeigu taip, priskiriame ja $this->birth_time, jei ne, $birth_time = null (nenurodyta)
        
$this->birth_time $birth_time $birth_time null;
    }
    
    
/**
     *  Getter'iai 
     */
    
function get_name() {
        return 
$this->name;
    }
    function 
get_surname() {
        return 
$this->surname;
    }
    function 
get_birthtime() {
        return 
$this->birth_time;
    }
    
// grazina pilna varda formatu: Vardas Pavarde
    
function get_fullname() {
        return 
$this->name ." " $this->surname;
    }
    
    
// grazina formatuota gimimo data
    
function get_birthday($format) {
        return 
$this->birth_time date($format$this->birth_time) : null;
    }
    
// grazina zmogaus amziu metais
    
function get_age() {
        
// jeigu zmogaus gimimo data yra ateityje, graziname 0
        
if (!$this->birth_time || time() - $this->birth_time 0) return 0;
        
$years intval(date("Y")) - intval(date("Y"$this->birth_time));
        return 
$years;
    }
   
}
?>

Taigi, klasė moka:
* priimti į konstruktorių vardą, pavardę ir gimimo datą ir juos užset'inti
* jeigu gimimo datos parametre nurodyta NE data, set'inamas null
* gražinti suformatuotą gimimo datą
* teisingai apskaičiuoti žmogaus amžių metais

Atrodytų, kas čia gali būti neaiškaus arba neteisingo, bet...



Kodo testavimas
Tam, kad sukurti PHPUnit testavimo atvejus šiai klasei, `Projects` side-bar'e randame reikiamą failą, jo kontekstiniame meniu pasirenkame Tools > Create PHPUnit tests (kaip parodyta sekančiame paveikslėlyje)


NetBeans pasiūlys pasirinkti direktoriją, kurioje bus saugomi testai. Pasirenkame `test` direktoriją, kurią sukūrėme žingsnyje `Nustatymai`. Folder'is `test` pažymimas kaip testų kaupimo folder'is ir projekto struktūroje išskiriamas atskirai, o taip pat automatiškai sukuriamas testavimo failas 'HumanTest.php'.

Pastaba: NetBeans tiesiog iškviečia PHPUnit Sceleton biblioteką, kuri sugeneruoja testų failų turinį. Šitą "skeletą" galima rašyti ir ranka.
Visi failai folderyje `test`, kuriuose aprašyti testavimo atvejai, turi baigtis žodeliu Test. T.y. visi testo failų pavadinimai turi atitiktį šabloną *Test.php. Failo viduje matome naujai sukurtą klasę HumanTest, kuri paveldi PHPUnit karkaso abstrakčia TestCase klasę. Būtent šioje (HumanTest) klasėje ir turi būti aprašyti klasės Human testavimo atvejai. Kiekvienam testui - atskiras metodas. Testavimo metodai turi prasidėti žodžiu 'test', t.y. testavimo metodų pavadinimai atitinka šabloną test*().
Klasėje numatyti keli standartiniai TestCase metodai - setUp(), tearDown() ir kt. Šitie metodai iškviečiami automatiškai prieš ir po testo vykdymo. Juose inicijuojami testuose naudojami objektai (kadangi mūsų klasė ir testavimo atvejai yra elementarūs, šiame straipsnyje jų nenaudosime).
PHPUnit siūlo daugybę assert* metodų, kuriais mes nurodome karkasui kokio rezultato tikimąsi, o kokie rezultatai gauti iš tikrųjų. Šiame straipsnyje naudosime tik kai kuriuos ir paprasčiausius iš jų. Pereikime prie pačio testavimo.
1. Norime ištestuoti, ar paduodant duomenis į klasės konstruktorių, jie sėkmingai išsisaugo. HumanTest klasėje sukuriame metodą testHumanData():
public function testHumanData() {
    $human = new Human("As", "Vitalikas", "1989-05-15");
    $this->assertEquals("As", $human->get_name());
    $this->assertEquals("Vitalikas", $human->get_surname());
    $this->assertEquals(611182800, $human->get_birthtime());
}
Patikriname, ar vardas ir pavardė tokie, kokius mes nustatėme, ir ar data teisingai sukonvertuojama į unix timestamp formatą.

2. Norime patikrinti, ar metodas get_fullname() tikrai gražina vardą ir pavardę:
public function testFullname() {
    $human = new Human("As", "Vitalikas");
    $this->assertEquals("As Vitalikas", $human->get_fullname());
}

3. Patikrinti, ar teisingai gražinamas žmogaus amžius:
public function testAge() {
    $human = new Human("As", "Vitalikas", "1989-05-15");
    $this->assertEquals(23, $human->get_age());

    // data, kuri dar tik bus siuose metuose
    $human = new Human("As", "Vitalikas", "1989-12-15");
    $this->assertEquals(22, $human->get_age());
}

4. Patikrinti, kad jeigu į konstruktorių paduodama bloga data, žmogaus objekte ji turi būti užsetinama į null'ą:
public function testNullOnBadDate() {
    $human = new Human("As", "Vitalikas", "labai bloga data");
    $this->assertNull($human->get_birthtime());
}

Visas HumanTest.php dabar atrodo taip:
<?php

// includiname reikiamą klasės failą, kad PHPUnit žinotų, kas tas Human
require_once dirname(__FILE__) . '/../Human.class.php';

/**
 * Test class for Human.
 * Generated by PHPUnit on 2012-06-14 at 12:46:21.
 */
class HumanTest extends PHPUnit_Framework_TestCase {
    
    public function 
testHumanData() {
        
$human = new Human("As""Vitalikas""1989-05-15");
        
$this->assertEquals("As"$human->get_name());
        
$this->assertEquals("Vitalikas"$human->get_surname());
        
$this->assertEquals(611182800$human->get_birthtime());
    }
   
    public function 
testFullname() {
        
$human = new Human("As""Vitalikas");
        
$this->assertEquals("As Vitalikas"$human->get_fullname());
    }
    
    public function 
testAge() {
        
$human = new Human("As""Vitalikas""1989-05-15");
        
$this->assertEquals(23$human->get_age());
        
// data, kuri dar tik bus siuose metuose
        
$human = new Human("As""Vitalikas""1989-12-15");
        
$this->assertEquals(22$human->get_age());
    }
    
    public function 
testNullOnBadDate() {
        
$human = new Human("As""Vitalikas""labai bloga data");
        
$this->assertNull($human->get_birthtime());
    }

}

?>


Norint paleisti testus, viršutiniame NetBeans IDE meniu reikia pasirinkti Run -> Test Project (arba tiesiog nuspausti Alt + F6).
Įvykdomi visi rasti testai, ir apačioje atsidariusiame lange matome jų rezultatus (pavaizduoti sekančiame paveikslėlyje)


Vuolia. Matome, kad nevisi testai praėjo sklandžiai (tik 75% iš jų). PHPUnit rodo, kad testas testAge() nepraėjo, ir vietoj tikėtino skaičiaus 22 buvo gautas 23. Sako, kad klaida įvyko 29 eilutėje - žiūrime:
<?php
$human 
= new Human("As""Vitalikas""1989-12-15");
$this->assertEquals(22$human->get_age());
?>
Kažkas negerai apskaičiuojant žmogaus amžių. Įdėmiau pažiūrėję į get_age() metodo kodą Human.class.php faile pastebėsime, kad jame paliktas bug'as. Skaičiuojant žmogaus amžių atimami tik metai, nekreipiant dėmesio į tai, jog gali būti tokia situacija, kai šiuo metu einančiuose metuose žmogaus gimtadienis dar neatėjo. Palikus tokią smulkią klaidą, kai sistema orientuotųsi ir skaičiuotų kažką rimtesnio pagal žmogaus amžių (leistų/uždraustų kažkur priėjimą, kreditų skaičių ar pan.), galėtų kilti labai rimtų nesklandumų ir problemų. O susirasti kurioje sistemos vietoje paliktas bug'as būtų labai sudėtinga ir užtrūktų daug mūsų brangaus laiko.



Išvados ir pasiūlymai
Šiame straipsnyje apžvelgiau tik patį patį primityviausią testavimo atvejį, tyčia palikdamas smulkų bug'ą kode. Pavyzdys skirtas tam, kad turėti bent bendrą supratimą apie tai, kaip veikia Unit testavimas, o iš tikrųjų jo galimybės yra daug daug daug platesnės (duomenų bazių testavimas, klasių sąryšių testavimas, objektų izoliavimas, mock'inimas ir t.t. ir t.t.). Taip pat yra patogūs įrankiai, kurie leidžia vizualiai pamatyti kiek procentaliai jūsų rašyto kodo yra padengta testais (Coverage) ir parodo kodo vietas, kurios nėra padengtos. PHPUnit taip pat turi integravimą su Selenium testų scenarijais - naršyklė vykdo anksčiau aprašytus veiksmus, tikėdamasi rezultate pamatyti kažkokią informaciją, bet apie tai, tikėtina, sekančiuos straipsniuose. Žodžiu, galimybių daug. Tikiuosi po šito straipsnio kas nors, kas prieš tai nenaudojo tokios testavimo metodikos, susidomės ja. Sėkmės!

Pilna PHPUnit dokumentacija - http://www.phpunit.de/manual/3.6/en/
Projekto failai prikabinti prie posto.
Atsiprašau už gramatines klaidas :)

P.S. Paveikslėliai pas mane serve. Bet jei nori, Lukai, gali permest pas save, nepamačiau čia patogios galimybės juos įkelt ne kaip priedus, o tiesiog įterpti į tekstą.

Manualai.lt Forumas