Scopri come fare i test in Laravel con semplici esempi, utilizzando PHPUnit e PEST

laravel php view blade

Quando si parla di test automatizzati o unit test, in qualsiasi linguaggio di programmazione, ci sono due pareri contrapposti:

  • Perdita di tempo
  • Non si riesce a farne a meno

Quindi, con questo articolo proveremo a convincere i primi, soprattutto dimostrando quanto è facile iniziare con i test automatizzati in Laravel.

Per prima cosa parliamo del “perché”, e poi vediamo qualche esempio per il come.

Perché abbiamo bisogno dei test automatizzati

I test automatizzati eseguono parti del codice e segnalano eventuali errori. Questo è il modo più semplice per descriverli. Immaginiamo di lanciare una nuova funzionalità in una app, e poi un assistente robot personale andrebbe a testare manualmente la nuova funzionalità, testando anche se il nuovo codice non ha interrotto nulla delle vecchie funzionalità.

Questo è il vantaggio principale: testare nuovamente tutte le funzionalità automaticamente. Questo potrebbe sembrare un lavoro extra, ma se non dici al “robot” di farlo, in alternativa dovremmo farlo noi manualmente, giusto ? 

Oppure le nuove funzionalità si potrebbero rilasciare senza verificarne il funzionamento, sperando che gli utenti segnalino i bug.

I test automatizzati sono in grado di darci diversi vantaggi:

  • Consentono di risparmiare tempo di test manuale;
  • Consentono di risparmiare tempo sia sulla nuova funzione implementata, e sia sulle funzioni consolidate evitandone la regressione;
  • Moltiplica questo vantaggio per tutte le nuove funzioni e tutte le funzioni già implementate;
  • I tre punti precedenti valgono per ogni nuova versione;

Prova a immaginare la tua applicazione tra un anno o due, con nuovi sviluppatori nel team che non conoscono il codice scritto negli anni precedenti, e neppure come testarlo. 

I nostri primi test automatizzati

Per eseguire il primo test automatizzato in Laravel, non è necessario scrivere alcun codice. Sì, hai letto bene. Tutto è già configurato e preparato nell’installazione predefinita di Laravel, compreso il primo vero esempio base.

Puoi provare ad installare un progetto Laravel ed eseguire subito i primi test:

laravel new project
cd project
php artisan test

Questo dovrebbe essere il risultato nella tua console:

laravel risultato test

Se diamo un’occhiata alla cartella predefinita di Laravel /tests, abbiamo due file:

tests/Feature/ExampleTest.php :

class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

Non è necessario conoscere alcuna sintassi per capire cosa sta succedendo qui: caricare la home page e verificare se il codice di stato HTTP è “200 OK“.

Nota anche come il nome del metodo test_the_application_returns_a_successful_response() diventa testo leggibile quando visualizzi i risultati del test, semplicemente sostituendo il simbolo di sottolineatura con uno spazio.

tests/Unit/ExampleTest.php :

class ExampleTest extends TestCase
{
    public function test_that_true_is_true()
    {
        $this->assertTrue(true);
    }
}

Sembra un po’ inutile, controllare che vero sia vero ? 

Parleremo specificamente dei test unitari un po’ più tardi. Per ora, devi capire cosa succede generalmente in ogni test.

  • Ogni file di test nella cartella /tests è una classe PHP che estende il TestCase di PHPUnit
  • All’interno di ogni classe, puoi creare più metodi, solitamente un metodo per una situazione da testare
  • All’interno di ciascun metodo ci sono tre azioni: preparazione della situazione, poi azione e poi verifica (affermare) se il risultato è quello previsto

Strutturalmente, questo è tutto ciò che devi sapere, tutto il resto dipende dalle cose esatte che vuoi testare.

Per generare una classe di test vuota, esegui semplicemente questo comando:

php artisan make:test HomepageTest

Viene generato il file tests/Feature/HomepageTest.php:

class HomepageTest extends TestCase
{
    // Replace this method with your own ones
    public function test_example()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

Vediamo ora cosa succede se un codice di test fallisce in Laravel

Andiamo ora a vedere cosa succede se le asserzioni del test non restituiscono il risultato atteso.

Modifichiamo i test di esempio in questo:

class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/non-existing-url');
 
        $response->assertStatus(200);
    }
}
 
 
class ExampleTest extends TestCase
{
    public function test_that_true_is_false()
    {
        $this->assertTrue(false);
    }
}

E ora, se eseguiamo il comando php artisan test di nuovo:

 FAIL  Tests\Unit\ExampleTest
⨯ that true is true
 
 FAIL  Tests\Feature\ExampleTest
⨯ the application returns a successful response
 
---
 
• Tests\Unit\ExampleTest > that true is true
Failed asserting that false is true.
 
at tests/Unit/ExampleTest.php:16
   12▕      * @return void
   13▕      */
   14▕     public function test_that_true_is_true()
   15▕     {
➜  16▕         $this->assertTrue(false);
   17▕     }
   18▕ }
   19▕
 
• Tests\Feature\ExampleTest > the application returns a successful response
Expected response status code [200] but received 404.
Failed asserting that 200 is identical to 404.
 
at tests/Feature/ExampleTest.php:19
   15▕     public function test_the_application_returns_a_successful_response()
   16▕     {
   17▕         $response = $this->get('/non-existing-url');
   18▕
➜  19▕         $response->assertStatus(200);
   20▕     }
   21▕ }
   22▕
 
 
Tests:  2 failed
Time:   0.11s

Ci sono due test falliti, contrassegnati come FAIL, con spiegazioni di seguito e frecce che indicano l’esatta riga di test fallita. Gli errori vengono indicati in questo modo.

Esempio: test del codice modulo di registrazione in Laravel

Supponiamo di avere un modulo e di dover testare vari casi: controlliamo se fallisce con dati non validi, controlliamo se ha successo con l’input corretto, ecc.

Lo starter kit ufficiale di Laravel Breeze include i test delle funzionalità al suo interno. Diamo un’occhiata ad alcuni esempi da lì:

tests/Feature/RegistrationTest.php

use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class RegistrationTest extends TestCase
{
    use RefreshDatabase;
 
    public function test_registration_screen_can_be_rendered()
    {
        $response = $this->get('/register');
 
        $response->assertStatus(200);
    }
 
    public function test_new_users_can_register()
    {
        $response = $this->post('/register', [
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => 'password',
            'password_confirmation' => 'password',
        ]);
 
        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }
}

Qui abbiamo due test in una classe, poiché sono entrambi legati al modulo di registrazione: uno controlla se il modulo è caricato correttamente e un altro controlla se l’invio funziona bene.

Acquisiamo familiarità con altri due metodi per verificare il risultato, altre due asserzioni: $this->assertAuthenticated()$response->assertRedirect(). Puoi controllare tutte le asserzioni disponibili nella documentazione ufficiale di PHPUnit e Laravel Response . Tieni presente che alcune asserzioni generali si verificano sull’oggetto $this, mentre altre controllano lo specifico $responsedalla chiamata del percorso.

Altra cosa importante è l’ use RefreshDatabase;affermazione, con il tratto, inserita sopra la classe. È necessario quando le azioni di test possono influenzare il database, come in questo esempio, la registrazione aggiunge una nuova voce nella userstabella del database. Per questo, dovresti creare un database di test separato che verrà aggiornato con php artisan migrate:freshogni volta che i test vengono eseguiti.

Hai due opzioni: creare fisicamente un database separato o utilizzare un database SQLite in memoria. Entrambi sono configurati nel file phpunit.xmlfornito per impostazione predefinita con Laravel. Nello specifico, hai bisogno di questa parte:

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="BCRYPT_ROUNDS" value="4"/>
    <env name="CACHE_DRIVER" value="array"/>
    <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
    <!-- <env name="DB_DATABASE" value=":memory:"/> -->
    <env name="MAIL_MAILER" value="array"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="TELESCOPE_ENABLED" value="false"/>
</php>

Vedi gli DB_CONNECTIONDB_DATABASEquali sono commentati? Se hai SQLite sul tuo server, l’azione più semplice è semplicemente rimuovere il commento da quelle righe e i tuoi test verranno eseguiti su quel database in memoria.

In questo test affermiamo che l’utente viene autenticato con successo e viene reindirizzato alla home page corretta, ma possiamo anche testare i dati effettivi nel database.

Oltre a questo codice:

$this->assertAuthenticated();
$response->assertRedirect(RouteServiceProvider::HOME);

Possiamo anche utilizzare le asserzioni di test del database e fare qualcosa del genere:

$this->assertDatabaseCount('users', 1);
 
// Or...
$this->assertDatabaseHas('users', [
    'email' => 'test@example.com',
]);

Esempio della pagina di Login

Andiamo ora a vedere un altro esempio di pagina di Login con Laravel Breeze

tests/Feature/AuthenticationTest.php:

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;
 
    public function test_login_screen_can_be_rendered()
    {
        $response = $this->get('/login');
 
        $response->assertStatus(200);
    }
 
    public function test_users_can_authenticate_using_the_login_screen()
    {
        $user = User::factory()->create();
 
        $response = $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);
 
        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }
 
    public function test_users_can_not_authenticate_with_invalid_password()
    {
        $user = User::factory()->create();
 
        $this->post('/login', [
            'email' => $user->email,
            'password' => 'wrong-password',
        ]);
 
        $this->assertGuest();
    }
}

Riguarda il modulo di accesso. La logica è simile alla registrazione, giusto? Ma tre metodi invece di due, quindi questo è un esempio di come testare scenari sia buoni che cattivi. Quindi, la logica comune è che dovresti testare entrambi i casi: quando le cose vanno bene e quando falliscono.

Inoltre, ciò che vedi in questo test è l’utilizzo di Database Factories : Laravel crea un utente falso ( di nuovo, sul tuo database di test aggiornato ) e quindi tenta di accedere, con credenziali corrette o errate.

Ancora una volta, Laravel genera la fabbrica predefinita con dati falsi per il Usermodello, fuori dagli schemi.

database/factories/UserFactory.php:

class UserFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}

Vedi, quante cose sono preparate dalla stessa Laravel, quindi sarebbe facile per noi iniziare a testare?

Quindi, se eseguiamo php artisan testdopo aver installato Laravel Breeze, dovremmo vedere qualcosa del genere:

 PASS  Tests\Unit\ExampleTest
✓ that true is true
 
 PASS  Tests\Feature\Auth\AuthenticationTest
✓ login screen can be rendered
✓ users can authenticate using the login screen
✓ users can not authenticate with invalid password
 
 PASS  Tests\Feature\Auth\EmailVerificationTest
✓ email verification screen can be rendered
✓ email can be verified
✓ email is not verified with invalid hash
 
 PASS  Tests\Feature\Auth\PasswordConfirmationTest
✓ confirm password screen can be rendered
✓ password can be confirmed
✓ password is not confirmed with invalid password
 
 PASS  Tests\Feature\Auth\PasswordResetTest
✓ reset password link screen can be rendered
✓ reset password link can be requested
✓ reset password screen can be rendered
✓ password can be reset with valid token
 
 PASS  Tests\Feature\Auth\RegistrationTest
✓ registration screen can be rendered
✓ new users can register
 
 PASS  Tests\Feature\ExampleTest
✓ the application returns a successful response
 
Tests:  17 passed
Time:   0.61s

Test funzionali a confronto con Test unitari e altri

Hai visto le sottocartelle tests/Feature e tests/Unit ?. 

Qual’è la differenza tra loro? 

A livello globale, al di fuori dell’ecosistema Laravel/PHP, esistono diversi tipi di test automatizzati. Puoi trovare termini come:

  • Test unitari
  • Test delle funzionalità
  • Prove di integrazione
  • Test funzionali
  • Test end-to-end
  • Prove di accettazione
  • Prove di fumo
  • eccetera.

Sembra complicato e le differenze effettive tra questi tipi di test a volte sono sfumate. Ecco perché Laravel ha semplificato tutti questi termini confusi e li ha raggruppati in due: unità/caratteristica.

In parole semplici, i test delle funzionalità cercano di eseguire le funzionalità effettive delle tue applicazioni: ottieni l’URL, chiama l’API, imita il comportamento esatto come la compilazione del modulo. I test delle funzionalità di solito eseguono operazioni identiche o simili a quelle che qualsiasi utente del progetto farebbe, manualmente, nella vita reale.

I test unitari hanno due significati. In generale, potresti scoprire che qualsiasi test automatizzato è chiamato “test unitario” e l’intero processo può essere chiamato “test unitario”. Ma nel contesto della funzionalità rispetto all’unità, questo processo riguarda il test di una specifica unità non pubblica del codice, in isolamento. Ad esempio, hai una classe Laravel con un metodo che calcola qualcosa, come il prezzo totale dell’ordine con parametri. Pertanto, il test unitario affermerebbe se i risultati corretti vengono restituiti da quel metodo (unità di codice), con parametri diversi.

Per generare uno unit test, è necessario aggiungere un flag:

php artisan make:test OrderPriceTest --unit

Il codice generato è lo stesso dello unit test predefinito di Laravel:

class OrderPriceTest extends TestCase
{
    public function test_example()
    {
        $this->assertTrue(true);
    }
}

Come puoi vedere, non esiste RefreshDatabase, e questa è una delle definizioni più comuni di unit test: non tocca il database, funziona come una “scatola nera”, isolata dall’applicazione in esecuzione.

Cercando di imitare l’esempio che ho citato prima, immaginiamo di avere una service class OrderPrice.

app/Services/OrderPriceService.php:

class OrderPriceService
{
    public function calculatePrice($productId, $quantity, $tax = 0.0)
    {
        // Some kind of calculation logic
    }
}

Then, the unit test could look something like this:

class OrderPriceTest extends TestCase
{
    public function test_single_product_no_taxes()
    {
        $product = Product::factory()->create(); // generate a fake product
        $price = (new OrderPriceService())->calculatePrice($product->id, 1);
        $this->assertEquals(1, $price);
    }
 
    public function test_single_product_with_taxes()
    {
        $price = (new OrderPriceService())->calculatePrice($product->id, 1, 20);
        $this->assertEquals(1.2, $price);
    }
 
    // More cases with more parameters
}

Nella mia esperienza personale con i progetti Laravel, la maggioranza assoluta dei test sono Feature test, non Unit test. Innanzitutto, devi testare se la tua applicazione funziona, il modo in cui la utilizzerebbero le persone reali.

Successivamente, se disponi di calcoli o logica speciali che puoi definire come unità, con parametri, puoi creare unit test appositamente per quello.

A volte, scrivere test richiede la modifica del codice stesso e il refactoring per renderlo più “testabile”: separando le unità in classi o metodi speciali.

Quando/come eseguire i test?

Qual è l’effettivo utilizzo di questo php artisan test, quando dovresti eseguirlo?

Esistono approcci diversi, a seconda del flusso di lavoro aziendale, ma in genere è necessario assicurarsi che tutti i test siano “verdi” (ovvero senza errori) prima di inviare le ultime modifiche al codice al repository.

Quindi, lavori localmente sul tuo compito e, quando ritieni di aver finito, esegui dei test per assicurarti di non aver rotto nulla. Ricorda, il tuo codice potrebbe causare bug non solo nella tua logica ma anche interrompere involontariamente qualche altro comportamento nel codice di qualcun altro scritto molto tempo fa.

Se facciamo un ulteriore passo avanti, è possibile automatizzare molte cose. Con vari strumenti CI/CD, puoi specificare i test da eseguire ogni volta che qualcuno inserisce le modifiche in un ramo Git specifico o prima di unire il codice nel ramo di produzione. Il flusso di lavoro più semplice sarebbe utilizzare Github Actions, ho un video separato che lo dimostra.

Cosa dovresti testare?

Ci sono opinioni diverse su quanto dovrebbe essere grande la cosiddetta “copertura del test”: provare ogni operazione e ogni caso possibile su ogni pagina, oppure limitare il lavoro alle parti più importanti.

In effetti, è qui che sono d’accordo con le persone che accusano i test automatizzati di impiegare più tempo che di apportare benefici effettivi. Ciò può accadere se scrivi test per ogni singolo dettaglio. Detto questo, potrebbe essere richiesto dal tuo progetto: la domanda principale è “qual è il prezzo del potenziale errore”.

In altre parole, è necessario dare priorità ai propri sforzi di test ponendosi la domanda “Cosa accadrebbe se questo codice fallisse?” Se il tuo sistema di pagamento presenta bug, ciò avrà un impatto diretto sull’azienda. Quindi, se la funzionalità dei tuoi ruoli/autorizzazioni è interrotta, si tratta di un enorme problema di sicurezza.

Mi piace come lo ha espresso Matt Stauffer durante una conferenza: “Devi prima testare quelle cose che, se falliscono, ti farebbero licenziare dal tuo lavoro”. Naturalmente è un’esagerazione, ma hai l’idea: prova prima le cose importanti. E poi altre funzionalità, se hai tempo.

PEST: nuova alternativa a PHPUnit

Tutti gli esempi sopra riportati sono basati sullo strumento di test Laravel predefinito: PHPUnit . Ma nel corso degli anni altri strumenti sono apparsi nell’ecosistema e uno degli ultimi popolari è PEST . Creato dal dipendente ufficiale Laravel Nuno Maduro , ha l’obiettivo di semplificare la sintassi, rendendo ancora più veloce la scrittura del codice per i test.

Sotto il cofano, viene eseguito su PHPUnit, come livello aggiuntivo, cercando solo di ridurre al minimo alcune parti ripetute predefinite del codice PHPUnit.

Diamo un’occhiata a un esempio. Ricordi la classe di test delle funzionalità predefinita in Laravel? Ti ricorderò:

namespace Tests\Feature;
 
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

Sai come apparirebbe lo stesso test con PEST?

test('the application returns a successful response')->get('/')->assertStatus(200);

Sì, UNA riga di codice e basta. Quindi, l’obiettivo di PEST è rimuovere il sovraccarico di:

  • Creazione di classi e metodi per tutto;
  • Estensione del caso di test;
  • Mettendo le azioni su linee separate: in PEST puoi concatenarle.

Per generare un test PEST in Laravel, è necessario specificare un flag aggiuntivo:

php artisan make:test HomepageTest --pest

Al momento della stesura di questo articolo, PEST è piuttosto popolare tra gli sviluppatori Laravel, ma è una tua preferenza personale se utilizzare questo strumento aggiuntivo e apprenderne la sintassi, oltre a una nota PHPUnit.

BlogInnovazione.it

Autore