Tutorial API Laravel: Cum să construiți și să testați un API RESTful

Publicat: 2022-03-11

Odată 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.

Tutorial API Laravel - Construirea unui serviciu web RESTful

Î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

Verbele HTTP: GET, POST, PUT și DELETE sunt acțiuni în API-urile RESTful

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> 

Când deschideți localhost:8000 în browser, ar trebui să vedeți pagina de exemplu Laravel

Când deschideți 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() și down() vor fi rulate când migrăm și, respectiv, rollback;
  • $table->increments('id') setează un număr întreg cu incrementare automată cu numele id ;
  • $table->timestamps() va configura marcajele de timp pentru noi— created_at și updated_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 metodele create() și update() 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țiunile store .
  • 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:

NotFoundHttpException Stacktrace

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.

Dacă API-urile ar fi în engleză, așa ar suna o conversație de autentificare API

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:

Un InvalidArgumentException Stacktrace

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.

Înrudit : Autentificare completă a utilizatorului și control al accesului – Un tutorial pentru pașaport Laravel, Pt. 1