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.
- Laravel API RESTful
- I verbi HTTP rappresentano le azioni
- Azione di aggiornamento: PUT vs. POST
- Risorse
- Una nota sulla coerenza
- Impostazione progetto per servizio Web Laravel
- Migrations e Models
- Seeding del database
- Routes e Controllers
- Codici di stato HTTP e formato della risposta
- Invio di una risposta 404 corretta
- Autenticazione
- Creazione dell’endpoint del registro
- Creazione di un endpoint di accesso
- Disconnessione
- Utilizzo di middleware per limitare l’accesso
- Testare i nostri endpoint
- Impostazione delle Factories per i test
- I nostri primi test
- Letture Correlate
Tempo di lettura stimato: 13 minuti
In questo tutorial vedremo come creare e testare una Laravel API
solida utilizzando il framework RESTful
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 risorsePOST
: creare risorsePUT
: aggiorna le risorseDELETE
: elimina le risorse
Azione di aggiornamento: PUT
vs. POST
In merito alle API RESTful
ci sono opinioni divergenti se aggiornare con POST
, PATCH
, 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 RFC
, PUT
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
e 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>
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()
edown()
vengono eseguiti rispettivamente durante lamigrate
e l’eventualerollback
; $table->increments('id')
imposta un campo intero autoincrementante nominandoloid
;$table->timestamps()
imposterà i campicreated_at
eupdated_at
di tipotimestamp
. 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
e 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
e create()
di Eloquent. È inoltre possibile utilizzare la proprietà update()
$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 lestore
azioni.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
:
$ 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 name
, email
, password
e 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 TokenGuard
per 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