Unit testing – první krůčky

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Ondřej Mirtes
Člen | 1536
+
0
-

Ahoj,
vím, že má zítra David na PS přednášku (kde bohužel nebudu a doufám, že ji někdo natočí :)) a abych si to trochu vynahradil, jal jsem se do zkoušení unit testingu v Nette sám. Nainstaloval jsem přes PEAR phpUnit, vytvořil si v projektu složku tests a v něm vytvořil jakýsi bootstrap (nemůžu využít bootstrap z app/, protože obsahuje $application->run()):

// absolute filesystem path to the web root
define('WWW_DIR', dirname(__FILE__));

// absolute filesystem path to the application root
define('APP_DIR', WWW_DIR . '/../app');

// absolute filesystem path to the libraries
define('LIBS_DIR', WWW_DIR . '/../libs');

require_once('c:\Program Files (x86)\Apache\php\PEAR\PHPUnit\Framework.php');

require_once(LIBS_DIR . '/Nette/loader.php');

$autoload = new RobotLoader;
$autoload->addDirectory(array(LIBS_DIR, APP_DIR));
$autoload->register();

Environment::loadConfig(APP_DIR . '/config.ini');

Environment::getApplication()->setRouter(new SimpleRouter(array(
	'module' => 'Web',
	'presenter' => 'Default',
	'action' => 'default',
)));

V config.ini jsem musel vytvořit prázdnou sekci [console < development].

Vytvořil jsem si runTests.bat (bohužel jsem nepřišel na to, jak po provedení testů automaticky pustit firefox s vygenerovaným reportem):

phpunit --coverage-html ./report ./tests

A napsal první test:

require_once(dirname(__FILE__) . '/../init.php');

class Web_DefaultPresenterTest extends PHPUnit_Framework_TestCase {

	public function testRun() {
		$presenter = new Web_DefaultPresenter;

		$response = $presenter->run(new PresenterRequest('Web:Default', 'GET', array()));

		$this->assertType('RenderResponse', $response);
	}

}

Mám otázky :)

  • Jdu na to správně? Nešel by třeba nějak zrecyklovat app/bootstrap.php místo abych ho defacto zkopíroval a promazal?
  • Jak moc důkladně testovat presentery? Tuším, že assertType nestačí, měl bych asi testovat i podobu samotného outputu a další věci (?)
  • Máte nějaký nápad, jak si ušetřit psaní require_once(dirname(__FILE__) . '/../init.php'); na začátku každého testu? Díky RobotLoaderu jsem si na tyhle věci odvykl :)

Díky.

DocX
Člen | 154
+
0
-
  • Jak moc důkladně testovat presentery? Tuším, že assertType nestačí, měl bych asi testovat i podobu samotného outputu a další věci (?)

Osobně si myslím, že akce vedoucí pouze k renderu stránky, je blbost testovat na konkrétní podobu výstupu. Ale spíš jen jestli nevyhazují nějakou vyjímku (tzn. všechny odkazy, ziskani dat, apod jsou OK) Případně při požadavku na ‚forbidden‘ akci (třeba editaci nevlastního článku) se vyhodí správná vyjímka, přesměruje nebo to co se prostě chce.

Naproti tomu akce nebo signály vedoucí k nějaké změně, bych testoval na to, jestli se při jejich spuštění požadovaná změna opravdu provede. Případně, jestli si po POST pošle redirect (pokud se to chce), apod. Samozřejmě kontrola vyjímek a stejně jako nahoře i ta kontrola přístupu apod.

Tohle mě jen tak na rychlo napadá, protože testovat presentery jako takové je asi vůbec ve světě PHP docela novinka :)

Editoval DocX (25. 9. 2009 12:48)

David Grudl
Nette Core | 8218
+
0
-

Z bootstrapu bych odstranil všechno, co není pro test potřebné, což je … všechno ;) Jinak na to jdeš správně.

Co všechno testovat záleží na konkrétním případu – stačí ti vědět, že presenter vrací nějaký renderovací výstup, nebo chceš vědět obsah určité položky šablony?

jiriknesl
Člen | 56
+
0
-

Chci vědět názvy šablon a předané proměnné.

jiriknesl
Člen | 56
+
0
-

Děkuju moc.

Honza Marek
Člen | 1664
+
0
-

David Grudl napsal(a):

Z bootstrapu bych odstranil všechno, co není pro test potřebné, což je … všechno ;)

No a jak se zbavím toho $application->run() ?

DocX
Člen | 154
+
0
-

Honza M. napsal(a):

David Grudl napsal(a):

Z bootstrapu bych odstranil všechno, co není pro test potřebné, což je … všechno ;)

No a jak se zbavím toho $application->run() ?

Možná

<?php
require APP_DIR . '/Presenters/SomePresenter.php';
...
$request = new PresenterRequest(...);
$presenter = new SomePresenter($request);
$response = $presenter->run();
...
?>

?

Editoval DocX (7. 10. 2009 22:38)

Patrik Votoček
Člen | 2221
+
0
-

spíš

$request = new PresenterRequest(...);
$presenterLoader = new PresenterLoader(...);
$presenter = $presenterLoader->getPresenter();
$response = $presenter->run();
DocX
Člen | 154
+
0
-

vrtak-cz napsal(a):

spíš

$request = new PresenterRequest(...);
$presenterLoader = new PresenterLoader(...);
$presenter = $presenterLoader->getPresenter();
$response = $presenter->run();

Ou, jasně, místo Presenter jsem chtěl napsat SomePresenter. A předpokládat, že by tam někde byl include. Ale tohle je taky pěkný.

Ped
Člen | 64
+
0
-

Jen pro zajimavost aktualni pomocni funkce z me „class Presenters_TestCase extends PHPUnit_Framework_TestCase“ tridy… (ocekavam ze to jeste zmenim trochu, asi si udelam prece jen 2 metody pro GET/POST osobitne, ale prozatim mi to umoznuje delat unit testy presenteru presne tak jak potrebuji)

<?php
...
protected function p_run( $_presenter, $_action = 'default', $_params = array() )
{
	//prepare request data
	$method = count( $_params ) ? 'POST' : 'GET';
	$fullparams = array_merge( array( 'action' => $_action ), $_params );
	$request = new Nette\Application\PresenterRequest( $_presenter, $method, $fullparams, $_params );
	//get desired presenter class
	global $application;
	$pname = $request->getPresenterName();
	$pclassname = $application->getPresenterLoader()->getPresenterClass( $pname );
	$request->setPresenterName( $pname );
	$request->freeze();
	$pclass = new $pclassname;
	//execute request (presenter)
	$pclass->autoCanonicalize = FALSE;		//OPRAVA c.1
	return $pclass->run( $request );
}
...
//a nejake telo unit testu pro ukazku:
public function testLogoutWorks()
{
	self::doLogin();
	$r = $this->p_run( 'Homepage', 'default', array( 'do' => 'logout' ) );
	$this->assertFalse( Nette\Environment::getUser()->isAuthenticated() );	//is logged out
	$this->assertType('Nette\Application\RedirectingResponse', $r);		//app should redirect
}

?>

Jestli se vam neco na tom nelibi, nebo mate vylepseni, budu vdecny za inspiraci.

Timto dekuji za inspiraci, bez vas bych to resil asi mnohem vetsi oklikou.

edit – oprava c.1
po pridani druheho presenteru jsem zjistil ze se mi porad vraci #301 s kanonizovanou URI a vzhledem k tomu ze se mi nepovedlo pripravit umely request ktery by neskoncil kanonizaci, tak jsem alespon nasel jak ji vypnout. Neni to tak ciste reseni jako kanonicky request, ale prozatim mne osobne staci.

Editoval Ped (20. 4. 2010 15:31)

michal.sanger
Člen | 4
+
0
-

Funguje vám to s aktuální dev verzí Nette? Já dostávám:

Fatal error: Call to a member function getService() on a non-object in Nette\Application\UI\Presenter.php on line 1257
deter
Člen | 5
+
0
-

Nerozebehnul nekdo phpunit testovani na verzi se jmennymi prostory? I s nejjednodussim testem

<?php

$this->object = new GuestbookPresenter;
$request = new \Nette\Application\PresenterRequest('Guestbook', 'POST', $requestData);
$response = $this->object->run($request);
?>

vyhazuje 3.radek Fatal error: Call to a member function getService() on a non-object in Nette\Application\Presenter.php on line 1254

Patrik Votoček
Člen | 2221
+
0
-

Musíš ještě

/** @var $context Nette\DI\Container */
$this->object->setContext($context);
joe
Člen | 313
+
0
-

Tak jsem se taky pustil do testování a po chvilce hraní, kdy jsem testování konečně zprovoznil mě trochu zarazilo testování následujícího presenteru (když jsem snad ve všech možných ukázkách viděl testování právě na RenderResponse – v nové verzi Nette tedy na \Nette\Application\Responses\TextResponse)

class TestPresenter extends \BasePresenter {
	public function renderDefault() { /* nic */ }
	public function renderTest($id) { /* nic */ }
}

Kód testů:

<?php

// trida BaseTest nacita index.php z document_root

require_once dirname(__FILE__) . '/../../../BaseTest.php';

class TestPresenterTest extends BaseTest {

	private $presenter;

	protected function setUp() {
		$this->presenter = new \FrontModule\TestPresenter();

		// (da se tady dostat ke kontextu i jinak?)
		$this->presenter->setContext(\Nette\Environment::getContext());
	}

	public function testRenderDefault() {
		$request = new \Nette\Application\Request('Front:Test', 'GET',
				array('lang' => 'cs'));
		$response = $this->presenter->run($request);
		// projde
		$this->assertType('Nette\Application\Responses\RedirectResponse', $response);
	}

	public function testRenderTest() {
		$request = new \Nette\Application\Request('Front:Test', 'GET',
				array('action' => 'test', 'lang' => 'cs', 'id' => 5));
		$response = $this->presenter->run($request);
		// neprojde
		$this->assertType('\Nette\Application\Responses\TextResponse', $response);
	}

}

Proč teda dostávám RedirectResponse? Může mi to někdo prosím vysvětlit? Mám snad zase někde něco špatně?


… jinak mi taky celkem dlouho trvalo, než jsem přišel na to, že do konstruktoru \Nette\Application\Request musím psát název modulu bez první dvojtečky :)


ještě OT, jde NetBeans donutit, aby mi napovídala (code completition)?

Editoval joe (25. 8. 2011 6:32)

Nox
Člen | 378
+
0
-

Kanonizace možná…

Napovídala co? pokud je to něco co si předtím newoval, mělo by napovídat. Pokud nemá odkud brát, tak /* @var $promenna Typ */. Pokud asserty, tak je to tím že navzdory manuálu je správná verze self::, metody jsou statické

Editoval Nox (25. 8. 2011 7:44)

Ondřej Brejla
Člen | 746
+
0
-

joe: v NetBeans: Tools → Options → PHP → General → Global Include Path → Add Folder (a tady vyberes folder s PHPUnit, treba /usr/share/php/PHPUnit, nebo nevim kde ho mas…)

Filip Procházka
Moderator | 4668
+
0
-

V panelu projektu máš Source Files, … Include Path. Tak klikneš pravým na Include Pathproperties a tam si přidáš knihovnu. :P

joe
Člen | 313
+
0
-

Dík, už funguje všechno, co jsem potřeboval

(první $this->presenter->autoCanonicalize = FALSE;)

2bfree
Člen | 248
+
0
-

S UnitTesty začínám a moc tomu nerozumím. Podle manuálu jsem si všechno nainstaloval, nakonfiguroval a teď se pokouším o svůj první test presenteru.

Mám jednoduchý presenter

<?php
/**
 * Easy presenter
 * @author [...]
 */
 class EasyPresenter extends BasePresenter {
    /**
     * Set template variables
     * @author [...]
     * @return void
     */
     public function renderDefault() {
         // set value of test variable
         $this->template->test = 'value';
     }
 }
?>

a k němu jednoduchý test

<?php
/**
 * Easy presenter test
 * @author [...]
 */
 class EasyPresenterTest extends PHPUnit_Framework_TestCase {

    public function testRenderDefault(){
        $presenter = new EasyPresenter;
        $presenter->setContext(Nette\Environment::getContext());

        $request = new Nette\Application\Request('Easy', 'GET');
        $response = $presenter->run($request);
        $this->assertType(
            'Nette\Application\Responses\TextResponse',
            $response
        );
    }

}
?>

Nicméně když pustím v NetBeans test nad EasyPresenter.php, tak mi to do output vygeneruje chybu

PHP Fatal error:  Class 'BasePresenter' not found in /private/var/www/test/app/presenters/EasyPresenter.php on line 6
PHP Stack trace:
PHP   1. {main}() /usr/bin/phpunit:0
PHP   2. PHPUnit_TextUI_Command::main() /usr/bin/phpunit:46
PHP   3. PHPUnit_TextUI_Command->run() /usr/lib/php/pear/PHPUnit/TextUI/Command.php:126
PHP   4. PHPUnit_TextUI_Command->handleArguments() /usr/lib/php/pear/PHPUnit/TextUI/Command.php:135
PHP   5. ReflectionClass->newInstanceArgs() /usr/lib/php/pear/PHPUnit/TextUI/Command.php:712
PHP   6. PHPUnit_Util_Skeleton_Test->__construct() /usr/lib/php/pear/PHPUnit/TextUI/Command.php:712
PHP   7. include_once() /usr/lib/php/pear/PHPUnit/Util/Skeleton/Test.php:120

Poradili byste co s tím prosím?

Filip Procházka
Moderator | 4668
+
0
-

Já ti poradím, přečti si http://www.aceblog.cz/…te-testovat/