Laravel API Tutorial: Jak zbudować i przetestować RESTful API
Opublikowany: 2022-03-11Wraz z rozwojem aplikacji mobilnych i frameworków JavaScript, korzystanie z RESTful API jest najlepszą opcją do zbudowania pojedynczego interfejsu między Twoimi danymi a Twoim klientem.
Laravel to framework PHP opracowany z myślą o produktywności programistów PHP. Napisany i utrzymywany przez Taylora Otwella, framework jest bardzo uparty i dąży do zaoszczędzenia czasu programisty poprzez przedkładanie konwencji nad konfigurację. Struktura ma również ewoluować wraz z Internetem i zawiera już kilka nowych funkcji i pomysłów w świecie tworzenia stron internetowych, takich jak kolejki zadań, uwierzytelnianie API po wyjęciu z pudełka, komunikacja w czasie rzeczywistym i wiele innych.
W tym samouczku omówimy sposoby budowania — i testowania — niezawodnego interfejsu API przy użyciu Laravel z uwierzytelnianiem. Będziemy używać Laravel 5.4, a cały kod jest dostępny do wglądu na GitHub.
RESTful API
Po pierwsze, musimy zrozumieć, co dokładnie jest uważane za RESTful API. REST oznacza REpresentational State Transfer i jest stylem architektonicznym komunikacji sieciowej między aplikacjami, która do interakcji opiera się na protokole bezstanowym (zwykle HTTP).
Czasowniki HTTP reprezentują akcje
W interfejsach API RESTful używamy czasowników HTTP jako akcji, a punkty końcowe to zasoby, na których operuje się. Będziemy używać czasowników HTTP ze względu na ich znaczenie semantyczne:
-
GET: pobieranie zasobów -
POST: tworzenie zasobów -
PUT: aktualizacja zasobów -
DELETE: usuń zasoby
Akcja aktualizacji: PUT vs. POST
Interfejsy API RESTful są przedmiotem wielu dyskusji i istnieje wiele opinii na temat tego, czy najlepiej aktualizować za pomocą POST , PATCH lub PUT , lub czy akcję tworzenia najlepiej pozostawić czasownikowi PUT . W tym artykule będziemy używać PUT do akcji aktualizacji, ponieważ zgodnie z HTTP RFC, PUT oznacza tworzenie/aktualizowanie zasobu w określonej lokalizacji. Innym wymaganiem dla czasownika PUT jest idempotencja, co w tym przypadku oznacza, że możesz wysłać żądanie 1, 2 lub 1000 razy, a wynik będzie taki sam: jeden zaktualizowany zasób w bazie danych.
Zasoby
Zasoby będą celami działań, w naszym przypadku Artykuły i Użytkownicy, i mają swoje własne punkty końcowe:
-
/articles -
/users
W tym samouczku dotyczącym interfejsu laravel api zasoby będą miały reprezentację 1:1 w naszych modelach danych, ale nie jest to wymagane. Możesz mieć zasoby reprezentowane w więcej niż jednym modelu danych (lub w ogóle nie reprezentowane w bazie danych) i modele całkowicie niedostępne dla użytkownika. Na koniec możesz zdecydować, jak zaprojektować zasoby i modele w sposób, który pasuje do Twojej aplikacji.
Uwaga na temat spójności
Największą zaletą korzystania z zestawu konwencji, takich jak REST, jest to, że Twój interfejs API będzie znacznie łatwiejszy do wykorzystania i rozwijania. Niektóre punkty końcowe są dość proste i w rezultacie Twój interfejs API będzie znacznie łatwiejszy w użyciu i utrzymaniu, w przeciwieństwie do punktów końcowych, takich jak GET /get_article?id_article=12 i POST /delete_article?number=40 . W przeszłości zbudowałem takie okropne API i wciąż się za to nienawidzę.
Zdarzają się jednak przypadki, w których trudno będzie zmapować do schematu Utwórz/Pobierz/Aktualizuj/Usuń. Pamiętaj, że adresy URL nie powinny zawierać czasowników, a zasoby niekoniecznie muszą być wierszami w tabeli. Inną rzeczą, o której należy pamiętać, jest to, że nie musisz wdrażać każdej akcji dla każdego zasobu.
Konfigurowanie projektu usługi internetowej Laravel
Jak w przypadku wszystkich nowoczesnych frameworków PHP, do zainstalowania i obsługi naszych zależności będziemy potrzebować Composera. Po wykonaniu instrukcji pobierania (i dodaniu do zmiennej środowiskowej ścieżki) zainstaluj Laravel za pomocą polecenia:
$ composer global require laravel/installerPo zakończeniu instalacji możesz utworzyć szkielet nowej aplikacji w następujący sposób:
$ laravel new myapp Dla powyższego polecenia musisz mieć ~/composer/vendor/bin w $PATH . Jeśli nie chcesz się tym zajmować, możesz również stworzyć nowy projekt za pomocą Composera:
$ composer create-project --prefer-dist laravel/laravel myappPo zainstalowaniu Laravela powinieneś być w stanie uruchomić serwer i sprawdzić, czy wszystko działa:
$ php artisan serve Laravel development server started: <http://127.0.0.1:8000> localhost:8000 w przeglądarce powinieneś zobaczyć tę przykładową stronę.Migracje i modele
Zanim zaczniesz pisać swoją pierwszą migrację, upewnij się, że masz bazę danych utworzoną dla tej aplikacji i dodaj jej poświadczenia do pliku .env znajdującego się w katalogu głównym projektu.
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secretMożesz także użyć Homestead, pudełka Włóczęgów stworzonego specjalnie dla Laravela, ale to trochę poza zakresem tego artykułu. Jeśli chcesz dowiedzieć się więcej, zapoznaj się z dokumentacją Homestead.
Zacznijmy od naszego pierwszego modelu i migracji — artykułu. Artykuł powinien mieć tytuł i pole treści, a także datę utworzenia. Laravel udostępnia kilka poleceń za pośrednictwem Artisan — narzędzia wiersza poleceń Laravela — które pomagają nam w generowaniu plików i umieszczaniu ich w odpowiednich folderach. Aby stworzyć model Artykułu, możemy uruchomić:
$ php artisan make:model Article -m Opcja -m jest skrótem od --migration i mówi Artisanowi, aby utworzył go dla naszego modelu. Oto wygenerowana migracja:
<?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'); } }Przeanalizujmy to przez chwilę:
- Metody
up()idown()zostaną uruchomione odpowiednio podczas migracji i wycofywania; -
$table->increments('id')ustawia automatycznie zwiększającą się liczbę całkowitą o nazwieid; -
$table->timestamps()ustawi dla nas znaczniki czasu —created_atiupdated_at, ale nie przejmuj się ustawianiem wartości domyślnych, Laravel zajmuje się aktualizacją tych pól w razie potrzeby. - I na koniec,
Schema::dropIfExists()oczywiście usunie tabelę, jeśli istnieje.
Pomijając to, dodajmy dwie linie do naszej metody up() :
public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('body'); $table->timestamps(); }); } Metoda string() tworzy równoważną kolumnę VARCHAR , podczas gdy text() tworzy odpowiednik TEXT . Po wykonaniu tych czynności przejdźmy dalej i przeprowadźmy migrację:
$ php artisan migrateMożesz również użyć tutaj opcji
--step, która oddzieli każdą migrację do własnej partii, aby w razie potrzeby można było je cofnąć.
Laravel po wyjęciu z pudełka zawiera dwie migracje, create_users_table i create_password_resets_table . Nie będziemy używać tabeli password_resets , ale przygotowanie dla nas tabeli users będzie pomocne.
Wróćmy teraz do naszego modelu i dodajmy te atrybuty do pola $fillable , abyśmy mogli użyć ich w naszych modelach Article::create i Article::update :
class Article extends Model { protected $fillable = ['title', 'body']; }Pola wewnątrz właściwości
$fillablemogą być masowo przypisane za pomocą metodcreate()iupdate()Eloquent. Możesz również użyć właściwości$guarded, aby zezwolić na wszystkie oprócz kilku właściwości.
Rozsiewanie bazy danych
Seeding bazy danych to proces wypełniania naszej bazy fikcyjnymi danymi, które możemy wykorzystać do jej przetestowania. Laravel jest dostarczany z Faker, świetną biblioteką do generowania dla nas tylko poprawnego formatu fikcyjnych danych. Stwórzmy więc nasz pierwszy siewnik:
$ php artisan make:seeder ArticlesTableSeeder Separatory będą znajdować się w katalogu /database/seeds . Oto jak to wygląda po skonfigurowaniu go do stworzenia kilku artykułów:
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, ]); } } }Uruchommy więc polecenie seed:
$ php artisan db:seed --class=ArticlesTableSeederPowtórzmy proces tworzenia seedera użytkowników:
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, ]); } } } Możemy to ułatwić, dodając nasze seedery do głównej klasy DatabaseSeeder w folderze database/seeds :
class DatabaseSeeder extends Seeder { public function run() { $this->call(ArticlesTableSeeder::class); $this->call(UsersTableSeeder::class); } } W ten sposób możemy po prostu uruchomić $ php artisan db:seed i uruchomi on wszystkie wywoływane klasy w metodzie run() .
Trasy i kontrolery
Stwórzmy podstawowe punkty końcowe dla naszej aplikacji: utwórz, pobierz listę, pobierz jedną, zaktualizuj i usuń. W pliku routes/api.php możemy po prostu zrobić to:
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; }) Trasy wewnątrz api.php będą poprzedzone prefiksem /api/ , a oprogramowanie pośredniczące do ograniczania przepustowości interfejsu API zostanie automatycznie zastosowane do tych tras (jeśli chcesz usunąć prefiks, możesz edytować klasę RouteServiceProvider w /app/Providers/RouteServiceProvider.php ).
Teraz przenieśmy ten kod do własnego kontrolera:
$ php artisan make:controller ArticleControllerArtykułController.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; } } Plik 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'); Możemy ulepszyć punkty końcowe, używając niejawnego powiązania modelu trasy. W ten sposób Laravel wstrzyknie instancję Article do naszych metod i automatycznie zwróci błąd 404, jeśli nie zostanie znaleziony. Będziemy musieli wprowadzić zmiany w pliku tras i kontrolerze:
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); } }Uwaga dotycząca kodów stanu HTTP i formatu odpowiedzi
Dodaliśmy również wywołanie response()->json() do naszych punktów końcowych. Dzięki temu możemy jawnie zwrócić dane JSON, a także wysłać kod HTTP, który może zostać przeanalizowany przez klienta. Najczęstsze kody, które będziesz zwracać, to:
-
200: OK. Standardowy kod sukcesu i opcja domyślna. -
201: Utworzono obiekt. Przydatne w działaniachstore. -
204: Brak treści. Kiedy akcja została wykonana pomyślnie, ale nie ma treści do zwrócenia. -
206: Treść częściowa. Przydatne, gdy musisz zwrócić listę zasobów podzieloną na strony. -
400: Zła prośba. Standardowa opcja dla żądań, które nie przejdą weryfikacji. -
401: Nieautoryzowane. Użytkownik musi zostać uwierzytelniony. -
403: Zabronione. Użytkownik jest uwierzytelniony, ale nie ma uprawnień do wykonania akcji. -
404: Nie znaleziono. Zostanie to zwrócone automatycznie przez Laravela, gdy zasób nie zostanie znaleziony. -
500: Wewnętrzny błąd serwera. Najlepiej byłoby, gdybyś nie zwracał tego wprost, ale jeśli coś nieoczekiwanego się zepsuje, to właśnie otrzyma twój użytkownik. -
503: Usługa niedostępna. Dość oczywiste, ale także inny kod, który nie będzie zwracany wprost przez aplikację.
Wysyłanie prawidłowej odpowiedzi 404
Jeśli próbowałeś pobrać nieistniejący zasób, zostaniesz zgłoszony wyjątek i otrzymasz cały stacktrace, w ten sposób:
Możemy to naprawić, edytując naszą klasę obsługi wyjątków, znajdującą się w app/Exceptions/Handler.php , aby zwracała odpowiedź 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); }Oto przykład zwrotu:
{ data: "Resource not found" } Jeśli używasz Laravela do obsługi innych stron, musisz edytować kod, aby działał z nagłówkiem Accept , w przeciwnym razie błędy 404 ze zwykłych żądań zwrócą również 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); } W takim przypadku żądania API będą wymagały nagłówka Accept: application/json .
Uwierzytelnianie
Istnieje wiele sposobów implementacji uwierzytelniania API w Laravel (jednym z nich jest Passport, świetny sposób na implementację OAuth2), ale w tym artykule przyjmiemy bardzo uproszczone podejście.
Aby rozpocząć, musimy dodać pole api_token do tabeli users :
$ php artisan make:migration --table=users adds_api_token_to_users_tableA następnie zaimplementuj migrację:
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']); }); }Następnie po prostu uruchom migrację za pomocą:
$ php artisan migrateTworzenie punktu końcowego rejestru
Wykorzystamy RegisterController (w folderze Auth ), aby zwrócić poprawną odpowiedź po rejestracji. Laravel jest dostarczany z uwierzytelnianiem po wyjęciu z pudełka, ale nadal musimy go nieco poprawić, aby zwrócić żądaną odpowiedź.
Do realizacji rejestracji administrator wykorzystuje cechę RegistersUsers . Oto jak to działa:
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()); } Musimy tylko zaimplementować metodę registered() w naszym RegisterController . Metoda otrzymuje $request i $user , więc to naprawdę wszystko, czego chcemy. Oto jak ta metoda powinna wyglądać w kontrolerze:

protected function registered(Request $request, $user) { $user->generateToken(); return response()->json(['data' => $user->toArray()], 201); }I możemy to połączyć w pliku tras:
Route::post('register', 'Auth\RegisterController@register');W powyższej sekcji użyliśmy metody w modelu User do wygenerowania tokena. Jest to przydatne, ponieważ mamy tylko jeden sposób generowania tokenów. Dodaj następującą metodę do swojego modelu użytkownika:
class User extends Authenticatable { ... public function generateToken() { $this->api_token = str_random(60); $this->save(); return $this->api_token; } } I to wszystko. Użytkownik jest teraz zarejestrowany i dzięki walidacji Laravela i uwierzytelniania out-box wymagane są pola name , email , password i password_confirmation , a informacja zwrotna jest obsługiwana automatycznie. Sprawdź metodę validator() wewnątrz RegisterController , aby zobaczyć, jak zaimplementowane są reguły.
Oto, co otrzymujemy, gdy osiągniemy ten punkt końcowy:
$ 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" } }Tworzenie punktu końcowego logowania
Podobnie jak punkt końcowy rejestracji, możemy edytować LoginController (w folderze Auth ), aby obsługiwał nasze uwierzytelnianie API. Metoda login cechy AuthenticatesUsers może zostać zmieniona, aby obsługiwać nasze 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); }I możemy to połączyć w pliku tras:
Route::post('login', 'Auth\LoginController@login'); Teraz, zakładając, że seedery zostały uruchomione, oto, co otrzymujemy, gdy wysyłamy żądanie POST do tej trasy:
$ 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" } } Aby wysłać token w żądaniu można to zrobić wysyłając atrybut api_token w ładunku lub jako okaziciela w nagłówkach żądania w postaci Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw .
Wylogowuję się
Przy naszej obecnej strategii, jeśli token jest błędny lub go brakuje, użytkownik powinien otrzymać nieuwierzytelnioną odpowiedź (którą zaimplementujemy w następnej sekcji). Tak więc dla prostego punktu końcowego wylogowania wyślemy token i zostanie on usunięty z bazy danych.
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); }Korzystając z tej strategii, dowolny token, który posiada użytkownik, będzie nieprawidłowy, a interfejs API odmówi dostępu (przy użyciu oprogramowania pośredniczącego, jak wyjaśniono w następnej sekcji). Należy to skoordynować z interfejsem, aby uniknąć pozostawania zalogowanym użytkownika bez dostępu do jakiejkolwiek treści.
Używanie oprogramowania pośredniego do ograniczania dostępu
Po utworzeniu api_token możemy przełączać oprogramowanie pośredniczące uwierzytelniania w pliku route:
Route::middleware('auth:api') ->get('/user', function (Request $request) { return $request->user(); }); Możemy uzyskać dostęp do bieżącego użytkownika za pomocą metody $request->user() lub przez fasadę 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 userI otrzymujemy taki wynik:
Dzieje się tak, ponieważ musimy edytować bieżącą unauthenticated metodę w naszej klasie Handler. Obecna wersja zwraca JSON tylko wtedy, gdy żądanie ma nagłówek Accept: application/json , więc zmieńmy go:
protected function unauthenticated($request, AuthenticationException $exception) { return response()->json(['error' => 'Unauthenticated'], 401); } Po naprawieniu możemy wrócić do punktów końcowych artykułów, aby umieścić je w oprogramowaniu pośredniczącym auth:api . Możemy to zrobić za pomocą grup tras:
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'); });W ten sposób nie musimy ustawiać oprogramowania pośredniczącego dla każdej z tras. W tej chwili nie oszczędza to dużo czasu, ale wraz z rozwojem projektu pomaga utrzymać trasy w stanie suchym.
Testowanie naszych punktów końcowych
Laravel zawiera integrację z PHPUnit po wyjęciu z pudełka z już skonfigurowanym phpunit.xml . Framework udostępnia nam również kilka pomocników i dodatkowe asercje, które znacznie ułatwiają nam życie, zwłaszcza w przypadku testowania interfejsów API.
Istnieje wiele zewnętrznych narzędzi, których możesz użyć do przetestowania swojego API; jednak testowanie wewnątrz Laravela jest znacznie lepszą alternatywą — możemy czerpać wszystkie korzyści z testowania struktury API i wyników, zachowując pełną kontrolę nad bazą danych. Na przykład dla punktu końcowego listy moglibyśmy uruchomić kilka fabryk i potwierdzić, że odpowiedź zawiera te zasoby.
Aby rozpocząć, musimy dostosować kilka ustawień, aby korzystać z bazy danych SQLite w pamięci. Użycie tego sprawi, że nasze testy będą działać błyskawicznie, ale kompromis polega na tym, że niektóre polecenia migracji (na przykład ograniczenia) nie będą działać poprawnie w tej konkretnej konfiguracji. Radzę odejść od SQLite podczas testowania, gdy zaczniesz otrzymywać błędy migracji lub jeśli wolisz silniejszy zestaw testów zamiast wydajnych przebiegów.
Przeprowadzimy również migracje przed każdym testem. Taka konfiguracja pozwoli nam zbudować bazę danych dla każdego testu, a następnie ją zniszczyć, unikając wszelkiego rodzaju zależności między testami.
W naszym pliku config/database.php musimy ustawić pole database w konfiguracji sqlite na :memory: :
... 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], ... ] Następnie włącz SQLite w phpunit.xml , dodając zmienną środowiskową 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> Pomijając to, wszystko, co pozostało, to skonfigurowanie naszej podstawowej klasy TestCase do korzystania z migracji i zasiewania bazy danych przed każdym testem. Aby to zrobić, musimy dodać cechę DatabaseMigrations , a następnie dodać wywołanie Artisan w naszej setUp() . Oto klasa po zmianach:
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'); } } Ostatnią rzeczą, którą lubię robić, jest dodanie polecenia test do composer.json :
"scripts": { "test" : [ "vendor/bin/phpunit" ], ... },Polecenie testowe będzie dostępne w następujący sposób:
$ composer testKonfigurowanie fabryk do naszych testów
Fabryki pozwolą nam szybko tworzyć obiekty z odpowiednimi danymi do testów. Znajdują się one w folderze database/factories . Laravel wychodzi z pudełka z fabryką dla klasy User , więc dodajmy jedną dla klasy Article :
$factory->define(App\Article::class, function (Faker\Generator $faker) { return [ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]; });Biblioteka Faker została już wstrzyknięta, aby pomóc nam w stworzeniu prawidłowego formatu losowych danych dla naszych modeli.
Nasze pierwsze testy
Możemy użyć metod asercji Laravela, aby łatwo trafić w punkt końcowy i ocenić jego odpowiedź. Stwórzmy nasz pierwszy test, test logowania, używając następującego polecenia:
$ php artisan make:test Feature/LoginTestA oto nasz 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' => '[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', ], ]); } } Te metody sprawdzają kilka prostych przypadków. Metoda json() trafia do punktu końcowego, a inne potwierdzenia są dość oczywiste. Jeden szczegół dotyczący assertJson() : ta metoda konwertuje odpowiedź na tablicę wyszukującą argument, więc kolejność jest ważna. W takim przypadku można połączyć wiele assertJson() .
Teraz utwórzmy test punktu końcowego rejestru i napiszmy kilka dla tego punktu końcowego:
$ 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 na koniec punkt końcowy wylogowania:
$ 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); } }Należy zauważyć, że podczas testowania aplikacja Laravel nie jest ponownie tworzona przy nowym żądaniu. Oznacza to, że kiedy trafimy na oprogramowanie pośredniczące uwierzytelniania, zapisze ono bieżącego użytkownika wewnątrz instancji
TokenGuard, aby uniknąć ponownego trafienia do bazy danych. Jednak mądry wybór — w tym przypadku oznacza to, że musimy podzielić test wylogowania na dwa, aby uniknąć problemów z wcześniej buforowanym użytkownikiem.
Testowanie punktów końcowych artykułu jest również proste:
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'], ]); } }Następne kroki
To wszystko. Na pewno jest miejsce na ulepszenia — możesz zaimplementować OAuth2 z pakietem Passport, zintegrować warstwę paginacji i transformacji (polecam Fractal), lista jest długa — ale chciałem przejść przez podstawy tworzenia i testowania API w Laravelu bez pakiety zewnętrzne.
Rozwój Laravela z pewnością poprawił moje doświadczenia z PHP, a łatwość testowania za jego pomocą umocniła moje zainteresowanie frameworkiem. Nie jest idealny, ale jest na tyle elastyczny, że pozwala obejść jego problemy.
Jeśli projektujesz publiczny interfejs API, zapoznaj się z 5 złotymi zasadami projektowania doskonałych interfejsów API sieci Web.
