Testing dei Model… like a boss! – Parte 2

Approfondiamo, in un articolo diviso in due parti, il testing dei model per la nostra applicazione.
francesco
Francesco Lettera
16/05/2014 in Tutorial

Traduzione seconda parte dell’articolo “Testing Like a Boss in Laravel: Models” su Tuts+

La prima parte di questo articolo tradotto in italiano, invece, la trovate qui.

Il Model "Page"

Il nostro CMS ha bisogno di un model per rappresentare le pagine statiche. Ecco come implementarlo:

<?php

// app/models/Page.php
 
use LaravelBook\Ardent\Ardent;
 
class Page extends Ardent {
 
    /**
     * Table
     */
    protected $table = 'pages';
 
    /**
     * Ardent validation rules
     */
    public static $rules = array(
        'title' => 'required',              // Page Title
        'slug' => 'required|alpha_dash',    // Slug (url)
        'content' => 'required',            // Content (markdown)
        'author_id' => 'required|numeric',  // Author id
    );
 
    /**
     * Array used by FactoryMuff
     */
    public static $factory = array(
        'title' => 'string',
        'slug' => 'string',
        'content' => 'text',
        'author_id' => 'factory|User',  // Will be the id of an existent User.
    );
 
    /**
     * Belongs to user
     */
    public function author()
    {
        return $this->belongsTo( 'User', 'author_id' );
    }
 
    /**
     * Renders the menu using cache
     *
     * @return string Html for page links.
     */
    public static function renderMenu()
    {
        $pages = Cache::rememberForever('pages_for_menu', function()
        {
            return Page::select(array('title','slug'))->get()->toArray();
        });
 
        $result = '';
 
        foreach( $pages as $page )
        {
            $result .= HTML::action( 'PagesController@show', $page['title'], ['slug'=>$page['slug']] ).' | ';
        }
 
        return $result;
    }
 
    /**
     * Forget cache when saved
     */
    public function afterSave( $success )
    {
        if( $success )
            Cache::forget('pages_for_menu');
    }
 
    /**
     * Forget cache when deleted
     */
    public function delete()
    {
        parent::delete();
        Cache::forget('pages_for_menu');
    }
 
}

Il metodo statico _renderMenu() _renderizza un numero di link per tutte le pagine esistenti. Questo numero è salvato nella chiave cache 'pages_for_menu'. In questo modo, quando ci saranno chiamate future al metodo _renderMenu() _non ci sarà bisogno di interrogare nuovamente il db.

Il risultato sarà un significativo miglioramento delle performance della nostra applicazione.

Ad ogni modo, se una _Page _è salvata o cancellata (i metodi sono _afterSave() _e _delete() _), il valore della cache sarà ripulito, grazie al metodo _renderMenu() _che riflette il nuovo stato del db. Quindi, se cambia il nome della pagina, o se viene cancellata, la chiave _'pages_for_menu' _sarà ripulita dalla cache (Cache::forget('pages_for_menu');).

Nota: il metodo _afterSave() _è presente nel pacchetto Ardent. Altrimenti, sarebbe opportuno implementare il metodo _save() _per cancellare la cache e richiamarlo attraverso parent::save()

Test del Model "Page"

All'interno di _app/tests/model/PageTest.php _scriveremo questi test:

<?php

// app/tests/models/PageTest.php
 
use Zizaco\FactoryMuff\Facade\FactoryMuff;
 
class PageTest extends TestCase
{
    public function test_get_author()
    {
        $page = FactoryMuff::create('Page');
 
        $this->assertEquals( $page->author_id, $page->author->id );
    }

Ancora una volta, ci troviamo di fronte ad un test "opzionale" per confermare la relazione. Le relazioni di _Illuminate/Database/Eloquent _sono già testate da Laravel stesso: non abbiamo quindi bisogno di scrivere ulteriori test a proposito.

public function test_render_menu()
{
    $pages = array();
 
    for ($i=0; $i < 4; $i++) {
        $pages[] = FactoryMuff::create('Page');
    }
 
    $result = Page::renderMenu();
 
    foreach ($pages as $page)
    {
        // Check if each page slug(url) is present in the menu rendered.
        $this->assertGreaterThan(0, strpos($result, $page->slug));
    }
 
    // Check if cache has been written
    $this->assertNotNull(Cache::get('pages_for_menu'));
}

Questo è il test più importante per il model. Prima di tutto abbiamo creato quattro pagine nel ciclo _for. _A seguire la variabile $result valorizzata dal metodo renderMenu(). Questa variabile dovrebbe contenere una stringa HTML, che a sua volta contiene i link alle pagine esistenti.

Il ciclo _foreach, _invece, controlla se lo slug (url) di ogni pagina è presente nella variabile _$result. _Questo è sufficiente, dal momento in cui l'esatto formato dell'HTML non è rilevante per il nostro test.

Infine, controlliamo se la chiave cache pages_for_menu ha immagazzinato qualcosa. In altre parole, renderMenu() ha fatto il suo dovere? Ha memorizzato qualche valore nella cache?

public function test_clear_cache_after_save()
{
    // An test value is saved in cache
    Cache::put('pages_for_menu','avalue', 5);
 
    // This should clean the value in cache
    $page = FactoryMuff::create('Page');
 
    $this->assertNull(Cache::get('pages_for_menu'));
}

Qui invece l'obiettivo è verificare, quando salviamo una nuova Page, se viene svuotata la cache in corrispondenza di pages_for_menu. Il metodo FactoryMuff:create('Page') innesca eventualmente il metodo save() e questa dovrebbe essere un'operazione sufficiente per svuotare la chiave 'pages_for_menu'.

public function test_clear_cache_after_delete()
{
    $page = FactoryMuff::create('Page');
 
    // An test value is saved in cache
    Cache::put('pages_for_menu','value', 5);
 
    // This should clean the value in cache
    $page->delete();
 
    $this->assertNull(Cache::get('pages_for_menu'));
}

Simile al precedente test, vogliamo sapere se la 'pages_for_menu' viene svuotato correttamente dopo la cancellazione di Page.

Ecco il test:

<?php

// app/tests/models/PageTest.php
 
use Zizaco\FactoryMuff\Facade\FactoryMuff;
 
class PageTest extends TestCase
{
    public function test_get_author()
    {
        $page = FactoryMuff::create('Page');
 
        $this->assertEquals( $page->author_id, $page->author->id );
    }
 
    public function test_render_menu()
    {
        $pages = array();
 
        for ($i=0; $i < 4; $i++) {
            $pages[] = FactoryMuff::create('Page');
        }
 
        $result = Page::renderMenu();
 
        foreach ($pages as $page)
        {
            // Check if each page slug(url) is present in the menu rendered.
            $this->assertGreaterThan(0, strpos($result, $page->slug));
        }
 
        // Check if cache has been written
        $this->assertNotNull(Cache::get('pages_for_menu'));
    }
 
    public function test_clear_cache_after_save()
    {
        // An test value is saved in cache
        Cache::put('pages_for_menu','avalue', 5);
 
        // This should clean the value in cache
        $page = FactoryMuff::create('Page');
 
        $this->assertNull(Cache::get('pages_for_menu'));
    }
 
    public function test_clear_cache_after_delete()
    {
        $page = FactoryMuff::create('Page');
 
        // An test value is saved in cache
        Cache::put('pages_for_menu','value', 5);
 
        // This should clean the value in cache
        $page->delete();
 
        $this->assertNull(Cache::get('pages_for_menu'));
    }
}

Il Model "User"

In relazione con i model precedenti, adesso tocca al model User. Ecco il codice per questo model:

<?php
 
// app/models/User.php
 
use Zizaco\Confide\ConfideUser;
 
class User extends ConfideUser {
 
    // Array used in FactoryMuff
    public static $factory = array(
        'username' => 'string',
        'email' => 'email',
        'password' => '123123',
        'password_confirmation' => '123123',
    );
 
    /**
     * Has many pages
     */
    public function pages()
    {
        return $this->hasMany( 'Page', 'author_id' );
    }
 
    /**
     * Has many posts
     */
    public function posts()
    {
        return $this->hasMany( 'Post', 'author_id' );
    }
 
}

Questo model non ha test. Siamo salvi.

Possiamo osservare, infatti, che ad eccezione delle relazioni (utili per i test) non ci sono metodi da implementare in questo model. E l'autenticazione?

Vero, ma... l'uso del package Confide fornisce già i test per l'autenticazione.

I test per Zizaco\Confide\ConfideUser li trovate in ConfideUserTest.php. E' importante determinare quali siano le responsabilità delle classi prima di scrivere i tuoi test. Testare l'opzione "reset della password" di un model _User _è ridondante. Questo perché la responsabilità di questo test è già all'interno di Zizaco\Confide\ConfideUser, non dentro il model User.

Lo stesso vale per i test di validazione dei dati: è proprio Ardent che gestisce queste responsabilità, non avrebbe senso testare nuovamente questa funzionalità.

In breve: mantieni i tuoi test puliti e organizzati. Verifica la responsabilità di ogni classe, e testa solo ciò che è strettamente legato a quella responsabilità

Conclusioni

L'uso di un db in memoria è sicuramente una best practice per eseguire test rispetto ad un classico database.

Grazie all'aiuto di alcuni package come Ardent, FactoryMuff e Confide, puoi ridurre la quantità di codice nei tuoi model, e mantenere i test puliti e obiettivi.