Testiraj!

September 4th, 2005

Testiranje enot (Unit Testing) je poseben način testiranja kode. Finta je, da pišeš teste preden narediš razred ali vsaj vzporedno. Testi niso nič drugega kot skupki metod, ki preverjajo, če neka funkcija (ali metoda) res deluje tako, kot bi morala. Test je zelen, če je uspešen, in rdeč, če pade. V tem primeru se pojavi tudi opis napake. V trenutku, ko se pojavi napaka, se lotiš programiranja in preurejanja kode (refactoring), dokler test ne javi uspešnosti. Takrat spišeš nov test za novo funkcionalnost in nadaljuješ …

Cikel programiranja je potem tak:

  1. pišeš test (write test)
  2. pišeš kodo (write code)
  3. preurejaš kodo (refactor)

Ta cikel se ponavlja neprestano, za čim manjše dele kode (recimo vsaj za vsako novo metodo), kar ima več koristnih stranskih učinkov:

  • programiraš samo tisto, kar je nujno, da je test uspešen, torej prišparaš na času in odvečnih neuporabnih feature-ih
  • dinamika test->rdeče->koda->zeleno pozitivno vpliva na programerja
  • če sprememba v kodi vpliva na kakšno drugo kodo, se takoj vidi kje, kako in zakaj, ker padejo testi od tiste druge kode
  • popolnoma stestirano aplikacijo je preprosto prenesti na drug sistem/okolje. Zagotoviti moraš samo, da so vsi testi na novem sistemu uspešni. Če je aplikacija pravilno stestirana, deluje pravilno, če delujejo njeni testi.
  • višji nivo avtomatizacije: testi lahko tečejo avtomatizirano, ob padcu lahko obvestijo skrbnika, ob uspešnosti se lahko projekt sam uploada.
  • miren spanec — vsa koda je testirana
  • testi so kul.

Uporabljam SimpleTest, obstajata pa vsaj še PHPUnit in PHPUnit2.

Naj razložim na preprostem primeru, več pa piše na spodnjih povezavah. Recimo, da hočem spisati razred, ki razbije niz v array, razreže ga pa pri presledkih. Najprej napišem preprost test, v katerem testiram, da prazen string vrne prazen array:
[php]class ReziloTestCase extends UnitTestCase
{
function testPrazenStringVrnePrazenArray()
{
$Rezilo = new Rezilo();
$res = $Rezilo->razrezi(”);
// zahtevek, da je $res enak array()
$this->assertEqual($res, array());
}
}
[/php]
Test je rdeč (oz. sploh nima barve, ker PHP vrže fatal error, ker razred še ne obstaja, ampak saj štekaš ane), zato spišem ravno toliko kode, da test postane zelen.
[php]class Rezilo
{
function razrezi($niz)
{
return array();
}
}[/php]

Mogoče zgleda trapasto, da vračam prazen array, če pa vem, da moram rezati string. No, ampak sploh ni trapasto. Finta je, da vedno spišeš samo toliko kode, da postane test zelen. In ker v testu nisem nikjer testiral rezanja stringa, ampak samo, da prazen string vrne prazen array, je koda popolnoma zadostna in pravilna!

Zdaj si dam novo nalogo. Hočem stestirati, da stvar res reže tudi stringe, ki niso prazni, ampak nimajo presledkov, torej pricakujem array z enim elementom. V razred ReziloTestCase dodam novo metodo:
[php]function testStringBrezPresledkovVrneArrayZEnimElementom()
{
$Rezilo = new Rezilo();
$res = $Rezilo->razrezi(’abcdefg’);
$this->assertEqual($res, array(’abcdefg’));
}[/php]

Glede imenovanja metod v testu: praviloma se morajo imenovati čim bolj opisno, ker v primeru padca testa takoj veš kaj mora stvar delati. Če vidiš, da je padel test v metodi MnozenjeVrneNicCeJeEdenOdFaktorjevNic, ti bo takoj jasno, medtem ko ti ne bo, če bo metodi ime Mnozenje ali MnozenjeNicel.

Predpone test pri metodah je pomembna, ker se avtomatično kličejo vse metode s to predpono.

Sledi koda v razredu:
[php]return explode(’ ‘, $niz);[/php]

Nato pa nov test, ker se odločim, da bo separator poljuben.
[php]function testRezanjeDelujeZDrugacnimSeparatorjem()
{
$R = new Rezilo();
$res = $R->razrezi(’a/b/c’, ‘/’);
$this->assertEqual($res, array(’a', ‘b’, ‘c’));
}[/php]

In temu seveda sledijo ustrezni popravki v razredu, ampak tega ne bom več pisal, gre za princip. Tudi assertEqual() ni edina metoda s katero lahko preverjam, obstajajo tudi WebTestCase, ki simulirajo browser in s tem uporabnika, obstajajo objekti, ki simulirajo druge objekte in še marsikaj. Z vsem tem lahko kvalitetno stestiraš vsako zadevo.

Še koda s katero lahko poganjaš zgornji test:
[php]require_once(’simpletest/unit_tester.php’);
require_once(’simpletest/reporter.php’);
require_once(’classes/Rezilo.class.php’);

$test = new ReziloTestCase();
$test->run(new HtmlReporter());[/php]

Testiranje je super stvar, enkrat ko zalaufa. Na začetku je čudno in težko, ampak prednosti, ki jih prinaša, so nenadomestljive. Predstavljaj si primer ko narediš na strani popravek, preveriš, če deluje, uploadaš in greš domov, kjer čez par ur zveš, da je padla dol neka druga zadeva zaradi te spremembe. V primeru testov bi enaka stvar zgledala takole: napišeš test, napišeš popravek, vidiš, da je padel test nekje drugje, popraviš tisto kodo, test je zelen, uploadaš, greš domov in TE NE SKRBI VEČ.

Obstaja še par koristnih nenapisanih pravil za testiranje:

  • ko odkriješ nekje bug, najprej napišeš test, ki ga potrdi. Potem ga šele odpraviš. Recimo, da po pol leta odkriješ, da rezilo ne deluje pravilno, če imaš v stringu 2 presledka, napišeš testno metodo testReziloDelujeCeStaVStringuDvaSeparatorja() v razredu ReziloTestCase, v njej pa kodo s konkretnim primerom, ki povzroči bug, recimo $Rezilo->razrezi(’a/b/c//’, ‘/’), potem pa se šele spraviš popravljati razred Rezilo. Ves čas ko to delaš, te testi varujejo pred tem, da bi pokvaril kakšno staro funkcionalnost.
  • če nečesa ne moreš stestirati, je kriva tvoja koda in NE TEST — ne glede na to, kako zgleda
  • testiranje se aplicira na javno dostopne metode in podatke. Private/protected metode v bistvu itak stestiraš tako, da testiraš public metode, ki jih potem uporabljajo. Test je namreč napisan v stilu “uporabnika” razreda.
  • če se koda v testih ponavlja (v mojem primeru $Rezilo = new Rezilo()), potem se splača določene kose premakniti v metodi setUp() in tearDown(), ki se zalaufata pred vsakim klicem katerekoli testne metode (setUp) in po njem (tearDown).

Pa še par koristnih (ali hecnih) stranskih efektov:

  • testi so lahko dokumentacija in primeri hkrati — če pogledaš test nekega razreda, lahko iz testa točno vidiš kaj dela in kako se ga uporablja. Ob kvalitetno testirani kodi se dogaja celo to, da preostale dokumentacije ni in da programerji sploh ne komentirajo več svoje kode! V končni fazi dokumentacija vedno zaostaja za kodo, testi pa nikoli.
  • testiranje z malce truda samo po sebi vsili dobro arhitekturo kode — dobro določene objekte, dobre povezave med njimi itd.
  • v večini primerov je kode v datoteki s testom precej več, kot kode za razred
  • ker je potrebno teste pisat (smo leni, ker smo programerji!), se programer ne spravi programirat nekega dodatnega feature-ja, ki ga trenutno ne rabi. Projekt potem raste tam, kjer mora.
  • ob zadosti visokem nivoju testiranja se zmanjša potreba po razhroščevanju praktično na ničlo, ker večino hroščev poloviš že med razvojem. Potem lahko rečeš že “ne debagiram, ker testiram”.

Čisto na koncu, naj se lotim še najpogosteje uporabljanih argumentov (= izgovorov) proti testiranju:

  • čas. Na začetku se zdi, da testiranje podaljša čas razvoja. No, to je vsekakor zmoten občutek, ker testiranje sproti nadzoruje kodo in poskrbi, da se hrošči sproti zaznajo in odpravijo, kar na dolgi rok (celoten razvoj projekta) VEDNO prišpara čas.
  • količina dela. Za razred, ki ima 100 vrstic, imam test, ki je dolg 500 vrstic. Torej sem namesto 100 napisal 600 vrstic kode in še razmišljal sem, kakšne teste naj pišem, namesto, da bi pisal kodo za projekt. Pa saj sem programer ne tester! No, podobno kot pri času je tudi ta izgovor neumesten, ker testi na srednji in dolgi rok zaradi svojih lastnosti (oblikujejo in varujejo kodo) več kot vrnejo vložen trud.
  • kaj testirati? Nekateri programerji ne testirajo, ker ne vejo točno kaj naj testirajo. Če ne veš točno, kaj hočeš testirat, se itak ne smeš spravit sploh programirat. To pa zato, ker očitno ne veš, kaj hočeš od sistema. Vedno mora bit jasno KDO počne KAJ in KAJ VE. Ko veš to, lahko TO testiraš. To so ključni podatki za vsak razred, metodo, podsistem, paket, karkoli.
  • firma ne pusti. Hehe. Zamenjat firmo. Resno :D

Ok, to je to. Zdaj pa testirat!

Več branja:



6 Responses to “Testiraj!”

  1. Rok Says:

    Uf zanimiva stvar tole. Ti to nucaš pri vseh projektih?

  2. dbev fat Says:

    se trudim … pri stari kodi gre težje, na svežih projektih pa lahko že rečem, da vedno testiram :)

  3. domn Says:

    res je, bil sem priča njegovi norosti :)

  4. Razvojni blog » Blog Archive » Test driven development oz. testiranje! Says:

    [...] Te dne sem začel delat na enem malo večjem projektu. Zaradi lajšanja bolečin pri iskanju bug-ov sem se odločil, da začnem zadevo pisat s pomočjo testov. Eden dober slovenski članek. [...]

  5. ursa Says:

    Na tale članek prilezla po zavitih poteh od ne vem več kod. Sporočam, da se je splačalo :)

    Kul stvar. Hvala! Grem testirat.

  6. Boštjan Says:

    Sliši se zanimivo, nism še probu, bom pa vsekakor, po daljšem razmisleku se mi zdi zelo uporabna stvar :)