Testing dei Model... like a boss! - Parte 1

Approfondiamo, in un articolo diviso in due parti, il testing dei model per la nostra applicazione.
francesco
Marco Spada
09/05/2014 in Tutorial

Traduzione prima parte dell'articolo "Testing Like a Boss in Laravel: Models" su Tuts+

Se stai studiando il TDD e devi ancora capireperché fare testing èuna best practice, questo non è l'articolo che fa perte. Darò per scontato, infatti, che hai già capito quali sono i vantaggi di un buon testing e mi concentrerò su come scriverli in modo giusto e ben organizzato. Laravel 4 offre degli strumenti migliorati, rispetto alla versione precedente, per il testing delle nostre applicazioni.

Setup

Database in-memory

A meno che tu non eseguaquery dirette al database (raw query), sai che Laravel mantiene un approccio "agnostico" al database. Con un semplice cambiamento di driver puoi far lavorare la tua applicazione con diversi DBMS (MySQL, PostgreSQL, SQLite, e.c.c).

Tra questi, SQLite offre una particolare opzione molto utile: in-memory database.

Tramite questa feature saremo in grado, settando la connessione con l’opzione :memory:, di incrementare le prestazioni dei nostri test, datocheil database non saràpiù presente sul disco rigido ma, appunto, in memoria. Inoltre, il database di produzione/sviluppo non sarà mai popolato con i dati di test poiché la connessione :memory: sarà sempre inizializzata con database vuoto.

In breve: l’in-memory database permette un testing veloce e pulito.

All’interno della directory app/config/testing crea un nuovo file chiamato database.php e modificalo nel seguente modo:

// app/config/testing/database.php
 
<?php
 
return array(
 
    'default' => 'sqlite',
 
    'connections' => array(
        'sqlite' => array(
            'driver'   => 'sqlite',
            'database' => ':memory:',
            'prefix'   => ''
        ),
    )
);

Come puoi notare, il file database.php è all’interno della cartella di configurazione di testing e ciò significa che sarà usato solo quando ci troveremo all’interno dell’ambiente di test (che Laravel imposta automaticamente). Per questo motivo, conun accesso "normale" all’applicazione non vieneutilizzato l’in-memory database.

Prima di eseguire un test

Come abbiamo detto, l’in-memory database è sempre vuoto ad ogni connessione. Per questo è importante eseguire una migration del database prima di ogni test.

Per farlo aggiungi il metodo _setUp()_alla fine della classe openapp/tests/TestCase.php.

/**
 * Migrates the database and set the mailer to 'pretend'.
 * This will cause the tests to run quickly.
 *
 */
private function prepareForTests()
{
    Artisan::call('migrate');
    Mail::pretend(true);
}

NOTA: setUp() viene eseguito da PHPUnit prima di ogni test. Cosa succede? Laclasse _Mailer_viene "messa" nello stato di pretend. In questo modo, durante itest, non vieneinviata nessuna mail. Vieneinvece scrittoun file di log con una riga di “sent” messages che indica che il messaggio è stato inviato.

Viene inoltre effettuata una chiamata a prepareForTests(). Non dimenticare inoltre cheparent::setUp(), è un overwriting del metodo della classe padre.

/**
 * Default preparation for each test
 *
 */
public function setUp()
{
    parent::setUp(); // Don't forget this!
 
    $this->prepareForTests();
}

A questo punto app/tests/TestCase.php dovrebbe avere un codice simile al seguente. (Ricorda che _createApplication_è creato automaticamente da Laravel, non dovrai quindi occupartene).

// app/tests/TestCase.php

<?php
 
class TestCase extends Illuminate\Foundation\Testing\TestCase {
 
    /**
	     * Default preparation for each test
	     */
	    public function setUp()
	    {
	        parent::setUp();
	 
	        $this->prepareForTests();
	    }
	 
	    /**
	     * Creates the application.
	     *
	     * @return Symfony\Component\HttpKernel\HttpKernelInterface
	     */
	    public function createApplication()
	    {
	        $unitTesting = true;
	 
	        $testEnvironment = 'testing';
	 
	        return require __DIR__.'/../../start.php';
	    }
	 
	    /**
	     * Migrates the database and set the mailer to 'pretend'.
	     * This will cause the tests to run quickly.
	     */
	    private function prepareForTests()
	    {
	        Artisan::call('migrate');
	        Mail::pretend(true);
	    }
	}

Ora, per scrivere i nostri test, estendiamo semplicemente la classe TestCase. Il database sarà inizializzato e migrato prima di ogni test.

I Test

In questo articolo non seguiremo l’approccio TDD (test driven development). Questo per motivididattici e con l’obiettivo di dimostrare come scrivere buoni test. Affronteremo quindi, prima i model e poi i relativi test.

Lo scenario che descriveremo sarà quello di un semplice blog/CMS, con autenticazione utente, post e pagine statiche (mostrate nel menu).

Il model "Post"

In questa fase è necessario notare il fatto che i model non estenderanno Eloquent ma Ardent. Ardent è un package che facilita le operazioni di validazione dei dati nel salvataggio dei model (vedi la proprietà $rulesproperty).

L’array $factory influenza l’azione del package FactoryMuff per la creazione degli oggetti durante il testing.

Puoi installare Ardentx e FactoryMuff trovandoli nella directoryPackagist o attraverso Composer. Nel nostro model Post c’è una relazione con il model User: a legarli è il metodo magico author.

Infine, abbiamo un metodo che ritorna la data nel formato "giorno/mese/anno".

// app/models/Post.php
 
<?php
 
use LaravelBook\Ardent\Ardent;
 
class Post extends Ardent {
 
    /**
     * Table
     */
    protected $table = 'posts';
 
    /**
     * Ardent validation rules
     */
    public static $rules = array(
        'title' => 'required',              // Post tittle
        'slug' => 'required|alpha_dash',    // Post Url
        'content' => 'required',            // Post content (Markdown)
        'author_id' => 'required|numeric',  // Author id
    );
 
    /**
     * Array used by FactoryMuff to create Test objects
     */
    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' );
    }
 
    /**
     * Get formatted post date
     *
     * @return string
     */
    public function postedAt()
    {
        $date_obj =  $this->created_at;
 
        if (is_string($this->created_at))
            $date_obj =  DateTime::createFromFormat('Y-m-d H:i:s', $date_obj);
 
        return $date_obj->format('d/m/Y');
    }
}

Test del model "Post"

Per mantenere il nostro progetto ben organizzato, andiamo a sistemarela classe col model Post all’interno di app/tests/models/PostTest.php. Andiamo ad effettuare i nostri test, un passo alla volta.

// app/tests/models/PostTest.php
 
<?php
 
use Zizaco\FactoryMuff\Facade\FactoryMuff;
 
class PostTest extends TestCase
{

Come richiesto per il PHPUnit testing in Laravel, estendiamo la classe TestCase. Inoltre, non dimenticare che il metodo prepareTests verrà eseguito prima di qualsiasi test.

public function test_relation_with_author()
{
    // Instantiate, fill with values, save and return
    $post = FactoryMuff::create('Post');
 
    // Thanks to FactoryMuff, this $post have an author
    $this->assertEquals( $post->author_id, $post->author->id );
}

In pratica stiamo testando la relazione "Post belongsTo User": tuttavia, lo scopo è principalmente dimostrare il funzionamento di FactoryMuff.

Una volta che l'array $factory della classe Post contiene 'author_id' => 'factory|User' (come mostra il codice del model sopra) il FactoryMuff istanzierà un nuovo model User con i suoi attributi valorizzati, salverà nel database e ritornerà l' id author_id nel Post.

Perché ciò sia possibile, il model User dovrà avere tutti i suoi attributi descritti nell'array $factory .

Fai attenzione a come accedi alle relazioni sul model User con $post->author. Ad esempio puoi accedere allo username con $post->author->username, e così per tutti gli altri attributi.

Il package FactoryMuff permette una rapida creazione di istanze di oggetti per il testing rispettando e istanziando le relazioni nececssarie. Nel nostro caso, quando creiamo un Post con FactoryMuff::create('Post'), il model User sarà subito disponibile.

	public function test_posted_at()
    {
        // Instantiate, fill with values, save and return
        $post = FactoryMuff::create('Post');
 
        // Regular expression that represents d/m/Y pattern
        $expected = '/\d{2}\/\d{2}\/\d{4}/';
 
        // True if preg_match finds the pattern
        $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false;
 
        $this->assertTrue( $matches );
    }
}

Infine, controlliamo che la stringa ritornata dal metodo postedAt() abbia il corretto formato "giorno/mese/anno". Per questa verifica, usiamo un'espressione regolare che testa il pattern \d{2}/\d{2}/\d{4} ("2 numeri" + "barra" + "2 numeri" + "barra" + "4 numeri").

In alternativa possiamo usare il matcher assertRegExp di PHPUnit.

A questo punto, il listato del file app/tests/models/PostTest.php appare simile a:

// app/tests/models/PostTest.php
 
<?php
 
use Zizaco\FactoryMuff\Facade\FactoryMuff;
 
class PostTest extends TestCase
{
    public function test_relation_with_author()
    {
        // Instantiate, fill with values, save and return
        $post = FactoryMuff::create('Post');
 
        // Thanks to FactoryMuff this $post have an author
        $this->assertEquals( $post->author_id, $post->author->id );
    }
 
    public function test_posted_at()
    {
        // Instantiate, fill with values, save and return
        $post = FactoryMuff::create('Post');
 
        // Regular expression that represents d/m/Y pattern
        $expected = '/\d{2}\/\d{2}\/\d{4}/';
 
        // True if preg_match finds the pattern
        $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false;
 
        $this->assertTrue( $matches );
    }
}

Nota: Non ho utilizzato il tipo di nomenclatura CamelCase per i metodi. Seppur in opposizione al PSR-1, metodi cometestRelationWithAuthor non mi sembravano leggibili. Sei comunque libero di usare lo stile che più ti piace.

Conclusione

Per ora è tutto: la prossima settimana, nella seconda parte, vedremo insieme come lavorare sul model "Page".