Tdd Best Practice In Webapp Con Restful Api

Ciao a tutti,

sono alla continua ricerca del miglior modo di scrivere codice con approccio TDD utilizzando il framework Yii. Ad oggi la maggior parte delle applicazioni web è composta da un frontend, uno strato di API (solitamente JSON) per comunicare in modo asincrono con il server ed un backend.

I livelli di test che uso su applicazioni di questo tipo sono prevalentemente quelli unitari per i modelli e quelli funzionali per l’interfaccia. Per questi ultimi l’approccio più “pubblicizzato” su guide, demo e manuali è quello di usare PHPUnit + Selenium ma anche Behat + Mink sembrano essere una buona accoppiata (che devo ancora provare seriamente).

Se avete confidenza con i test funzionali che utilizzano un oggetto browser (come Selenium) saprete meglio di me che meno avete bisogno di farli girare e meglio vi sentite . Questo perchè i tempi di caricamento sono molto più lunghi, il codice dei test è meno manutenibile e alcuni casi d’uso (ad es. Il processo di registrazion/Login con l’ SDK js di Facebook che usa il popup) sono poco piacevoli da scrivere.

Se l’applicazione è molto vicina all’essere una single web page application (e quindi ad avere molte chiamate asincrone) mi pongo il problema di dover testare i corretti output JSON delle chiamate API. Vorrei testare questo tipo di chiamate sotto forma di test unitario per renderli più veloci e manutenibili. Considerando che molte di queste chiamate sono permesse solo ad utenti loggati tramite il filtro accessControl ci ho pensato un po su e dall’idea che mi sono fatto fin’ora le possibilità sono essenzialmente 2:

[list=1]

[*] usare cUrl verso l’endpoint che restituisce il JSON

[*] fare una chiamata diretta alla funzione del controller

[/list]

Nel primo caso si possono applicare le fixture ma non è in alcun modo possibile mockare il CWebUser perchè se si usa Apache la chiamata fatta da cUrl avvia una nuova istanza dell’applicazione che non è quella avviata dai test. Questo è risolvibile rendendo tutte le API restless (attualmente lavoro non da poco) e quindi evitando il problema di usare l’accessControl per filtrare l’accesso alla funzione in questione.

Nel secondo l’unico modo che ho trovato di mockare il webuser è quello di ridefinirlo nella classe del test che sta girando. Questa cosa sembra funzionare ma se nella mia classe di test ho dei casi d’uso che richiedono diversi tipi di utente ho il problema che non posso in alcun modo cambiare a runtime (e nè dinamicamente al momento del setup) il mock del webuser. Per altro l’unico modo che ho trovato di mockare il webuser è quello che trovate sotto, perchè $this->getMock(‘WebUser’) non influisce in alcun modo sul singleton WebUser definito nella configurazione di CWebApplication.

Vi porto un esempio concreto, actionAjaxGetFavStore() può essere chiamata solo se l’utente è loggato:




class UserControllerTest extends CDbTestCase

{

	public $fixtures=array(

		/* NEEDED FIXTURES*/

	);


	public function testUserCanGetFavouriteStore() {

		

		$controller = new UserController(1);

		$result = json_decode($controller->actionAjaxGetFavStore());			

		$this->assertInternalType('array', $result->data);		

			

		$model  = $result->data[0];

		$this->assertEquals($model->name, "Nome dello Store");	

		

	}

}


class WebUser extends CWebUser {

	

	public function getId() {

	    return 1;

	}


	public function getIsGuest() {

		return false;

	}

};



Conoscete una strada migliore? C’è qualcosa di sbagliato a livello concettuale nel mio caso?

Con Behat e Mink mi sono sempre creato una frase del tipo




Given I am "sensorario" with role "admin"

When I go to "/ajaxGetFavStore"

Then the response should contain "acesso negato"



Diventa tutto molto più semplice. Senza curl o cose strane: ci pensa il MinkContext a fare il lavoro sporco delle chiamate get/post. Ma non so se ho risposto alla tua domanda.

Si vero però questo non mi permette il distacco completo dall’interfaccia soprattutto per testare lo strato di comunicazione che c’è di mezzo. Al momento abbiamo trovato una soluzione (che non credo sia la piu pulita del mondo) che consiste nel creare una specie una sorta di mock per la classe WebUser. In questo modo evito a Yii di appoggiarsi alla sessione quando è il momento di fare il login.





class WebUserMock extends WebUser {


public function login($identity,$duration=0)

{

    $id=$identity->getId();

    $states=$identity->getPersistentStates();

    if($this->beforeLogin($id,$states,false))

    {

        $this->changeIdentity($id,$identity->getName(),$states);

        $duration = 0;

        if($duration>0)

        {

            if($this->allowAutoLogin)

                $this->saveToCookie($duration);

            else

                throw new CException(Yii::t('yii','{class}.allowAutoLogin must be set true in order to use cookie-based authentication.',

                    array('{class}'=>get_class($this))));

        }


        $this->afterLogin(false);

    }

    return !$this->getIsGuest();

}


public function changeIdentity($id,$name,$states)

{   

    $this->setId($id);

    $this->setName($name);

    $this->loadIdentityStates($states);

}


// Load user model.

protected function loadUser() {

    $id = Yii::app()->user->id;

        if ($id!==null)

            $this->_model=User::model()->findByPk($id);

    return $this->_model;

}

};



Nel file di configurazione di test si fa l’override della classe user che usa l’ambiente di sviluppo dicendogli di usare quella manipolata:

protected/config/test.php




'user'=>array(

    'class' => 'application.tests.mock.WebUserMock',

    'allowAutoLogin'=>false,

), 



Cosi a questo punto nel metodo di setUp o ovunque nel corso dei test si puo loggare l’utente che si vuole direttamente generando una UserIdentity e dandola in pasto al nostro WebUser finto :)




$identity = new UserIdentity($this->users('User_2')->email, md5('demo'));               

$identity->authenticate();      

if($identity->errorCode===UserIdentity::ERROR_NONE)                                     

    Yii::app()->user->login($identity);             



Non sono ancora certo che sia la soluzione migliore ma sembra funzionare :)

C’è sempre una soluzione migliore. Ma si cerca solo quando quella attuale non è più adeguata alle nostre esigenze. La mia filosofia è che prima il codice deve funzionare, poi a farlo funzionare meglio si fa sempre in tempo: meglio avere più feature oppure meglio avere più efficenza (a scapito delle feature)? Credo sia una guerra di religione che non può vincere nessuno =).

Ho letto questo su stackoverflow, forse puo suggerirti qualche altra soluzione: http://stackoverflow.com/questions/14769234/how-to-force-a-login-using-yii

Oggi ho fatto un esperimento ed ho provato a risolvere il problema del login in questo modo:




    /**

     * @Given /^I am logged as "([^"]*)" with password "([^"]*)"$/

     */

    public function iAmLoggedAsWithPassword($username, $password)

    {

        $this->getSession()->visit($this->locatePath('/index.php?r=site/login'));

        $this->getSession()->getPage()->fillField('LoginForm[username]', $this->fixStepArgument($username));

        $this->getSession()->getPage()->fillField('LoginForm[password]', $this->fixStepArgument($password));

        $this->getSession()->getPage()->pressButton('Login');

    }



Non devo scrivere alcun tipo di codice, se non che c’è da aggiungere questo metodo al proprio FeatureContext. Io sto usando Mink.