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ą.

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 ne bėgti, bijant kažką sugadinti, bet 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 į “gyvą 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. 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 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:

Kodas

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 > 0 ? $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 direktorijoje `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čią 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 taip pat siūlo daugybę assert* metodų, kuriais galima nurodyti 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 patikrinti, ar paduodant duomenis į klasės konstruktorių, jie sėkmingai išsisaugo. HumanTest klasėje sukuriame metodą testHumanData():
    <?php
    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());
    }
    ?>

    Metodas assertEquals patikrina ar pirmu ir antru argumentu pateiktos reikšmės yra lygios. Tokiu būdu patikriname, ar vardas ir pavardė tokie, kokius mes nustatėme kūriant objektą, ir ar data teisingai sukonvertuojama į unix timestamp formatą.

  2. Norime įsitikinti, kad metodas get_fullname()tikrai gražina vardą ir pavardę:
    <?php
    public function testFullname() {
        $human = new Human("As", "Vitalikas");
        $this->assertEquals("As Vitalikas", $human->get_fullname());
    }
    ?>
  3. Patikrinti ar teisingai gražinamas žmogaus amžius:
    <?php
    // dabartinė data - 2012-07-17
    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 yra nustatoma į null’ą:
    <?php
    public function testNullOnBadDate() {
        $human = new Human("As", "Vitalikas", "labai bloga data");
        $this->assertNull($human->get_birthtime());
    }
    ?>

    Metodas assertNull patikrina ar pirmu argumentu pateikta reikšmė yra lygi null.

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-07-17 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 vietoje 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 palikta klaida. 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 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 atimtų daug mūsų brangaus laiko.

Išvados

Šiame straipsnyje apžvelgiau tik patį patį primityviausią testavimo atvejį, tyčia palikdamas smulkų bug’ą kode. Pavyzdys skirtas tam, kad turėti bendrą supratimą apie tai, kaip veikia Unit testavimas, o iš tikrųjų jo galimybės yra daug platesnės (duomenų bazių testavimas, klasių sąryšių testavimas, objektų izoliavimas 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ėdamąsi rezultate pamatyti kažkokį rezultatą, bet apie tai, tikėtina, sekančiuose 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/
Pilną NetBeans projektą galima parsisiųsti iš čia.