Formalizace výstupu presenteru a testování presenterů

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
David Grudl
Nette Core | 8227
+
0
-

V Nette\Application je každý HTTP požadavek formalizován do podoby jednoduchého objektu PresenterRequest. To má celou řadu výhod, aplikace je v podstatě odstíněna od HTTP, vnitřní operace se stávají srozumitelnější. A pak je tu klíčová výhoda při testování presenterů – můžeme si vytvořit objekt presenteru, přihrávat mu různé umělé vytvořené požadavky (tj. objekty PresenterRequest) a testovat, jak na ně reaguje. Bez nutnosti jakkoliv ovlivňovat prostředí, simulovat HTTP požadavky atd.

Pravda je, že uvedené neplatí úplně na 100 %, detekce AJAXu a ovládání cache je zatím stále vázáno na HTTP vrstvu.

Uvnitř presenteru přistupovat k parametrům dotazováním se na objekt HttpRequest se považuje za hrubou chybu. Přinejmenším to znemožňuje užívání některých postupů, jako je uschovávání a obnovení požadavku pomocí metod Application::storeRequest() a restoreRequest().


Co naopak není vůbec formalizované, tak je odpověď presenteru. Žádný PresenterResponse neexistuje. Vlastně v oblasti odpovědi panuje naprostá volnost.

Ale je to na škodu? Z jednoho jediného důvodu je: vážně to totiž komplikuje výše zmíněné testování. (Nechtěl bych teď diskutovat o důležitosti automatizovaného testování presenterů, berme testovatelnost jako klíčovou vlastnost moderního kvalitního frameworku.) Je super, že presenteru mohu snadno přihrát jakýkoliv požadavek vytvořením jednoho objektu. Ale je problém ověřit jeho reakci.

Reakce presenteru v podstatě může být následující:

  • vyhození výjimky při chybě
  • vyhození řídící výjimky AbortException či jejího potomka:
    • výjimky ForwardingException nebo RedirectingException dále zpracované dispatcherem Application
    • vypsání něčeho na výstup a vyhození výjimky TerminateException
  • vytvoření šablony $template a její následné vykreslení na výstup
  • vytvoření datového objektu $payload a jeho následné vypsání v serializované podobě
  • vytvoření šablony $template a její následná transformace do prvků objektu $payload (snippety) + opět vypsání v serializované podobě

Z hlediska testování:

  • vyhozené výjimky se dají testovat snadno a dobře
  • vypsaný $payload se dá zachytit a deserializovat, je tedy testovatelný, byť ne úplně elegantně
  • vykreslená šablona testování prakticky znemožňuje

Jak učinit presentery dobře testovatelnými? Tím, že bude možné otestovat přímo objekt $template, namísto jeho vykreslené podoby. Měl bych jedno snadné a jedno nesnadné řešení, jak toho dosáhnout. To snadné je především triviální na implementaci – stačí přidat nějakou událost, která se zavolá těsně před vykreslením šablony v metodě renderTemplate() a předat ji šablonu. Testovací skript si tedy vytvoří presenter, napíchne se na tuto událost a pak mu jen předá vstupní PresenterRequest a sleduje, jestli vypadne výjimka, jestli se něco vypíše na výstup a vypadne TerminateException nebo jestli se zavolá obsluha oné události, která zkontroluje podobu objektu šablony. A zároveň vyhodí svou výjimku, čímž zabrání následnému vykreslení šablony. Chytré a jednoduché, co?

No a pak je tu druhá možnost – výstup presenteru formalizovat do podoby nového objektu IPresenterResponse. Takových může existovat celá řada – forwardovací, redirectovací, šablonu vykreslující, payload vykreslující, prostý text vypisující nebo dokonce soubor na výstup posílající. Asi nemusím vysvětlovat, že pro testování by tohle byl stav naprosto ideální. Zavolat $response = $presenter->run($request); a pak jen ověřit, že $response je očekávaný objekt.

Implementace druhého řešení je samozřejmě mnohem složitější. Ale moc mě láká, protože jde o čistou cestu. Tak, jak je čistý vstup (PresenterRequest), tak může být čistý i výstup (IPresenterResponse). Zároveň to část odpovědnosti presenteru přesouvá pryč. A obecně, pokud je možné část odpovědnosti třídy přesunout jinam, mělo by se to udělat, protože to je známka, že původní třída byla příliš složitá.

Zkusil jsem si to nanečisto napsat a v podstatě by úprava nepředstavovala zavlečení žádné zpětné nekompatibility. Kromě situací, kdy v Presenter::shutdown() očekáváme, že bylo něco vykresleno, ať už z jakéhokoliv důvodu (kešování atd). Na druhou stranu, tyhle postupy byly vždy hack a s novými pravidly by se řešili lépe. Také by bylo fajn předělat extras PDF generátory nebo FileDownloadery, nicméně ty současné by měly fungovat stále, jen by způsobily netestovatelnost presenteru.

Co si o tom myslíte?

jasir
Člen | 746
+
0
-

Tak já bych chtěl v první řadě vyjádřit nadšení a radost ze skvělého faktu, že Nette kráčí ke ke snadné testovatelnosti. Za to velmi děkuji.

Co se týče naznačených možností – obě cesty jsou zajímavé, ale druhá je skvělá. Stav, kdy testujeme stav jednoho objektu – PresenterResponse – (a nemusíme řešit zvlášt testy na různé vyhozené Exception), je ideální. Náročnost implementace neumím posoudit, ale jsem pro možnost č.2.

Honza Marek
Člen | 1664
+
0
-

Zní to rozhodně zajímavě. Jen mi není jasné technické provedení, jak bude probíhat třeba takové přesměrování. Bude snad PresenterResponse výjimkou?

David Grudl
Nette Core | 8227
+
0
-

Myslím, že se mi to nakonec povedlo – k dispozici v poslední nightly build.

Teď by to chtělo hlavně důkladně otestovat.

jasir
Člen | 746
+
0
-

Šlape to krásně, žádné problémy jsem nezaznamenal.

Pokusil jsem se o první test a vybral jsem si klasický loginForm.
Presenter request myslím vytvářím dobře. Zatím se mi v PhpUnit nedaří aby prošel ten první test, padá mi to InvalidLinkException: No route for Admin:Default:default(_fid=7399). Ještě přesně nevím co s tím, Environment je ‚console‘. Druhý test už projde. Takže otázke je, jak pod ‚console‘ nastavit routy atp. aby fungovalo vytváření linků.

<?php
class AuthPresenterTest extends MyTestCase {

	public function testGoodTryLogin() {
		$request = new PresenterRequest(
			'Admin:Auth',
			'POST',
			array('action'=>'login',  'do'=>'loginForm-submit'),
			array('username'=>'admin','password'=>'demo', 'login'=>'Login')
		);

		$presenter = new Admin_AuthPresenter($request);
		$response = $presenter->run();

		$this->assertType('RedirectingResponse',$response,'Good login - redirecting response');
	}

	public function testBadLogin() {
		$request = new PresenterRequest(
			'Admin:Auth',
			'POST',
			array('action'=>'login',  'do'=>'loginForm-submit'),
			array('login-username'=>'admin','login-password'=>'baad')
		);

		$presenter = new Admin_AuthPresenter($request);
		$response = $presenter->run();

		$this->assertType('RenderResponse',$response,'Bad login - render response');

	}
}
?>
David Grudl
Nette Core | 8227
+
0
-

Nějaký router by tam být měl, klidně SimpleRouter.

Tam je ještě potřeba dořešit jednu záležitost – kam si presenter šáhne pro router. Zatím se k němu dostává přes globální volání Environment::getApplication()->getRouter() což je samozřejmě špatně. Tedy pro testování to chce nastavit Environment::getApplication()->setRouter(new SimpleRouter).

David Grudl
Nette Core | 8227
+
0
-

Ve frameworku žádný objekt PresenterResponse nenajdete, je tu naopak interface IPresenterResponse a k dispozici hned pětice výchozích implementací:

  • RenderResponse nejčastěji užívaný response, umí vykreslit libovolný řetězec nebo objekt (např. šablonu) na výstup.
  • JsonResponse přijde ke slovu v AJAXových požadavcích, vykreslí payload serializovaný do formátu JSON.
  • ForwardingResponse přejde na jiný presenter, nástupce dřívější ForwardingException.
  • RedirectingResponse přesměruje na jiné URL, nástupce RedirectingException.
  • DownloadResponse pošle na výstup „soubor ke stažení“.

Je samozřejmě možné odeslat i svůj vlastní response, nejlépe třeba metodou terminate.

Panda
Člen | 569
+
0
-

Celý systém s IPresenterResponse je vynikající, výborně se s tím pracuje (a to nejen pro účely testování aplikace). Narazil jsem ale na drobné omezení JsonResponse – nešla by upravit, aby nepřijímala jen třídu typu stdClass, ale i obyčejné pole? JsonResponse totiž teď používám ke komunikaci s JavaScriptovou komponentou (konkrétně jde o jsTree), které se objekt v JSON odpovědi příčí. Sice jsem si vytvořil něco na způsob JsonArrayResponse, ale připadá mi to jako zbytečná třída navíc.

David Grudl
Nette Core | 8227
+
0
-

A nelze použít něco jako new JsonResponse((object) $pole)?

LM
Člen | 206
+
0
-

U stdClass chybí namespace.

Panda
Člen | 569
+
0
-

David Grudl napsal(a):

A nelze použít něco jako new JsonResponse((object) $pole)?

To jsem samozřejmě zkoušel, ale to se právě převede do JSON jako objekt, takže výstup vypadá nějak takto:

{
	"0": {
		"attributes": {"id": 1},
		"state": "leaf",
		"data": "Hlavn\u00ed str\u00e1nka",
		"children": []
	},
	"1": {
		"attributes": {"id": 8},
		"state": "leaf",
		"data": "Dokumenty",
		"children": []
	},
	// ...
}

A to se té komponentě nelíbí, není to pole. Teď jsem ale ve zdrojáku našel callback, který ovlivňuje načítání dat, takže to pole můžu nakonec poslat zapouzdřené do objektu.

David Grudl
Nette Core | 8227
+
0
-

Jo, už rozumím. Je to fixnuté.