Articoli

Laravel API: come creare e testare un’API RESTful

l framework Laravel consente di risparmiare tempo agli sviluppatori favorendo la convenzione rispetto alla configurazione.

Laravel mira a evolversi insieme al web, e incorpora rapidamente diverse funzionalità innovative, e idee del mondo dello sviluppo web.

Job queues, autenticazione Laravel API, comunicazione in tempo reale e molto altro.

Tempo di lettura stimato: 13 minuti

In questo tutorial vedremo come creare e testare una Laravel API RESTful solida utilizzando il framework Laravel con autenticazione.

Laravel API RESTful

REST sta per REpresentational State Transfer ed è uno stile architettonico per la comunicazione di rete tra applicazioni, che si basa su un protocollo stateless (solitamente HTTP) per l’interazione.

I verbi HTTP rappresentano le azioni

Nelle Laravel API RESTful, utilizziamo i verbi HTTP come azioni e gli endpoint sono le risorse su cui si agisce. Utilizzeremo i verbi HTTP per il loro significato semantico:

  • GET: recuperare risorse
  • POST: creare risorse
  • PUT: aggiorna le risorse
  • DELETE: elimina le risorse
Metodi HTTP rappresentano le azioni

Azione di aggiornamento: PUT vs. POST

In merito alle API RESTful ci sono opinioni divergenti se aggiornare con POSTPATCH, o PUT, o se sia meglio lasciare l’azione create al verbo PUT. In questo articolo utilizzeremo PUT per l’azione di aggiornamento, poiché secondo HTTP RFCPUT significa creare/aggiornare una risorsa in una posizione specifica. Un altro requisito per il verbo PUT è l’idempotenza, che in questo caso significa sostanzialmente che puoi inviare quella richiesta 1, 2 o 1000 volte e il risultato sarà lo stesso: una risorsa aggiornata nel database.

Risorse

Le risorse saranno gli obiettivi delle azioni, nel nostro caso Items e Users, e avranno i propri endpoint:

  • /items
  • /users

In questo tutorial sulle Laravel API, le risorse avranno una rappresentazione 1:1 nei nostri modelli di dati. È possibile avere risorse rappresentate in più di un modello dati (o non rappresentate affatto nel database) e modelli completamente non accessibili all’utente. Alla fine, sarai tu a decidere come progettare risorse e modelli in modo che si adattino alla tua applicazione.

Una nota sulla coerenza

Il vantaggio più grande derivante dall’utilizzo di una serie di convenzioni come REST è che la tua API sarà molto più semplice da utilizzare e sviluppare. Alcuni endpoint sono piuttosto semplici e, di conseguenza, la tua API sarà molto più facile da utilizzare e gestire rispetto ad endpoint come GET /get_item?id_item=12 POST /delete_item?number=40.

Tuttavia, in alcuni casi sarà difficile eseguire il mapping a uno schema di creazione/recupero/aggiornamento/eliminazione. Ricorda che gli URL non devono contenere verbi e che le risorse non sono necessariamente righe di una tabella. Un’altra cosa da tenere presente è che non è necessario implementare ogni azione per ogni risorsa.

Impostazione progetto per servizio Web Laravel

Come con tutti i moderni framework PHP, avremo bisogno di Composer per installare e gestire le nostre dipendenze. Dopo aver seguito le istruzioni di download (e aver aggiunto la variabile d’ambiente al percorso), installiamo Laravel utilizzando il comando:

$ composer global require laravel/installer

Al termine dell’installazione, puoi creare una nuova applicazione come segue:

$ laravel new myapp

Per il comando precedente, devi avere ~/composer/vendor/bin nel tuo file $PATH. Se non vuoi occupartene, puoi anche creare un nuovo progetto utilizzando Composer, come segue:

$ composer create-project --prefer-dist laravel/laravel myapp

Con Laravel installato, dovresti essere in grado di avviare il server e verificare se tutto funziona:

$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>
Laravel installato

Quando apri 

https://localhost:8000 nel browser, dovresti vedere questa pagina di esempio.

Migrations e Models

Prima di scrivere la nostra migration, dobbiamo assicurarci di avere un database creato per questa app e aggiungere le sue credenziali al nel file .env situato nella radice del progetto.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=warehoue
DB_USERNAME=warehoue
DB_PASSWORD=topsecret

Iniziamo con il nostro primo model e migration: l’item, che dovrebbe avere un campo nome e un campo per il prezzo, nonché una data di creazione. Laravel fornisce diversi comandi tramite Artisan, che ci aiuta a generare file inserendoli nelle cartelle corrette. Per creare il modello Item, possiamo eseguire:

$ php artisan make:model Item -m

L’ opzione -m è l’abbreviazione di --migration e indica ad Artisan di crearne uno per il nostro modello. Ecco la migration generata:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('items');
    }
}

Analizziamolo per un secondo:

  • I metodi up()down() vengono eseguiti rispettivamente durante la migrate e l’eventuale rollback;
  • $table->increments('id') imposta un campo intero autoincrementante nominandolo id;
  • $table->timestamps() imposterà i campi created_atupdated_at di tipo timestamp. Non dobbiamo preoccuparci di impostare un valore predefinito, perchè Laravel si occupa di aggiornare questi campi quando necessario.
  • E infine, Schema::dropIfExists() per cancellare la tabella, se esiste.

Detto questo, aggiungiamo due righe al nostro metodo up():

public function up()
{
    Schema::create('items', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->double('price');
        $table->timestamps();
    });
}

Il metodo string() ci crea una colonna VARCHAR equivalente mentre il metodo double() ci crea un DOUBLE equivalente. Fatto ciò, procediamo con la migrazione:

$ php artisan migrate

Puoi anche utilizzare l’opzione --step, che separa ogni migrazione nel proprio batch in modo da poterla ripristinare individualmente se necessario.

Laravel viene fornito con due migrazioni create_users_table create_password_resets_table. Non utilizzeremo la tabella password_resets, ma la tabella users ci sarà utile.

Ora torniamo al nostro modello e aggiungiamo questi attributi al campo $fillable in modo da poterli utilizzare nei modelli Item::create e Item::update

class Item extends Model
{
    protected $fillable = ['name', 'price'];
}

I campi all’interno della proprietà $fillable possono essere assegnati in massa utilizzando i metodi create() e update() di Eloquent. È inoltre possibile utilizzare la proprietà $guarded per consentire tutte le proprietà tranne alcune.

Seeding del database

Il seeding del database è il processo di riempimento del database con dati fittizi che possiamo utilizzare per testarlo. Laravel viene fornito con Faker , un’ottima libreria per generare per noi solo il formato corretto di dati fittizi. Creiamo quindi il nostro primo seeder:

$ php artisan make:seeder ItemsTableSeeder

I seeders sono nella directory /database/seeds. Ecco come appare dopo averlo configurato per creare alcuni items:

class ItemsTableSeeder extends Seeder
{
    public function run()
    {
        // Let's truncate our existing records to start from scratch.
        Item::truncate();

        $faker = \Faker\Factory::create();

        // And now, let's create a few items in our database:
        for ($i = 0; $i < 50; $i++) {
            Item::create([
                'name' => $faker->sentence,
                'price' => $book->setPrice($faker->randomNumber(2)),
            ]);
        }
    }
}

Quindi eseguiamo il comando seed:

$ php artisan db:seed --class=ItemTableSeeder

Ripetiamo il processo per creare un seeder Users:

class UsersTableSeeder extends Seeder
{
    public function run()
    {
        // Let's clear the users table first
        User::truncate();

        $faker = \Faker\Factory::create();

        // Let's make sure everyone has the same password and 
        // let's hash it before the loop, or else our seeder 
        // will be too slow.
        $password = Hash::make('bloginnovazione');

        User::create([
            'name' => 'Administrator',
            'email' => 'admin@test.com',
            'password' => $password,
        ]);

        // And now let's generate a few dozen users for our app:
        for ($i = 0; $i < 10; $i++) {
            User::create([
                'name' => $faker->name,
                'email' => $faker->email,
                'password' => $password,
            ]);
        }
    }
}

Possiamo renderlo più semplice aggiungendo i nostri seeders alla DatabaseSeederclasse principale all’interno della database/seeds cartella:

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $this->call(ItemsTableSeeder::class);
        $this->call(UsersTableSeeder::class);
    }
}

In questo modo, possiamo semplicemente eseguire $php artisan db:seed e verranno eseguite tutte le classi chiamate nel metodo run().

Routes e Controllers

Creiamo gli endpoint di base per la nostra applicazione: crea, recupera l’elenco, recuperane uno singolo, aggiorna ed elimina. Sul file routes/api.php, possiamo semplicemente fare questo:

Use App\Item;
 
Route::get('items', function() {
    // If the Content-Type and Accept headers are set to 'application/json', 
    // this will return a JSON structure. This will be cleaned up later.
    return Item::all();
});
 
Route::get('items/{id}', function($id) {
    return Items::find($id);
});

Route::post('items', function(Request $request) {
    return Item::create($request->all);
});

Route::put('items/{id}', function(Request $request, $id) {
    $item = Item::findOrFail($id);
    $item->update($request->all());

    return $item;
});

Route::delete('items/{id}', function($id) {
    Item::find($id)->delete();

    return 204;
})

Le rotte all’interno api.php avranno il prefisso /api/ e il middleware di limitazione dell’API verrà applicato automaticamente a queste rotte (se desideri rimuovere il prefisso puoi modificare la classe RouteServiceProvider su /app/Providers/RouteServiceProvider.php).

Ora spostiamo questo codice sul proprio Controller:

$ php artisan make:controller ItemController

ItemController.php:

use App\Item;
 
class ItemController extends Controller
{
    public function index()
    {
        return Item::all();
    }
 
    public function show($id)
    {
        return Item::find($id);
    }

    public function store(Request $request)
    {
        return Item::create($request->all());
    }

    public function update(Request $request, $id)
    {
        $item = Item::findOrFail($id);
        $item->update($request->all());

        return $item;
    }

    public function delete(Request $request, $id)
    {
        $item = Item::findOrFail($id);
        $item->delete();

        return 204;
    }
}

Il file routes/api.php:

Route::get('items', 'ItemController@index');
Route::get('items/{id}', 'ItemController@show');
Route::post('items', 'ItemController@store');
Route::put('items/{id}', 'ItemController@update');
Route::delete('items/{id}', 'ItemController@delete');

Possiamo migliorare gli endpoint utilizzando l’associazione implicita del modello di percorso. In questo modo, Laravel inietterà l’istanza Item nei nostri metodi e restituirà automaticamente un 404 se non viene trovata. Dovremo apportare modifiche al file dei percorsi e al controller:

Route::get('items', 'ItemController@index');
Route::get('items/{item}', 'ItemController@show');
Route::post('items', 'ItemController@store');
Route::put('items/{item}', 'ItemController@update');
Route::delete('items/{item}', 'ItemController@delete');
class ItemController extends Controller
{
    public function index()
    {
        return Item::all();
    }

    public function show(Item $item)
    {
        return $item;
    }

    public function store(Request $request)
    {
        $item= Item::create($request->all());

        return response()->json($item, 201);
    }

    public function update(Request $request, Item $item)
    {
        $item->update($request->all());

        return response()->json($item, 200);
    }

    public function delete(Item $item)
    {
        $item->delete();

        return response()->json(null, 204);
    }
}

Codici di stato HTTP e formato della risposta

Aggiungiamo la response()->json() agli endpoint. Ciò ci consente di restituire esplicitamente dati JSON e di inviare un codice HTTP che può essere analizzato dal client. I codici più comuni che restituirai saranno:

  • 200: OK. Il codice di successo standard e l’opzione predefinita.
  • 201: Oggetto creato. Utile per le storeazioni.
  • 204: Nessun contenuto. Quando un’azione è stata eseguita correttamente, ma non è presente alcun contenuto da restituire.
  • 206: Contenuto parziale. Utile quando devi restituire un elenco impaginato di risorse.
  • 400: Brutta richiesta. L’opzione standard per le richieste che non superano la convalida.
  • 401: Non autorizzato. L’utente deve essere autenticato.
  • 403: Vietato. L’utente è autenticato, ma non dispone delle autorizzazioni per eseguire un’azione.
  • 404: Non trovato. Questo verrà restituito automaticamente da Laravel quando la risorsa non viene trovata.
  • 500: Errore interno del server. Idealmente non lo restituirai esplicitamente, ma se qualcosa si rompe inaspettatamente, questo è ciò che riceverà il tuo utente.
  • 503: Servizio non disponibile. Abbastanza autoesplicativo, ma anche un altro codice che non verrà restituito esplicitamente dall’applicazione.

Invio di una risposta 404 corretta

Se hai provato a recuperare una risorsa inesistente, ti verrà lanciata un’eccezione e riceverai l’intero stacktrace, in questo modo:

Possiamo risolvere questo problema modificando la nostra classe del gestore delle eccezioni, situata in app/Exceptions/Handler.php, per restituire una risposta JSON:

public function render($request, Exception $exception)
{
    // This will replace our 404 response with
    // a JSON response.
    if ($exception instanceof ModelNotFoundException) {
        return response()->json([
            'error' => 'Resource not found'
        ], 404);
    }

    return parent::render($request, $exception);
}

Ecco un esempio del reso:

{
    data: "Resource not found"
}

Se stai utilizzando Laravel per servire altre pagine, devi modificare il codice per farlo funzionare con l’intestazione Accept, altrimenti gli errori 404 provenienti dalle richieste regolari restituiranno anche un JSON.

public function render($request, Exception $exception)
{
    // This will replace our 404 response with
    // a JSON response.
    if ($exception instanceof ModelNotFoundException &&
        $request->wantsJson())
    {
        return response()->json([
            'data' => 'Resource not found'
        ], 404);
    }

    return parent::render($request, $exception);
}

In questo caso, le richieste API avranno bisogno dell’intestazione Accept: application/json.

Autenticazione

Esistono molti modi per implementare l’autenticazione API in Laravel (uno di questi è Passport , un ottimo modo per implementare OAuth2), ma in questo articolo adotteremo un approccio molto semplificato.

Per iniziare, dovremo aggiungere un campo api_token alla tabella users:

Newsletter sull’Innovazione
Non perderti le notizie più importanti sull'Innovazione. Iscriviti per riceverle via e-mail.
$ php artisan make:migration --table=users adds_api_token_to_users_table

E quindi implementare la migrazione:

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('api_token', 60)->unique()->nullable();
    });
}

public function down()
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn(['api_token']);
    });
}

Successivamente, esegui semplicemente la migrazione utilizzando:

$ php artisan migrate

Creazione dell’endpoint del registro

Utilizzeremo il simbolo RegisterController (nella cartella Auth) per restituire la risposta corretta al momento della registrazione. Laravel viene fornito con l’autenticazione pronta all’uso, ma dobbiamo ancora modificarla leggermente per restituire la risposta che desideriamo.

Il titolare del trattamento utilizza la caratteristica per RegistersUsers implementare la registrazione. Ecco come funziona:

public function register(Request $request)
{
    // Here the request is validated. The validator method is located
    // inside the RegisterController, and makes sure the name, email
    // password and password_confirmation fields are required.
    $this->validator($request->all())->validate();

    // A Registered event is created and will trigger any relevant
    // observers, such as sending a confirmation email or any 
    // code that needs to be run as soon as the user is created.
    event(new Registered($user = $this->create($request->all())));

    // After the user is created, he's logged in.
    $this->guard()->login($user);

    // And finally this is the hook that we want. If there is no
    // registered() method or it returns null, redirect him to
    // some other URL. In our case, we just need to implement
    // that method to return the correct response.
    return $this->registered($request, $user)
                    ?: redirect($this->redirectPath());
}

Dobbiamo solo implementare il metodo registered() nel nostro file RegisterController. Il metodo riceve la $request e $user, ed è ciò che vogliamo. Ecco come dovrebbe apparire il metodo all’interno del controller:

protected function registered(Request $request, $user)
{
    $user->generateToken();

    return response()->json(['data' => $user->toArray()], 201);
}

E possiamo collegarlo al file dei percorsi:

Route::post('register', 'Auth\RegisterController@register');

Nella sezione precedente, abbiamo utilizzato un metodo sul modello User per generare il token. Questo è utile in modo che abbiamo un solo modo per generare i token. Aggiungi il seguente metodo al tuo modello Utente:

class User extends Authenticatable
{
    ...
    public function generateToken()
    {
        $this->api_token = str_random(60);
        $this->save();

        return $this->api_token;
    }
}

L’utente è ora registrato e grazie alla convalida di Laravel e all’autenticazione immediata, i campi nameemailpassword password_confirmation sono obbligatori e il feedback viene gestito automaticamente. Controlla il metodo validator() all’interno RegisterController per vedere come vengono implementate le regole.

Ecco cosa otteniamo quando raggiungiamo l’endpoint:

$ curl -X POST http://localhost:8000/api/register \
 -H "Accept: application/json" \
 -H "Content-Type: application/json" \
 -d '{"name": "John", "email": "john.doe@bloginnovazione.it", "password": "bloginnovazione123", "password_confirmation": "bloginnovazione123"}'
{
    "data": {
        "api_token":"0syHnl0Y9jOIfszq11EC2CBQwCfObmvscrZYo5o2ilZPnohvndH797nDNyAT",
        "created_at": "2017-06-20 21:17:15",
        "email": "john.doe@bloginnovazione.it",
        "id": 51,
        "name": "John",
        "updated_at": "2017-06-20 21:17:15"
    }
}

Creazione di un endpoint di accesso

Proprio come l’endpoint di registrazione, possiamo modificare LoginController (nella cartella Auth) per supportare la nostra autenticazione API. Il metodo login di AuthenticatesUsers può essere sovrascritto per supportare la nostra Laravel API:

public function login(Request $request)
{
    $this->validateLogin($request);

    if ($this->attemptLogin($request)) {
        $user = $this->guard()->user();
        $user->generateToken();

        return response()->json([
            'data' => $user->toArray(),
        ]);
    }

    return $this->sendFailedLoginResponse($request);
}

E possiamo collegarlo al file dei percorsi:

Route::post('login', 'Auth\LoginController@login');

Ora, presupponendo che i seeders siano stati eseguiti, ecco cosa otteniamo quando inviamo una richiesta POST a quel percorso:

$ curl -X POST localhost:8000/api/login \
  -H "Accept: application/json" \
  -H "Content-type: application/json" \
  -d "{\"email\": \"admin@test.com\", \"password\": \"bloginnovazione\" }"
{
    "data": {
        "id":1,
        "name":"Administrator",
        "email":"admin@test.com",
        "created_at":"2017-04-25 01:05:34",
        "updated_at":"2017-04-25 02:50:40",
        "api_token":"Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw"
    }
}

Per inviare il token in una richiesta, puoi farlo inviando un attributo api_token nel payload o come token al portatore nelle intestazioni della richiesta sotto forma di Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw.

Disconnessione

Con la nostra strategia attuale, se il token è sbagliato o mancante, l’utente dovrebbe ricevere una risposta non autenticata (che implementeremo nella sezione successiva). Pertanto, per un endpoint di disconnessione semplice, invieremo il token e verrà rimosso dal database.

routes/api.php:

Route::post('logout', 'Auth\LoginController@logout');

Auth\LoginController.php:

public function logout(Request $request)
{
    $user = Auth::guard('api')->user();

    if ($user) {
        $user->api_token = null;
        $user->save();
    }

    return response()->json(['data' => 'User logged out.'], 200);
}

Utilizzando questa strategia, qualunque token abbia l’utente non sarà valido e la Laravel API negherà l’accesso (utilizzando middleware, come spiegato nella sezione successiva). Questo deve essere coordinato con il front-end per evitare che l’utente rimanga loggato senza avere accesso ad alcun contenuto.

Utilizzo di middleware per limitare l’accesso

Una volta creato api_token, possiamo attivare/disattivare il middleware di autenticazione nel file dei percorsi:

Route::middleware('auth:api')
    ->get('/user', function (Request $request) {
        return $request->user();
    });

Possiamo accedere all’utente corrente utilizzando il metodo $request->user() o tramite la facade Auth

Auth::guard('api')->user(); // instance of the logged user
Auth::guard('api')->check(); // if a user is authenticated
Auth::guard('api')->id(); // the id of the authenticated user

E otteniamo un risultato come questo:

Questo perché dobbiamo modificare il metodo unauthenticated corrente sulla nostra classe Handler. La versione attuale restituisce un JSON solo se la richiesta ha l’intestazione Accept: application/json, quindi cambiamola:

protected function unauthenticated($request, AuthenticationException $exception)
{
    return response()->json(['error' => 'Unauthenticated'], 401);
}

Risolto questo problema, possiamo tornare agli endpoint dell’articolo per inserirli nel middleware auth:api. Possiamo farlo utilizzando i gruppi di percorsi:

Route::group(['middleware' => 'auth:api'], function() {
    Route::get('items', 'ItemController@index');
    Route::get('items/{item}', 'ItemController@show');
    Route::post('items', 'ItemController@store');
    Route::put('items/{item}', 'ItemController@update');
    Route::delete('items/{item}', 'ItemController@delete');
});

In questo modo non dobbiamo impostare il middleware per ciascuno dei percorsi. Non fa risparmiare molto tempo in questo momento, ma man mano che il progetto cresce aiuta a mantenere i percorsi puliti.

Testare i nostri endpoint

Laravel include l’integrazione con PHPUnit pronta all’uso con un file phpunit.xml già configurato. Il framework ci fornisce anche diversi aiuti e asserzioni extra che ci rendono la vita molto più semplice, soprattutto per testare le Laravel API.

Esistono numerosi strumenti esterni che puoi utilizzare per testare la tua Laravel API; tuttavia, testare all’interno di Laravel è un’alternativa migliore: possiamo avere tutti i vantaggi di testare la struttura e i risultati dell’API mantenendo il pieno controllo del database. Per l’endpoint dell’elenco, ad esempio, potremmo eseguire un paio di fabbriche e affermare che la risposta contiene tali risorse.

Per iniziare, dovremo modificare alcune impostazioni per utilizzare un database SQLite in memoria. Usarlo renderà i nostri test estremamente veloci, ma il compromesso è che alcuni comandi di migrazione (vincoli, ad esempio) non funzioneranno correttamente in quella particolare configurazione.

Eseguiremo anche le migrazioni prima di ogni test. Questa configurazione ci consentirà di costruire il database per ogni test e poi di distruggerlo, evitando qualsiasi tipo di dipendenza tra i test.

Nel nostro file config/database.php, dovremo impostare il campo database, nella configurazione sqlite, con :memory::

...
'connections' => [

    'sqlite' => [
        'driver' => 'sqlite',
        'database' => ':memory:',
        'prefix' => '',
    ],
    
    ...
]

Quindi abilitiamo SQLite in phpunit.xml, aggiungendo la variabile di ambiente DB_CONNECTION:

    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="DB_CONNECTION" value="sqlite"/>
    </php>

Detto questo, tutto ciò che resta è configurare la nostra classe TestCase base per utilizzare le migration e i seeder, prima di ogni test. Per fare ciò, dobbiamo aggiungere il DatabaseMigrations e quindi aggiungere una chiamata Artisan al nostro metodo setUp().

Ecco la classe dopo le modifiche:

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Artisan;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, DatabaseMigrations;

    public function setUp()
    {
        parent::setUp();
        Artisan::call('db:seed');
    }
}

Una cosa da aggiungere è il comando test a composer.json:

    "scripts": {
        "test" : [
            "vendor/bin/phpunit"
        ],
    ... 
    },    

Il comando test sarà disponibile in questo modo:

$ composer test

Impostazione delle Factories per i test

Le factories ci consentiranno di creare rapidamente oggetti con i dati giusti per i test. Si trovano nella cartella database/factories. Laravel ha una factory per la classe User, quindi aggiungiamone una per la classe Item:

$factory->define(App\Item::class, function (Faker\Generator $faker) {
    return [
        'title' => $faker->sentence,
        'body' => $faker->paragraph,
    ];
});

La libreria Faker è già inserita per aiutarci a creare il formato corretto di dati casuali per i nostri modelli.

I nostri primi test

Possiamo utilizzare i metodi assert di Laravel per raggiungere facilmente un endpoint e valutarne la risposta. Creiamo il nostro primo test, il test di accesso, utilizzando il seguente comando:

$ php artisan make:test Feature/LoginTest

Ed ecco il nostro test:

class LoginTest extends TestCase
{
    public function testRequiresEmailAndLogin()
    {
        $this->json('POST', 'api/login')
            ->assertStatus(422)
            ->assertJson([
                'email' => ['The email field is required.'],
                'password' => ['The password field is required.'],
            ]);
    }


    public function testUserLoginsSuccessfully()
    {
        $user = factory(User::class)->create([
            'email' => 'testlogin@user.com',
            'password' => bcrypt('bloginnovazione123'),
        ]);

        $payload = ['email' => 'testlogin@user.com', 'password' => 'bloginnovazione123'];

        $this->json('POST', 'api/login', $payload)
            ->assertStatus(200)
            ->assertJsonStructure([
                'data' => [
                    'id',
                    'name',
                    'email',
                    'created_at',
                    'updated_at',
                    'api_token',
                ],
            ]);

    }
}

Questi metodi testano un paio di casi semplici. Il metodo json() raggiunge l’endpoint e le altre asserzioni sono piuttosto autoesplicative. Un dettaglio su assertJson(): questo metodo converte la risposta in un array che cerca l’argomento, quindi l’ordine è importante. In questo caso puoi concatenare più chiamate.

Ora creiamo il test dell’endpoint del registro e scriviamo una coppia per quell’endpoint:

$ php artisan make:test RegisterTest

class RegisterTest extends TestCase
{
    public function testsRegistersSuccessfully()
    {
        $payload = [
            'name' => 'John',
            'email' => 'john@bloginnovazione.it',
            'password' => 'bloginnovazione123',
            'password_confirmation' => 'bloginnovazione123',
        ];

        $this->json('post', '/api/register', $payload)
            ->assertStatus(201)
            ->assertJsonStructure([
                'data' => [
                    'id',
                    'name',
                    'email',
                    'created_at',
                    'updated_at',
                    'api_token',
                ],
            ]);;
    }

    public function testsRequiresPasswordEmailAndName()
    {
        $this->json('post', '/api/register')
            ->assertStatus(422)
            ->assertJson([
                'name' => ['The name field is required.'],
                'email' => ['The email field is required.'],
                'password' => ['The password field is required.'],
            ]);
    }

    public function testsRequirePasswordConfirmation()
    {
        $payload = [
            'name' => 'John',
            'email' => 'john@bloginnovazione.it',
            'password' => 'bloginnovazione123',
        ];

        $this->json('post', '/api/register', $payload)
            ->assertStatus(422)
            ->assertJson([
                'password' => ['The password confirmation does not match.'],
            ]);
    }
}

E infine, l’endpoint di disconnessione:

$ php artisan make:test LogoutTest
class LogoutTest extends TestCase
{
    public function testUserIsLoggedOutProperly()
    {
        $user = factory(User::class)->create(['email' => 'user@test.com']);
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];

        $this->json('get', '/api/items', [], $headers)->assertStatus(200);
        $this->json('post', '/api/logout', [], $headers)->assertStatus(200);

        $user = User::find($user->id);

        $this->assertEquals(null, $user->api_token);
    }

    public function testUserWithNullToken()
    {
        // Simulating login
        $user = factory(User::class)->create(['email' => 'user@test.com']);
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];

        // Simulating logout
        $user->api_token = null;
        $user->save();

        $this->json('get', '/api/items', [], $headers)->assertStatus(401);
    }
}

È importante notare che, durante il test, l’applicazione Laravel non viene nuovamente istanziata su una nuova richiesta. Ciò significa che quando raggiungiamo il middleware di autenticazione, salva l’utente corrente all’interno dell’istanza TokenGuardper evitare di colpire nuovamente il database. Una scelta saggia, tuttavia: in questo caso significa che dobbiamo dividere il test di logout in due, per evitare eventuali problemi con l’utente precedentemente memorizzato nella cache.

Anche testare gli endpoint dell’articolo è semplice:

class ItemTest extends TestCase
{
    public function testsItemsAreCreatedCorrectly()
    {
        $user = factory(User::class)->create();
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];
        $payload = [
            'title' => 'Lorem',
            'body' => 'Ipsum',
        ];

        $this->json('POST', '/api/items', $payload, $headers)
            ->assertStatus(200)
            ->assertJson(['id' => 1, 'title' => 'Lorem', 'body' => 'Ipsum']);
    }

    public function testsItemsAreUpdatedCorrectly()
    {
        $user = factory(User::class)->create();
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];
        $item = factory(Item::class)->create([
            'title' => 'First Item',
            'body' => 'First Body',
        ]);

        $payload = [
            'title' => 'Lorem',
            'body' => 'Ipsum',
        ];

        $response = $this->json('PUT', '/api/items/' . $item->id, $payload, $headers)
            ->assertStatus(200)
            ->assertJson([ 
                'id' => 1, 
                'title' => 'Lorem', 
                'body' => 'Ipsum' 
            ]);
    }

    public function testsArtilcesAreDeletedCorrectly()
    {
        $user = factory(User::class)->create();
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];
        $item = factory(Item::class)->create([
            'title' => 'First Item',
            'body' => 'First Body',
        ]);

        $this->json('DELETE', '/api/items/' . $item->id, [], $headers)
            ->assertStatus(204);
    }

    public function testItemsAreListedCorrectly()
    {
        factory(Item::class)->create([
            'title' => 'First Item',
            'body' => 'First Body'
        ]);

        factory(Item::class)->create([
            'title' => 'Second Item',
            'body' => 'Second Body'
        ]);

        $user = factory(User::class)->create();
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];

        $response = $this->json('GET', '/api/items', [], $headers)
            ->assertStatus(200)
            ->assertJson([
                [ 'title' => 'First Item', 'body' => 'First Body' ],
                [ 'title' => 'Second Item', 'body' => 'Second Body' ]
            ])
            ->assertJsonStructure([
                '*' => ['id', 'body', 'title', 'created_at', 'updated_at'],
            ]);
    }

}

Letture Correlate

Ercole Palmeri

Newsletter sull’Innovazione
Non perderti le notizie più importanti sull'Innovazione. Iscriviti per riceverle via e-mail.