Tutorial API Laravel: Cum să construiți și să testați un API RESTful
Publicat: 2022-03-11Odată cu creșterea dezvoltării mobile și a cadrelor JavaScript, utilizarea unui API RESTful este cea mai bună opțiune pentru a construi o interfață unică între datele și clientul dvs.
Laravel este un cadru PHP dezvoltat având în vedere productivitatea dezvoltatorilor PHP. Scris și întreținut de Taylor Otwell, cadrul este foarte înțelept și se străduiește să economisească timp dezvoltatorului, favorizând convențiile față de configurație. Cadrul urmărește, de asemenea, să evolueze odată cu web-ul și a încorporat deja mai multe funcții și idei noi în lumea dezvoltării web, cum ar fi cozile de locuri de muncă, autentificarea API din cutie, comunicarea în timp real și multe altele.
În acest tutorial, vom explora modalitățile prin care puteți construi și testa un API robust folosind Laravel cu autentificare. Vom folosi Laravel 5.4 și tot codul este disponibil pentru referință pe GitHub.
API-uri RESTful
În primul rând, trebuie să înțelegem ce anume este considerat un API RESTful. REST înseamnă REpresentational State Transfer și este un stil arhitectural pentru comunicarea în rețea între aplicații, care se bazează pe un protocol fără stat (de obicei HTTP) pentru interacțiune.
Verbele HTTP reprezintă acțiuni
În API-urile RESTful, folosim verbele HTTP ca acțiuni, iar punctele finale sunt resursele asupra cărora se acționează. Vom folosi verbele HTTP pentru semnificația lor semantică:
-
GET
: recuperați resurse -
POST
: creează resurse -
PUT
: actualizare resurse -
DELETE
: ștergeți resurse
Acțiune de actualizare: PUT vs. POST
API-urile RESTful sunt o chestiune de multe dezbateri și există o mulțime de opinii despre dacă este cel mai bine să actualizați cu POST
, PATCH
sau PUT
, sau dacă acțiunea de creare este mai bine lăsată la verbul PUT
. În acest articol vom folosi PUT
pentru acțiunea de actualizare, deoarece conform RFC HTTP, PUT
înseamnă a crea/actualiza o resursă într-o anumită locație. O altă cerință pentru verbul PUT
este idempotenta, ceea ce în acest caz înseamnă practic că poți trimite acea cerere de 1, 2 sau 1000 de ori și rezultatul va fi același: o resursă actualizată în baza de date.
Resurse
Resursele vor fi ținta acțiunilor, în cazul nostru Articole și Utilizatori, și au propriile lor puncte finale:
-
/articles
-
/users
În acest tutorial api laravel, resursele vor avea o reprezentare 1:1 pe modelele noastre de date, dar aceasta nu este o cerință. Puteți avea resurse reprezentate în mai mult de un model de date (sau deloc reprezentate în baza de date) și modele complet interzise pentru utilizator. În cele din urmă, vei decide cum să arhitecți resursele și modelele într-un mod care se potrivește aplicației tale.
O notă despre consistență
Cel mai mare avantaj al utilizării unui set de convenții precum REST este că API-ul dvs. va fi mult mai ușor de consumat și dezvoltat. Unele puncte finale sunt destul de simple și, în consecință, API-ul dvs. va fi mult mai ușor de utilizat și de întreținut, în comparație cu punctele finale precum GET /get_article?id_article=12
și POST /delete_article?number=40
. Am construit API-uri groaznice în trecut și încă mă urăsc pentru asta.
Cu toate acestea, vor exista cazuri în care va fi dificil să se mapeze la o schemă Creare/Preluare/Actualizare/Ștergere. Rețineți că adresele URL nu trebuie să conțină verbe și că resursele nu sunt neapărat rânduri dintr-un tabel. Un alt lucru de reținut este că nu trebuie să implementați fiecare acțiune pentru fiecare resursă.
Configurarea unui proiect Laravel Web Service
Ca și în cazul tuturor cadrelor PHP moderne, vom avea nevoie de Composer pentru a instala și gestiona dependențele noastre. După ce urmați instrucțiunile de descărcare (și adăugați la variabila de mediu cale), instalați Laravel utilizând comanda:
$ composer global require laravel/installer
După ce instalarea se termină, puteți monta o nouă aplicație ca aceasta:
$ laravel new myapp
Pentru comanda de mai sus, trebuie să aveți ~/composer/vendor/bin
în $PATH
. Dacă nu doriți să vă ocupați de asta, puteți crea și un nou proiect folosind Composer:
$ composer create-project --prefer-dist laravel/laravel myapp
Cu Laravel instalat, ar trebui să puteți porni serverul și să testați dacă totul funcționează:
$ php artisan serve Laravel development server started: <http://127.0.0.1:8000>
localhost:8000
în browser, ar trebui să vedeți această pagină exemplu.Migrații și modele
Înainte de a scrie efectiv prima migrare, asigurați-vă că aveți o bază de date creată pentru această aplicație și adăugați acreditările acesteia în fișierul .env
situat în rădăcina proiectului.
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret
De asemenea, puteți folosi Homestead, o cutie Vagrant special creată pentru Laravel, dar aceasta este puțin în afara scopului acestui articol. Dacă doriți să aflați mai multe, consultați documentația Homestead.
Să începem cu primul nostru model și migrare — articolul. Articolul ar trebui să aibă un titlu și un câmp de corp, precum și o dată de creare. Laravel oferă mai multe comenzi prin Artisan—instrumentul de linie de comandă al lui Laravel—care ne ajută prin generarea de fișiere și plasarea lor în folderele corecte. Pentru a crea modelul articol, putem rula:
$ php artisan make:model Article -m
Opțiunea -m
este prescurtarea pentru --migration
și îi spune lui Artisan să creeze unul pentru modelul nostru. Iată migrația generată:
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateArticlesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('articles'); } }
Să disecăm asta pentru o secundă:
- Metodele
up()
șidown()
vor fi rulate când migrăm și, respectiv, rollback; -
$table->increments('id')
setează un număr întreg cu incrementare automată cu numeleid
; -
$table->timestamps()
va configura marcajele de timp pentru noi—created_at
șiupdated_at
, dar nu vă faceți griji cu privire la setarea implicită, Laravel se ocupă de actualizarea acestor câmpuri atunci când este necesar. - Și, în cele din urmă,
Schema::dropIfExists()
va, desigur, să arunce tabelul dacă acesta există.
Cu asta din drum, să adăugăm două linii la metoda noastră up()
:
public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('body'); $table->timestamps(); }); }
Metoda string()
creează o coloană echivalentă VARCHAR
în timp ce text()
creează un echivalent TEXT
. După ce s-a făcut, hai să mergem mai departe și să migrăm:
$ php artisan migrate
De asemenea, puteți utiliza opțiunea
--step
aici și va separa fiecare migrare în propriul lot, astfel încât să le puteți derula înapoi individual, dacă este necesar.
Laravel vine cu două migrări, create_users_table
și create_password_resets_table
. Nu vom folosi tabelul password_resets
, dar pregătirea tabelului users
va fi de ajutor.
Acum să ne întoarcem la modelul nostru și să adăugăm acele atribute în câmpul $fillable
, astfel încât să le putem folosi în modelele noastre Article::create
și Article::update
:
class Article extends Model { protected $fillable = ['title', 'body']; }
Câmpurile din proprietatea
$fillable
pot fi atribuite în masă folosind metodelecreate()
șiupdate()
ale Eloquent. De asemenea, puteți utiliza proprietatea$guarded
, pentru a permite toate proprietățile, cu excepția unora.
Semănarea bazei de date
Seedingul bazei de date este procesul de completare a bazei de date cu date fictive pe care le putem folosi pentru a o testa. Laravel vine cu Faker, o bibliotecă excelentă pentru a genera doar formatul corect de date fictive pentru noi. Deci, să creăm primul nostru semănător:
$ php artisan make:seeder ArticlesTableSeeder
Seeders-urile vor fi localizate în directorul /database/seeds
. Iată cum arată după ce l-am configurat pentru a crea câteva articole:
class ArticlesTableSeeder extends Seeder { public function run() { // Let's truncate our existing records to start from scratch. Article::truncate(); $faker = \Faker\Factory::create(); // And now, let's create a few articles in our database: for ($i = 0; $i < 50; $i++) { Article::create([ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]); } } }
Deci, să rulăm comanda seed:
$ php artisan db:seed --class=ArticlesTableSeeder
Să repetăm procesul pentru a crea 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('toptal'); User::create([ 'name' => 'Administrator', 'email' => '[email protected]', '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, ]); } } }
O putem face mai ușoară adăugând seederele noastre la clasa principală DatabaseSeeder
din interiorul folderului baza de database/seeds
:
class DatabaseSeeder extends Seeder { public function run() { $this->call(ArticlesTableSeeder::class); $this->call(UsersTableSeeder::class); } }
În acest fel, putem rula pur și simplu $ php artisan db:seed
și va rula toate clasele apelate în metoda run()
.
Rute și controlere
Să creăm punctele finale de bază pentru aplicația noastră: creați, preluați lista, preluați unul singur, actualizați și ștergeți. Pe fișierul routes/api.php
, putem face pur și simplu acest lucru:
Use App\Article; Route::get('articles', 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 Article::all(); }); Route::get('articles/{id}', function($id) { return Article::find($id); }); Route::post('articles', function(Request $request) { return Article::create($request->all); }); Route::put('articles/{id}', function(Request $request, $id) { $article = Article::findOrFail($id); $article->update($request->all()); return $article; }); Route::delete('articles/{id}', function($id) { Article::find($id)->delete(); return 204; })
Rutele din interiorul api.php
vor fi prefixate cu /api/
și middleware-ul de throttling API va fi aplicat automat acestor rute (dacă doriți să eliminați prefixul puteți edita clasa RouteServiceProvider
pe /app/Providers/RouteServiceProvider.php
).
Acum să mutăm acest cod în propriul său controler:
$ php artisan make:controller ArticleController
ArticleController.php:
use App\Article; class ArticleController extends Controller { public function index() { return Article::all(); } public function show($id) { return Article::find($id); } public function store(Request $request) { return Article::create($request->all()); } public function update(Request $request, $id) { $article = Article::findOrFail($id); $article->update($request->all()); return $article; } public function delete(Request $request, $id) { $article = Article::findOrFail($id); $article->delete(); return 204; } }
Fișierul routes/api.php
:
Route::get('articles', 'ArticleController@index'); Route::get('articles/{id}', 'ArticleController@show'); Route::post('articles', 'ArticleController@store'); Route::put('articles/{id}', 'ArticleController@update'); Route::delete('articles/{id}', 'ArticleController@delete');
Putem îmbunătăți punctele finale utilizând legarea implicită a modelului de rută. În acest fel, Laravel va injecta instanța Article
în metodele noastre și va returna automat un 404 dacă nu este găsit. Va trebui să facem modificări în fișierul rute și pe controler:
Route::get('articles', 'ArticleController@index'); Route::get('articles/{article}', 'ArticleController@show'); Route::post('articles', 'ArticleController@store'); Route::put('articles/{article}', 'ArticleController@update'); Route::delete('articles/{article}', 'ArticleController@delete');
class ArticleController extends Controller { public function index() { return Article::all(); } public function show(Article $article) { return $article; } public function store(Request $request) { $article = Article::create($request->all()); return response()->json($article, 201); } public function update(Request $request, Article $article) { $article->update($request->all()); return response()->json($article, 200); } public function delete(Article $article) { $article->delete(); return response()->json(null, 204); } }
O notă despre codurile de stare HTTP și formatul de răspuns
De asemenea, am adăugat response()->json()
la punctele noastre finale. Acest lucru ne permite să returnăm în mod explicit datele JSON și să trimitem un cod HTTP care poate fi analizat de client. Cele mai comune coduri pe care le veți returna vor fi:
-
200
: OK. Codul standard de succes și opțiunea implicită. -
201
: Obiect creat. Util pentru acțiunilestore
. -
204
: Fără conținut. Când o acțiune a fost executată cu succes, dar nu există conținut de returnat. -
206
: Conținut parțial. Util atunci când trebuie să returnați o listă paginată de resurse. -
400
: Cerere proastă. Opțiunea standard pentru cererile care nu trec validarea. -
401
: Neautorizat. Utilizatorul trebuie să fie autentificat. -
403
: Interzis. Utilizatorul este autentificat, dar nu are permisiunile pentru a efectua o acțiune. -
404
: Nu a fost găsit. Acesta va fi returnat automat de Laravel atunci când resursa nu este găsită. -
500
: Eroare internă de server. În mod ideal, nu veți returna în mod explicit acest lucru, dar dacă ceva neașteptat se întrerupe, acesta este ceea ce va primi utilizatorul dvs. -
503
: Serviciu indisponibil. Destul de explicativ, dar și un alt cod care nu va fi returnat explicit de aplicație.
Trimiterea unui răspuns corect 404
Dacă ați încercat să preluați o resursă inexistentă, vi se va arunca o excepție și veți primi întregul stacktrace, astfel:
Putem remedia acest lucru prin editarea clasei noastre de gestionare a excepțiilor, situată în app/Exceptions/Handler.php
, pentru a returna un răspuns 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); }
Iată un exemplu de retur:
{ data: "Resource not found" }
Dacă utilizați Laravel pentru a difuza alte pagini, trebuie să editați codul pentru a funcționa cu antetul Accept
, altfel erorile 404 din solicitările obișnuite vor returna și 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); }
În acest caz, solicitările API vor avea nevoie de antetul Accept: application/json
.
Autentificare
Există multe modalități de a implementa autentificarea API în Laravel (una dintre ele fiind Passport, o modalitate excelentă de a implementa OAuth2), dar în acest articol vom adopta o abordare foarte simplificată.
Pentru a începe, va trebui să adăugăm un câmp api_token
la tabelul users
:
$ php artisan make:migration --table=users adds_api_token_to_users_table
Și apoi implementați migrarea:
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']); }); }
După aceea, rulați migrarea folosind:
$ php artisan migrate
Crearea punctului final de înregistrare
Vom folosi RegisterController
(în folderul Auth
) pentru a returna răspunsul corect la înregistrare. Laravel vine cu autentificarea din cutie, dar trebuie totuși să o modificăm puțin pentru a returna răspunsul pe care îl dorim.
Controlorul folosește caracteristica RegistersUsers
pentru a implementa înregistrarea. Iată cum funcționează:
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()); }
Trebuie doar să implementăm metoda registered()
în RegisterController
. Metoda primește $request
și $user
, așa că asta e tot ce ne dorim. Iată cum ar trebui să arate metoda în interiorul controlerului:

protected function registered(Request $request, $user) { $user->generateToken(); return response()->json(['data' => $user->toArray()], 201); }
Și îl putem lega pe fișierul rute:
Route::post('register', 'Auth\RegisterController@register');
În secțiunea de mai sus, am folosit o metodă pe modelul User pentru a genera jetonul. Acest lucru este util, astfel încât să avem doar o singură modalitate de a genera jetoanele. Adăugați următoarea metodă la modelul dvs. de utilizator:
class User extends Authenticatable { ... public function generateToken() { $this->api_token = str_random(60); $this->save(); return $this->api_token; } }
Si asta e. Utilizatorul este acum înregistrat și, datorită validării lui Laravel și a autentificării out of the box, sunt necesare câmpurile name
, email
, password
și password_confirmation
, iar feedback-ul este gestionat automat. Verificați metoda validator()
din RegisterController
pentru a vedea cum sunt implementate regulile.
Iată ce obținem când atingem acel punct final:
$ curl -X POST http://localhost:8000/api/register \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name": "John", "email": "[email protected]", "password": "toptal123", "password_confirmation": "toptal123"}'
{ "data": { "api_token":"0syHnl0Y9jOIfszq11EC2CBQwCfObmvscrZYo5o2ilZPnohvndH797nDNyAT", "created_at": "2017-06-20 21:17:15", "email": "[email protected]", "id": 51, "name": "John", "updated_at": "2017-06-20 21:17:15" } }
Crearea unui punct final de conectare
La fel ca și punctul final de înregistrare, putem edita LoginController
(în folderul Auth
) pentru a sprijini autentificarea noastră API. Metoda de login
a caracteristicii AuthenticatesUsers
poate fi suprascrisă pentru a sprijini API-ul nostru:
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); }
Și îl putem lega pe fișierul rute:
Route::post('login', 'Auth\LoginController@login');
Acum, presupunând că seeders-urile au fost rulate, iată ce primim atunci când trimitem o solicitare POST
către acea rută:
$ curl -X POST localhost:8000/api/login \ -H "Accept: application/json" \ -H "Content-type: application/json" \ -d "{\"email\": \"[email protected]\", \"password\": \"toptal\" }"
{ "data": { "id":1, "name":"Administrator", "email":"[email protected]", "created_at":"2017-04-25 01:05:34", "updated_at":"2017-04-25 02:50:40", "api_token":"Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw" } }
Pentru a trimite jetonul într-o solicitare, o puteți face trimițând un atribut api_token
în încărcătura utilă sau ca simbol purtător în antetele cererii sub formă de Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw
.
Delogare
Cu strategia noastră actuală, dacă simbolul este greșit sau lipsește, utilizatorul ar trebui să primească un răspuns neautentificat (pe care îl vom implementa în secțiunea următoare). Deci, pentru un punct final de deconectare simplă, vom trimite jetonul și acesta va fi eliminat din baza de date.
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); }
Folosind această strategie, orice simbol pe care îl are utilizatorul va fi invalid și API-ul va refuza accesul (folosind middleware, așa cum se explică în secțiunea următoare). Acest lucru trebuie să fie coordonat cu front-end pentru a evita ca utilizatorul să rămână conectat fără a avea acces la niciun conținut.
Utilizarea middleware-urilor pentru a restricționa accesul
Cu api_token
-ul creat, putem comuta middleware-ul de autentificare în fișierul rute:
Route::middleware('auth:api') ->get('/user', function (Request $request) { return $request->user(); });
Putem accesa utilizatorul curent folosind metoda $request->user()
sau prin fațada 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
Și obținem un rezultat ca acesta:
Acest lucru se datorează faptului că trebuie să edităm metoda curentă unauthenticated
din clasa noastră Handler. Versiunea curentă returnează un JSON numai dacă cererea are antetul Accept: application/json
, așa că haideți să-l schimbăm:
protected function unauthenticated($request, AuthenticationException $exception) { return response()->json(['error' => 'Unauthenticated'], 401); }
Cu acest lucru remediat, putem reveni la punctele finale ale articolului pentru a le include în middleware-ul auth:api
. Putem face asta folosind grupuri de rute:
Route::group(['middleware' => 'auth:api'], function() { Route::get('articles', 'ArticleController@index'); Route::get('articles/{article}', 'ArticleController@show'); Route::post('articles', 'ArticleController@store'); Route::put('articles/{article}', 'ArticleController@update'); Route::delete('articles/{article}', 'ArticleController@delete'); });
În acest fel, nu trebuie să setăm middleware-ul pentru fiecare dintre rute. Nu economisește mult timp acum, dar pe măsură ce proiectul crește, ajută la menținerea rutelor USCATE.
Testarea punctelor noastre finale
Laravel include integrarea cu PHPUnit din cutie cu un phpunit.xml
deja configurat. Cadrul ne oferă, de asemenea, câțiva ajutoare și afirmații suplimentare care ne ușurează viața, în special pentru testarea API-urilor.
Există o serie de instrumente externe pe care le puteți folosi pentru a vă testa API-ul; cu toate acestea, testarea în interiorul Laravel este o alternativă mult mai bună - putem avea toate beneficiile testării unei structuri API și a rezultatelor, păstrând în același timp controlul deplin al bazei de date. Pentru punctul final al listei, de exemplu, am putea rula câteva fabrici și să afirmăm că răspunsul conține acele resurse.
Pentru a începe, va trebui să modificăm câteva setări pentru a utiliza o bază de date SQLite în memorie. Folosind asta, testele noastre vor rula rapid, dar compromisul este că unele comenzi de migrare (constrângeri, de exemplu) nu vor funcționa corect în acea configurație specială. Vă sfătuiesc să vă îndepărtați de SQLite la testare atunci când începeți să primiți erori de migrare sau dacă preferați un set mai puternic de teste în loc de rulări performante.
De asemenea, vom rula migrațiile înainte de fiecare test. Această configurare ne va permite să construim baza de date pentru fiecare test și apoi să o distrugem, evitând orice tip de dependență între teste.
În fișierul nostru config/database.php
, va trebui să setăm câmpul database
de date din configurația sqlite
la :memory:
:
... 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], ... ]
Apoi activați SQLite în phpunit.xml
adăugând variabila de mediu 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>
Cu asta în afara drumului, tot ce mai rămâne este configurarea clasei noastre de bază TestCase
pentru a folosi migrațiile și a genera baza de date înainte de fiecare test. Pentru a face acest lucru, trebuie să adăugăm trăsătura DatabaseMigrations
și apoi să adăugăm un apel Artisan
pe metoda noastră setUp()
. Iată clasa după modificări:
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'); } }
Un ultim lucru pe care îmi place să-l fac este să adaug comanda de testare la composer.json
:
"scripts": { "test" : [ "vendor/bin/phpunit" ], ... },
Comanda de testare va fi disponibilă astfel:
$ composer test
Înființarea de fabrici pentru testele noastre
Fabricile ne vor permite să creăm rapid obiecte cu datele potrivite pentru testare. Sunt localizate în folderul baza de database/factories
. Laravel iese din cutie cu o fabrică pentru clasa User
, așa că haideți să adăugăm una pentru clasa Article
:
$factory->define(App\Article::class, function (Faker\Generator $faker) { return [ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]; });
Biblioteca Faker este deja injectată pentru a ne ajuta să creăm formatul corect de date aleatorii pentru modelele noastre.
Primele noastre teste
Putem folosi metodele de afirmare ale lui Laravel pentru a atinge cu ușurință un punct final și a evalua răspunsul acestuia. Să creăm primul nostru test, testul de conectare, folosind următoarea comandă:
$ php artisan make:test Feature/LoginTest
Și iată testul nostru:
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' => '[email protected]', 'password' => bcrypt('toptal123'), ]); $payload = ['email' => '[email protected]', 'password' => 'toptal123']; $this->json('POST', 'api/login', $payload) ->assertStatus(200) ->assertJsonStructure([ 'data' => [ 'id', 'name', 'email', 'created_at', 'updated_at', 'api_token', ], ]); } }
Aceste metode testează câteva cazuri simple. Metoda json()
atinge punctul final, iar celelalte afirmații sunt destul de explicite. Un detaliu despre assertJson()
: această metodă convertește răspunsul într-o matrice căutând argumentul, deci ordinea este importantă. Puteți înlănțui mai multe assertJson()
în acest caz.
Acum, să creăm testul punctului final al registrului și să scriem un cuplu pentru acel punct final:
$ php artisan make:test RegisterTest
class RegisterTest extends TestCase { public function testsRegistersSuccessfully() { $payload = [ 'name' => 'John', 'email' => '[email protected]', 'password' => 'toptal123', 'password_confirmation' => 'toptal123', ]; $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' => '[email protected]', 'password' => 'toptal123', ]; $this->json('post', '/api/register', $payload) ->assertStatus(422) ->assertJson([ 'password' => ['The password confirmation does not match.'], ]); } }
Și, în sfârșit, punctul final de deconectare:
$ php artisan make:test LogoutTest
class LogoutTest extends TestCase { public function testUserIsLoggedOutProperly() { $user = factory(User::class)->create(['email' => '[email protected]']); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $this->json('get', '/api/articles', [], $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' => '[email protected]']); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; // Simulating logout $user->api_token = null; $user->save(); $this->json('get', '/api/articles', [], $headers)->assertStatus(401); } }
Este important de menționat că, în timpul testării, aplicația Laravel nu este instanțiată din nou la o nouă solicitare. Ceea ce înseamnă că atunci când atingem middleware-ul de autentificare, acesta salvează utilizatorul curent în instanța
TokenGuard
pentru a evita lovirea din nou a bazei de date. O alegere înțeleaptă, totuși — în acest caz, înseamnă că trebuie să împărțim testul de deconectare în două, pentru a evita orice probleme cu utilizatorul memorat anterior în cache.
Testarea punctelor finale ale articolului este, de asemenea, simplă:
class ArticleTest extends TestCase { public function testsArticlesAreCreatedCorrectly() { $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $payload = [ 'title' => 'Lorem', 'body' => 'Ipsum', ]; $this->json('POST', '/api/articles', $payload, $headers) ->assertStatus(200) ->assertJson(['id' => 1, 'title' => 'Lorem', 'body' => 'Ipsum']); } public function testsArticlesAreUpdatedCorrectly() { $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $article = factory(Article::class)->create([ 'title' => 'First Article', 'body' => 'First Body', ]); $payload = [ 'title' => 'Lorem', 'body' => 'Ipsum', ]; $response = $this->json('PUT', '/api/articles/' . $article->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"]; $article = factory(Article::class)->create([ 'title' => 'First Article', 'body' => 'First Body', ]); $this->json('DELETE', '/api/articles/' . $article->id, [], $headers) ->assertStatus(204); } public function testArticlesAreListedCorrectly() { factory(Article::class)->create([ 'title' => 'First Article', 'body' => 'First Body' ]); factory(Article::class)->create([ 'title' => 'Second Article', 'body' => 'Second Body' ]); $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $response = $this->json('GET', '/api/articles', [], $headers) ->assertStatus(200) ->assertJson([ [ 'title' => 'First Article', 'body' => 'First Body' ], [ 'title' => 'Second Article', 'body' => 'Second Body' ] ]) ->assertJsonStructure([ '*' => ['id', 'body', 'title', 'created_at', 'updated_at'], ]); } }
Pasii urmatori
Cam despre asta e. Cu siguranță există loc de îmbunătățire — puteți implementa OAuth2 cu pachetul Passport, puteți integra un strat de paginare și transformare (recomand Fractal), lista poate continua — dar am vrut să parcurg elementele de bază ale creării și testării unui API în Laravel fără pachete externe.
Dezvoltarea Laravel mi-a îmbunătățit cu siguranță experiența cu PHP, iar ușurința de a testa cu acesta mi-a consolidat interesul pentru cadru. Nu este perfect, dar este suficient de flexibil pentru a vă permite să rezolvați problemele sale.
Dacă proiectați un API public, consultați cele 5 reguli de aur pentru un design API Web excelent.