V tomto článku se pokusím Vám ukázat vývoj řízený pomocí testů (tzv. TDD) v Nette na jednoduché aplikaci - návštěvní knize. Pokud Vás vývoj řízený testy nezajímá, ale raději byste se seznámili se samotným Nette, prostě části o testech přeskočte. Strohé zadání návštěvní knihy zní nějak takto:
Zadání aplikace
Na úvodní stránce aplikace bude zobrazen přehled všech příspěvků v knize.
V případě, že v knize nejsou žádné příspěvky o tom uživatele aplikace informuje.
Kdokoli může přidat příspěvek do knihy.
Administrátor může příspěvky mazat, normálnímu uživateli se to nesmí povést.
Začínáme
Abychom mohli začít, musíme si nejprve předpřipravit stavební kámen. Protože se článek nezabývá návrhem databáze, pro naše účely jsem jednu připravil, proto si ji prosím stáhněte zde: guestbook.sqlite.
Nejprve si stáhneme poslední stabilní verzi Nette pro PHP 5.2 a začneme nahráním Skeletonu na webserver. Skeleton najdete ve složce tools z archivu, vezmeme jeho 4 podsložky a nakopírujeme je do složky guestbook na webserveru (nesmíme zapomenout doplnit knihovny Nette a dibi do složky libs). Poté nastavíme práva na 777 složkám app/log a app/temp. Pokud jste ještě nevytvořili ve Vašem IDE projekt pro návštěvní knihu, učiňte tak právě nyní. Ve složce app bude samotný kód aplikace, ve složce libs a tests jak název napovídá budou knihovny (Nette, dibi, přip. Texy, Zend nebo jiné komponenty z Nette Extras), zatímco ve složce document_root budou umístěny soubory přístupné z venčí - tedy CSS, JavaScriptové soubory a soubor index.php načítající aplikaci ze složky app. Pokud nenahráváte složky app a libs mimo document root. Je důležité, aby se v nich nacházel soubor .htaccess z archivu Nette, jinak ve vaší aplikaci vzniká bezpečnostní díra!
Při programování aplikací v Nette se používá architektura MVP, pokud ji neznáte, můžete si přečíst popis v dokumentaci: MVP.
Staženou databázi nahrajeme do složky app/models. Nyní je dobré vědět, co který soubor ve Skeletonu dělá: soubor app/config.ini obsahuje konfiguraci aplikace, tedy typicky třeba připojení k databázi, různé nastavení služeb, konfiguraci php atd. Pokud navštívíte soubor document_root/index.php, stane se zhruba toto - nadefinují se základní konstanty (WWW_DIR, LIBS_DIR a APP_DIR), načte (require) se soubor app/bootstrap.php, který následně spustí aplikaci (příkazem $application->run()). Dále se již pomocí autoloadingu načtou a spustí presentery, modely a šablony načež se celá aplikace vykreslí. Pokud jste již někdy testy psali, víte, že pokud testujete model, nepotřebujete (resp. je nežádoucí), aby běžela celá aplikace - v Nette se to dá obejít podmíněním (if (Environment::getName() !== "console")) příkazu $application->run(). Právě jsme se seznámili s třídou Environment - jak její název napovídá, obsahuje pomocné metody (statické) pro práci s prostředím - umožňuje načítat konfigurační soubory, více viz API). Abychom mohli začít, upravíme soubor app/config.ini doplněním následujícího kódu do sekce common:
Na konec souboru app/config.ini ještě přidejte [console < development].
Nyní se připojíme k databázi v bootstrap.php pomocí pár řádků PŘED voláním $application->run() doplněním:
Pokud se teď rozhodnete otevřít aplikaci v prohlížeči, měli byste vidět "It works!". Pokud místo toho vidíte červenou stránku (tzv. Laděnku), zkontrolujte oprávnění u souboru guestbook.sqlite.
Píšeme testy
U TDD se nejprve píšou testy a až poté se píše kód (snaží se o "co nejmenší, co vyhoví testům"). Nejjednodušší bude začít s testem modelu. Vytvoříme tedy soubor tests/GuestbookTest.php, začneme s následujícím kódem:
<?php
require_once "PHPUnit/Framework.php";
require_once dirname(__FILE__) . "/../document_root/index.php";
/**
* Test of Guestbook model
*/
class GuestbookTest extends PHPUnit_Framework_TestCase
{
/** @var Guestbook */
private $model;
public function setUp()
{
$this->model = new Guestbook;
dibi::query("TRUNCATE TABLE %n", $this->model->table);
}
}
Dá se to použít jako šablona pro všechny testy modelů - díky metodě setUp se při spuštění každého testu vyprázdní tabulka. Ujasníme si, co na modelu chceme testovat - v modelu budeme potřebovat insert a delete. Insert můžeme otestovat tak, že po jeho provedení bude v tabulce více jak 0 záznamů. V našem testu provedeme 2 inserty, zkusíme výpis všech záznamů a poté spustíme test smazání řádku - ověříme, že v tabulce jeden řádek zůstal. Nic dalšího v modelu testovat nemusíme, duplicitní záznamy jsou povoleny. Pokud se na napsání testu necítíte, zde je připravený:
<?php
require_once "PHPUnit/Framework.php";
require_once dirname(__FILE__) . "/../document_root/index.php";
/**
* Test of Guestbook model
*/
class GuestbookTest extends PHPUnit_Framework_TestCase
{
/** @var Guestbook */
private $model;
protected function setUp()
{
$this->model = new Guestbook;
dibi::query("TRUNCATE TABLE %n", $this->model->table);
}
public function testInsert()
{
$values = array(
'author' => 'Jožko',
'email' => 'jozko@gmail.com',
'title' => 'Lorem',
'content' => 'Ipsum dolor sit amet',
'added' => new DateTime(),
);
$this->model->insert($values);
$this->model->insert($values);
$this->assertEquals(2, dibi::fetchSingle("SELECT COUNT([id]) FROM %n", $this->model->table));
}
public function testSelect()
{
$this->testInsert();
$rows = $this->model->fetchAll();
$this->assertEquals(2, count($rows));
}
public function testDelete()
{
$this->testInsert();
$id = dibi::fetchSingle("SELECT MAX([id]) FROM %n", $this->model->table);
$this->model->delete($id);
$this->assertEquals(1, dibi::fetchSingle("SELECT COUNT([id]) FROM %n", $this->model->table));
}
}
Nyní si zkuste sami napsat model vyhovující testům - třídu Guestbook umístěte do souboru app/models. Z testů vyplývá, že model musí dědit od Nette\Object nebo podobné třídy (kvůli property "table", více viz. stránka o Nette\Object), musí mít metody insert, fetchAll a metodu delete. Pokud to nezvládnete, zkuste se inspirovat v archivu ke stažení.
Test presenteru a presenter
Abychom mohli otestovat presenter, musíte znát pár věcí.
Základním prvek v Nette je komponenta. Komponentou je například formulář, menu, nebo třeba "stránkovadlo". Komponenta je třída dědící od Nette\Control nebo Nette\Component, vytváří se v presenteru metodou createComponent<Name>. Dovolte mi to ilustrovat na příkladu: máme formulář pro přidání příspěvku do naší knihy, pojmenujeme ho třeba addItemForm - v presenteru musí existovat metoda createComponentAddItemForm (která form vytvoří a poté ho vrátí příkazem return), v šabloně se poté tato komponenta vykreslí skrze volání {widget addItemForm} - dejte si pozor na velikost písmen. Více viz FAQ/Co přesně dělá volání {widget ...} v šabloně. K formuláři se v presenteru dá přistoupit pomocí $this['addItemForm'] (presenter implementuje rozhranní ArrayAccess).
Po odeslání formuláře se přidá get parametr do URI adresy určující, jaký signál byl proveden - v tomto případě se na konec adresy přidá ?do=addItemForm-submit. Více o signálech. Toho využijeme při testování.
Abychom mohli spustit životní cyklus presenteru, potřebujeme vytvořit objekt typu PresenterRequest obsahující patřičné informace. Pokud se aplikace spustí přes prohlížeč, PresenterRequest je vytvořen díky routování, v našem případě jej musíme vytvořit uměle.
Pokud na presenteru zavoláme $presenter->run($request), metoda nám vrátí odpověď - jedná se o objekt implementující rozhranní IPresenterResponse.
Presenter bude sestávat z jedné action (default), z jednoho formuláře (addItemForm) a jednoho signálu (deleteItem). To nám spolu s předchozím vysvětlením stačí k tomu, abychom napsali test (tests/GuestbookPresenterTest.php). Základ je téměř stejný jako u testu modelu:
<?php
require_once dirname(__FILE__) . "/../document_root/index.php";
require_once "PHPUnit/Framework.php";
/**
* Test of Guestbook Presenter
*/
class GuestbookPresenterTest extends PHPUnit_Framework_TestCase
{
/** @var GuestbookPresenter */
private $object;
protected function setUp()
{
$this->object = new GuestbookPresenter;
}
}
Jakmile navštívíme presenter, měli bychom vidět formulář. To otestujeme následující metodou:
public function testSeeAddItemForm()
{
$requestData = array(
'action' => 'default' // přistupujeme k výchozí action
);
$request = new PresenterRequest('Guestbook', 'get', $requestData); // vytváříme request
$response = $this->object->run($request); // spouštíme presenter
$this->assertType("AppForm", $response->getSource()->presenter['addItemForm']); // getSource vrací šablonu, její proměnná presenter by měla mít komponentu addItemForm typu AppForm
}
Protože formuláře v Nette by měli být psány přes pattern Post-Redirect-Get, vyzkoušíme, jestli nás aplikace po správném vyplnění formuláře přesměruje. Pokud ale formulář vyplníme nevalidně, aplikace by nás přesměrovat neměla.
Nejprve formulář vyplníme správně:
public function testFillFormAndRedirect()
{
$requestData = array(
'action' => 'default',
'do' => 'addItemForm-submit', // signál
// dále následují data formuláře
'author' => 'Jožko',
'email' => 'jozko@gmail.com',
'title' => 'Lorem',
'content' => 'Ipsum dolor sit amet',
'save' => 'save', // tlačítko (submit button)
// obecně: název form. prvku => hodnota
);
$request = new PresenterRequest('Guestbook', 'POST', $requestData, $requestData);
$response = $this->object->run($request);
$this->assertType('RedirectingResponse', $response); // aplikace nás musí přesměrovat
}
Nyní všechna pole formuláře necháme prázdná:
public function testFillFormAndNotRedirect()
{
$requestData = array(
'action' => 'default',
'do' => 'addItemForm-submit', // signál
// dále následují data formuláře
'save' => 'save', // tlačítko (submit button)
);
$request = new PresenterRequest('Guestbook', 'POST', $requestData, $requestData);
$response = $this->object->run($request);
$this->assertType('RenderResponse', $response); // aplikace nás NESMÍ přesměrovat
}
Poslední věc, kterou musíme otestovat, je zajištění bezpečnosti - pouze administrátor může mazat příspěvky. V našem případě postačí primitivní ověření, zda je uživatel přihlášen (k tomu drobně upravíme UsersModel.php ze Skeletonu, najdete ho v archivu). Pokud se pokusí smazat zprávu nepřihlášený uživatel, vyhodíme výjimku ForbiddenRequestException - což při vhodném nastavení nechá vykreslit Error presenter (selhání ověříme blokem try - catch), zatímco pokud uživatel bude přihlášen, přesměrujeme zpět.
Na základě výše uvedeného testu si zkuste napsat presenter. Pokud se na to necítíte, zkuste se inspirovat presenterem z archivu. Nezapomeňte také na výpis všech položek.
Tím se stane presenter Guestbook výchozím a pokud nebude v URL obsažena informace o presenteru, zobrazí se právě on.
Kompletní test aplikace
Nakonec ještě Seleniem vyzkoušíme přidat příspěvek, uvádím už jen zdroják:
<?php
require_once dirname(__FILE__) . "/../document_root/index.php";
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class GuestbookAddTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected function setUp()
{
$this->setBrowser('*firefox');
$this->setBrowserUrl("http://localhost/guestbook/document_root/");
}
public function testSuccess()
{
$this->open();
$this->type("author", "Jožko");
$this->type("title", "Titulek");
$this->type("content", "obsah");
$this->clickAndWait("save");
$this->assertTextPresent("byl přidán");
}
}
AJAX
Na závěr, aby naše kniha byla "cool", si ukážeme, jak jednoduše se formuláře či odkazy dají zAJAXovat. Potřebovat k tomu budeme jQuery (použijeme verzi 1.4) a soubor jquery.nette.js (který sestavíme spojením souborů dvou rozšíření, oba pochází z dílny Honzy Marka - Ajax s jQuery a Ajaxové formuláře s jQuery). Poslední JavaScriptový soubor si napíšeme dle vzoru z dokumentace k oběma rozšířením, pojmenujeme ho třeba guestbook.js:
$(function () {
// vhodně nastylovaný div vložím po načtení stránky
$('<div id="ajax-spinner"></div>').appendTo("body").ajaxStop(function () {
// a při události ajaxStop spinner schovám a nastavím mu původní pozici
$(this).hide().css({
position: "fixed",
left: "50%",
top: "50%"
});
}).hide();
});
// zajaxovatění odkazů provedu takto
$("a.ajax").live("click", function (event) {
event.preventDefault();
$.get(this.href);
// zobrazení spinneru a nastavení jeho pozice
$("#ajax-spinner").show().css({
position: "absolute",
left: event.pageX + 20,
top: event.pageY + 40
});
});
$("form").live('submit', function () { // POZOR, AŽ OD jQuery 1.4!!!
$(this).ajaxSubmit();
return false;
});
Všechny tři potřebné soubory umístíme do složky document_root/js a načteme je v app/templates/@layout.phtml. Dále uděláme 4 věci a naše aplikace je téměř kompletně AJAXová: obalíme blok s příspěvky kódem {snippet itemlist} a {/snippet} v souboru app/templates/Guestbook/default.phtml a obalíme výpis flash zpráv kódem {snippet flashes} a {/snippet} v souboru @layout.phtml, nesmíme zapomenout aplikovat Zavináčovou magii; do metod handleDelete a addItem presenteru Guestbook přidáme řádek $this->invalidateControl() a podmíníme přesměrování pomocí if (!$this->isAjax()); nakonec odkazu pro smazání přidáme třídu ajax a voila - máme plně funkční, web 2.0 aplikaci :-). Pokud byste chtěli do návštěvní knihy přidat třeba našeptávač napovídající jméno, mohly by se Vám hodit informace ze seriálu na Zdrojáku - konkrétně z dílu Nette Framework - AJAX (pokračování).
Předpoklady
Pár slov úvodem
V tomto článku se pokusím Vám ukázat vývoj řízený pomocí testů (tzv. TDD) v Nette na jednoduché aplikaci - návštěvní knize. Pokud Vás vývoj řízený testy nezajímá, ale raději byste se seznámili se samotným Nette, prostě části o testech přeskočte. Strohé zadání návštěvní knihy zní nějak takto:
Zadání aplikace
Začínáme
Abychom mohli začít, musíme si nejprve předpřipravit stavební kámen. Protože se článek nezabývá návrhem databáze, pro naše účely jsem jednu připravil, proto si ji prosím stáhněte zde: guestbook.sqlite. Nejprve si stáhneme poslední stabilní verzi Nette pro PHP 5.2 a začneme nahráním Skeletonu na webserver. Skeleton najdete ve složce tools z archivu, vezmeme jeho 4 podsložky a nakopírujeme je do složky guestbook na webserveru (nesmíme zapomenout doplnit knihovny Nette a dibi do složky libs). Poté nastavíme práva na 777 složkám app/log a app/temp. Pokud jste ještě nevytvořili ve Vašem IDE projekt pro návštěvní knihu, učiňte tak právě nyní. Ve složce app bude samotný kód aplikace, ve složce libs a tests jak název napovídá budou knihovny (Nette, dibi, přip. Texy, Zend nebo jiné komponenty z Nette Extras), zatímco ve složce document_root budou umístěny soubory přístupné z venčí - tedy CSS, JavaScriptové soubory a soubor index.php načítající aplikaci ze složky app. Pokud nenahráváte složky app a libs mimo document root. Je důležité, aby se v nich nacházel soubor .htaccess z archivu Nette, jinak ve vaší aplikaci vzniká bezpečnostní díra!
Při programování aplikací v Nette se používá architektura MVP, pokud ji neznáte, můžete si přečíst popis v dokumentaci: MVP.
Staženou databázi nahrajeme do složky app/models. Nyní je dobré vědět, co který soubor ve Skeletonu dělá: soubor app/config.ini obsahuje konfiguraci aplikace, tedy typicky třeba připojení k databázi, různé nastavení služeb, konfiguraci php atd. Pokud navštívíte soubor document_root/index.php, stane se zhruba toto - nadefinují se základní konstanty (WWW_DIR, LIBS_DIR a APP_DIR), načte (require) se soubor app/bootstrap.php, který následně spustí aplikaci (příkazem
$application->run()
). Dále se již pomocí autoloadingu načtou a spustí presentery, modely a šablony načež se celá aplikace vykreslí. Pokud jste již někdy testy psali, víte, že pokud testujete model, nepotřebujete (resp. je nežádoucí), aby běžela celá aplikace - v Nette se to dá obejít podmíněním (if (Environment::getName() !== "console")
) příkazu$application->run()
. Právě jsme se seznámili s třídou Environment - jak její název napovídá, obsahuje pomocné metody (statické) pro práci s prostředím - umožňuje načítat konfigurační soubory, více viz API). Abychom mohli začít, upravíme soubor app/config.ini doplněním následujícího kódu do sekce common:Na konec souboru app/config.ini ještě přidejte
[console < development]
. Nyní se připojíme k databázi v bootstrap.php pomocí pár řádků PŘED voláním$application->run()
doplněním:Pokud se teď rozhodnete otevřít aplikaci v prohlížeči, měli byste vidět "It works!". Pokud místo toho vidíte červenou stránku (tzv. Laděnku), zkontrolujte oprávnění u souboru guestbook.sqlite.
Píšeme testy
U TDD se nejprve píšou testy a až poté se píše kód (snaží se o "co nejmenší, co vyhoví testům"). Nejjednodušší bude začít s testem modelu. Vytvoříme tedy soubor tests/GuestbookTest.php, začneme s následujícím kódem:
Dá se to použít jako šablona pro všechny testy modelů - díky metodě
setUp
se při spuštění každého testu vyprázdní tabulka. Ujasníme si, co na modelu chceme testovat - v modelu budeme potřebovat insert a delete. Insert můžeme otestovat tak, že po jeho provedení bude v tabulce více jak 0 záznamů. V našem testu provedeme 2 inserty, zkusíme výpis všech záznamů a poté spustíme test smazání řádku - ověříme, že v tabulce jeden řádek zůstal. Nic dalšího v modelu testovat nemusíme, duplicitní záznamy jsou povoleny. Pokud se na napsání testu necítíte, zde je připravený:Nyní si zkuste sami napsat model vyhovující testům - třídu Guestbook umístěte do souboru app/models. Z testů vyplývá, že model musí dědit od Nette\Object nebo podobné třídy (kvůli property "table", více viz. stránka o Nette\Object), musí mít metody insert, fetchAll a metodu delete. Pokud to nezvládnete, zkuste se inspirovat v archivu ke stažení.
Test presenteru a presenter
Abychom mohli otestovat presenter, musíte znát pár věcí.
Základním prvek v Nette je komponenta. Komponentou je například formulář, menu, nebo třeba "stránkovadlo". Komponenta je třída dědící od Nette\Control nebo Nette\Component, vytváří se v presenteru metodou
createComponent<Name>
. Dovolte mi to ilustrovat na příkladu: máme formulář pro přidání příspěvku do naší knihy, pojmenujeme ho třeba addItemForm - v presenteru musí existovat metoda createComponentAddItemForm (která form vytvoří a poté ho vrátí příkazem return), v šabloně se poté tato komponenta vykreslí skrze volání{widget addItemForm}
- dejte si pozor na velikost písmen. Více viz FAQ/Co přesně dělá volání{widget ...}
v šabloně. K formuláři se v presenteru dá přistoupit pomocí$this['addItemForm']
(presenter implementuje rozhranní ArrayAccess).Po odeslání formuláře se přidá get parametr do URI adresy určující, jaký signál byl proveden - v tomto případě se na konec adresy přidá ?do=addItemForm-submit. Více o signálech. Toho využijeme při testování.
Abychom mohli spustit životní cyklus presenteru, potřebujeme vytvořit objekt typu PresenterRequest obsahující patřičné informace. Pokud se aplikace spustí přes prohlížeč, PresenterRequest je vytvořen díky routování, v našem případě jej musíme vytvořit uměle.
Pokud na presenteru zavoláme
$presenter->run($request)
, metoda nám vrátí odpověď - jedná se o objekt implementující rozhranní IPresenterResponse.Presenter bude sestávat z jedné action (
default
), z jednoho formuláře (addItemForm
) a jednoho signálu (deleteItem
). To nám spolu s předchozím vysvětlením stačí k tomu, abychom napsali test (tests/GuestbookPresenterTest.php
). Základ je téměř stejný jako u testu modelu:Jakmile navštívíme presenter, měli bychom vidět formulář. To otestujeme následující metodou:
Protože formuláře v Nette by měli být psány přes pattern Post-Redirect-Get, vyzkoušíme, jestli nás aplikace po správném vyplnění formuláře přesměruje. Pokud ale formulář vyplníme nevalidně, aplikace by nás přesměrovat neměla.
Nejprve formulář vyplníme správně:
Nyní všechna pole formuláře necháme prázdná:
Poslední věc, kterou musíme otestovat, je zajištění bezpečnosti - pouze administrátor může mazat příspěvky. V našem případě postačí primitivní ověření, zda je uživatel přihlášen (k tomu drobně upravíme UsersModel.php ze Skeletonu, najdete ho v archivu). Pokud se pokusí smazat zprávu nepřihlášený uživatel, vyhodíme výjimku ForbiddenRequestException - což při vhodném nastavení nechá vykreslit Error presenter (selhání ověříme blokem try - catch), zatímco pokud uživatel bude přihlášen, přesměrujeme zpět.
Oba testy vypadají takto:
Na základě výše uvedeného testu si zkuste napsat presenter. Pokud se na to necítíte, zkuste se inspirovat presenterem z archivu. Nezapomeňte také na výpis všech položek.
Routování
Až doposud jsme aplikaci museli navštěvovat přes link ve formátu http://localhost/guestbook/document_root/guestbook - pro pohodlnější přístup upravíme v app/bootstrap.php řádky z:
na
Tím se stane presenter Guestbook výchozím a pokud nebude v URL obsažena informace o presenteru, zobrazí se právě on.
Kompletní test aplikace
Nakonec ještě Seleniem vyzkoušíme přidat příspěvek, uvádím už jen zdroják:
AJAX
Na závěr, aby naše kniha byla "cool", si ukážeme, jak jednoduše se formuláře či odkazy dají zAJAXovat. Potřebovat k tomu budeme jQuery (použijeme verzi 1.4) a soubor jquery.nette.js (který sestavíme spojením souborů dvou rozšíření, oba pochází z dílny Honzy Marka - Ajax s jQuery a Ajaxové formuláře s jQuery). Poslední JavaScriptový soubor si napíšeme dle vzoru z dokumentace k oběma rozšířením, pojmenujeme ho třeba guestbook.js:
Všechny tři potřebné soubory umístíme do složky document_root/js a načteme je v app/templates/@layout.phtml. Dále uděláme 4 věci a naše aplikace je téměř kompletně AJAXová: obalíme blok s příspěvky kódem
{snippet itemlist}
a{/snippet}
v souboru app/templates/Guestbook/default.phtml a obalíme výpis flash zpráv kódem{snippet flashes}
a{/snippet}
v souboru @layout.phtml, nesmíme zapomenout aplikovat Zavináčovou magii; do metod handleDelete a addItem presenteru Guestbook přidáme řádek$this->invalidateControl()
a podmíníme přesměrování pomocíif (!$this->isAjax())
; nakonec odkazu pro smazání přidáme třídu ajax a voila - máme plně funkční, web 2.0 aplikaci :-). Pokud byste chtěli do návštěvní knihy přidat třeba našeptávač napovídající jméno, mohly by se Vám hodit informace ze seriálu na Zdrojáku - konkrétně z dílu Nette Framework - AJAX (pokračování).Další možnosti rozšíření
Download
{{tags: tutorial}}
{{author: Ola|1784}}