Учебное пособие по Laravel API: как создать и протестировать RESTful API
Опубликовано: 2022-03-11С развитием мобильной разработки и фреймворков JavaScript использование RESTful API является лучшим вариантом для создания единого интерфейса между вашими данными и вашим клиентом.
Laravel — это PHP-фреймворк, разработанный с учетом производительности разработчиков PHP. Написанный и поддерживаемый Тейлором Отвеллом, этот фреймворк очень самоуверен и стремится сэкономить время разработчиков, отдавая предпочтение соглашению, а не конфигурации. Фреймворк также стремится развиваться вместе с Интернетом и уже включает в себя несколько новых функций и идей в мире веб-разработки, таких как очереди заданий, встроенная аутентификация API, связь в реальном времени и многое другое.
В этом руководстве мы рассмотрим способы создания и тестирования надежного API с использованием Laravel с аутентификацией. Мы будем использовать Laravel 5.4, и весь код доступен для ознакомления на GitHub.
RESTful API
Во-первых, нам нужно понять, что именно считается RESTful API. REST расшифровывается как REpresentational State Transfer и представляет собой архитектурный стиль для сетевого взаимодействия между приложениями, который использует для взаимодействия протокол без сохранения состояния (обычно HTTP).
Глаголы HTTP представляют действия
В RESTful API мы используем HTTP-глаголы как действия, а конечные точки — это ресурсы, над которыми действуют. Мы будем использовать глаголы HTTP для их семантического значения:
-
GET
: получить ресурсы -
POST
: создать ресурсы -
PUT
: обновить ресурсы -
DELETE
: удалить ресурсы
Действие обновления: PUT против POST
API-интерфейсы RESTful являются предметом долгих споров, и существует множество мнений о том, лучше ли обновлять с помощью POST
, PATCH
или PUT
, или лучше оставить действие создания для глагола PUT
. В этой статье мы будем использовать PUT
для действия обновления, поскольку, согласно HTTP RFC, PUT
означает создание/обновление ресурса в определенном месте. Другим требованием к глаголу PUT
является идемпотентность, что в данном случае в основном означает, что вы можете отправить этот запрос 1, 2 или 1000 раз, и результат будет одинаковым: один обновленный ресурс в базе данных.
Ресурсы
Целями действий будут ресурсы, в нашем случае статьи и пользователи, и у них есть свои конечные точки:
-
/articles
-
/users
В этом руководстве по laravel API ресурсы будут иметь представление 1:1 в наших моделях данных, но это не является обязательным требованием. У вас могут быть ресурсы, представленные более чем в одной модели данных (или вообще не представленные в базе данных) и модели, полностью недоступные для пользователя. В конце концов, вы решаете, как спроектировать ресурсы и модели таким образом, чтобы они соответствовали вашему приложению.
Примечание о последовательности
Самым большим преимуществом использования набора соглашений, таких как REST, является то, что ваш API будет намного проще использовать и разрабатывать. Некоторые конечные точки довольно просты, и в результате ваш API будет намного проще в использовании и обслуживании по сравнению с такими конечными точками, как GET /get_article?id_article=12
и POST /delete_article?number=40
. В прошлом я создавал такие ужасные API и до сих пор ненавижу себя за это.
Однако будут случаи, когда будет сложно сопоставить схему создания/получения/обновления/удаления. Помните, что URL-адреса не должны содержать глаголов и что ресурсы не обязательно являются строками в таблице. Еще одна вещь, о которой следует помнить, это то, что вам не нужно реализовывать каждое действие для каждого ресурса.
Настройка проекта веб-службы Laravel
Как и во всех современных PHP-фреймворках, нам понадобится Composer для установки и обработки наших зависимостей. После того, как вы выполните инструкции по загрузке (и добавите переменную среды пути), установите Laravel с помощью команды:
$ composer global require laravel/installer
После завершения установки вы можете создать новое приложение следующим образом:
$ laravel new myapp
Для приведенной выше команды вам нужно иметь ~/composer/vendor/bin
в вашем $PATH
. Если вы не хотите иметь с этим дело, вы также можете создать новый проект с помощью Composer:
$ composer create-project --prefer-dist laravel/laravel myapp
Установив Laravel, вы сможете запустить сервер и проверить, все ли работает:
$ php artisan serve Laravel development server started: <http://127.0.0.1:8000>
localhost:8000
в своем браузере, вы должны увидеть этот пример страницы.Миграции и модели
Прежде чем писать свою первую миграцию, убедитесь, что у вас есть база данных, созданная для этого приложения, и добавьте ее учетные данные в файл .env
, расположенный в корне проекта.
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret
Вы также можете использовать Homestead, коробку Vagrant, специально созданную для Laravel, но это немного выходит за рамки этой статьи. Если вы хотите узнать больше, обратитесь к документации Homestead.
Давайте начнем с нашей первой модели и миграции — статьи. Статья должна иметь заголовок и поле тела, а также дату создания. Laravel предоставляет несколько команд через Artisan — инструмент командной строки Laravel — которые помогают нам создавать файлы и помещать их в правильные папки. Чтобы создать модель статьи, мы можем запустить:
$ php artisan make:model Article -m
Параметр -m
является сокращением от --migration
и указывает Artisan создать его для нашей модели. Вот сгенерированная миграция:
<?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'); } }
Давайте разберем это на секунду:
- Методы
up()
иdown()
будут запускаться при миграции и откате соответственно; -
$table->increments('id')
устанавливает автоматически увеличивающееся целое число с именемid
; -
$table->timestamps()
установит для нас временные метки —created_at
иupdated_at
, но не беспокойтесь об установке значения по умолчанию, Laravel позаботится об обновлении этих полей при необходимости. - И, наконец,
Schema::dropIfExists()
, конечно же, удалит таблицу, если она существует.
С этим покончено, давайте добавим две строки в наш метод up()
:
public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('body'); $table->timestamps(); }); }
Метод string()
создает эквивалентный столбец VARCHAR
, а text()
создает эквивалент TEXT
. Сделав это, давайте продолжим миграцию:
$ php artisan migrate
Вы также можете использовать параметр
--step
здесь, и он разделит каждую миграцию на отдельный пакет, чтобы вы могли откатить их по отдельности, если это необходимо.
Laravel из коробки поставляется с двумя миграциями, create_users_table
и create_password_resets_table
. Мы не будем использовать таблицу password_resets
, но будет полезно иметь готовую таблицу users
.
Теперь вернемся к нашей модели и добавим эти атрибуты в поле $fillable
, чтобы мы могли использовать их в наших моделях Article::create
и Article::update
:
class Article extends Model { protected $fillable = ['title', 'body']; }
Поля внутри свойства
$fillable
могут быть массово назначены с помощью методов Eloquentcreate()
иupdate()
. Вы также можете использовать свойство$guarded
, чтобы разрешить все свойства, кроме нескольких.
Заполнение базы данных
Заполнение базы данных — это процесс заполнения нашей базы данных фиктивными данными, которые мы можем использовать для ее тестирования. Laravel поставляется с Faker, отличной библиотекой для создания правильного формата фиктивных данных для нас. Итак, давайте создадим наш первый сидер:
$ php artisan make:seeder ArticlesTableSeeder
Сидеры будут расположены в каталоге /database/seeds
. Вот как это выглядит после того, как мы настроили его для создания нескольких статей:
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, ]); } } }
Итак, давайте запустим команду seed:
$ php artisan db:seed --class=ArticlesTableSeeder
Давайте повторим процесс, чтобы создать сидер 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, ]); } } }
Мы можем упростить задачу, добавив наши сиды в основной класс DatabaseSeeder
внутри папки database/seeds
:
class DatabaseSeeder extends Seeder { public function run() { $this->call(ArticlesTableSeeder::class); $this->call(UsersTableSeeder::class); } }
Таким образом, мы можем просто запустить $ php artisan db:seed
, и он запустит все вызываемые классы в методе run()
.
Маршруты и контроллеры
Давайте создадим основные конечные точки для нашего приложения: создать, получить список, получить один, обновить и удалить. В файле routes/api.php
мы можем просто сделать это:
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; })
Маршруты внутри api.php
будут иметь префикс /api/
, и промежуточное ПО регулирования API будет автоматически применяться к этим маршрутам (если вы хотите удалить префикс, вы можете отредактировать класс RouteServiceProvider
в /app/Providers/RouteServiceProvider.php
).
Теперь давайте переместим этот код в его собственный контроллер:
$ php artisan make:controller 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; } }
Файл 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');
Мы можем улучшить конечные точки, используя неявную привязку модели маршрута. Таким образом, Laravel внедрит экземпляр Article
в наши методы и автоматически вернет 404, если он не будет найден. Нам придется внести изменения в файл маршрутов и в контроллер:
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); } }
Примечание о кодах состояния HTTP и формате ответа
Мы также добавили вызов response()->json()
к нашим конечным точкам. Это позволяет нам явно возвращать данные JSON, а также отправлять код HTTP, который может быть проанализирован клиентом. Наиболее распространенные коды, которые вы будете возвращать, будут следующими:
-
200
: ОК. Стандартный код успеха и опция по умолчанию. -
201
: Объект создан. Полезно для действийstore
. -
204
: Нет контента. Когда действие было выполнено успешно, но нет содержимого для возврата. -
206
: Частичный контент. Полезно, когда вам нужно вернуть список ресурсов с разбивкой на страницы. -
400
: Неверный запрос. Стандартный вариант для запросов, не прошедших проверку. -
401
: Несанкционировано. Пользователь должен пройти аутентификацию. -
403
: Запрещено. Пользователь аутентифицирован, но не имеет разрешений на выполнение действия. -
404
: Не найдено. Это будет автоматически возвращено Laravel, когда ресурс не будет найден. -
500
: Внутренняя ошибка сервера. В идеале вы не собираетесь явно возвращать это, но если что-то неожиданное сломается, это то, что получит ваш пользователь. -
503
: Сервис недоступен. Довольно очевидный, но также и другой код, который не будет явно возвращаться приложением.
Отправка правильного ответа 404
Если вы попытаетесь получить несуществующий ресурс, вам будет выдано исключение, и вы получите всю трассировку стека, например:
Мы можем исправить это, отредактировав наш класс обработчика исключений, расположенный в app/Exceptions/Handler.php
, чтобы он возвращал ответ 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); }
Вот пример возврата:
{ data: "Resource not found" }
Если вы используете Laravel для обслуживания других страниц, вам нужно отредактировать код для работы с заголовком Accept
, иначе ошибки 404 из обычных запросов также вернут 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); }
В этом случае для запросов API потребуется заголовок Accept: application/json
.
Аутентификация
Есть много способов реализовать аутентификацию API в Laravel (один из них — Passport, отличный способ реализовать OAuth2), но в этой статье мы воспользуемся очень упрощенным подходом.
Для начала нам нужно добавить поле api_token
в таблицу users
:
$ php artisan make:migration --table=users adds_api_token_to_users_table
А затем реализовать миграцию:
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']); }); }
После этого просто запустите миграцию, используя:
$ php artisan migrate
Создание конечной точки регистрации
Мы будем использовать RegisterController
(в папке Auth
), чтобы возвращать правильный ответ при регистрации. Laravel поставляется с аутентификацией из коробки, но нам все еще нужно немного настроить ее, чтобы вернуть нужный нам ответ.
Контроллер использует трейт RegistersUsers
для реализации регистрации. Вот как это работает:
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()); }
Нам просто нужно реализовать метод Registered registered()
в нашем RegisterController
. Метод получает $request
и $user
, так что это действительно все, что нам нужно. Вот как метод должен выглядеть внутри контроллера:

protected function registered(Request $request, $user) { $user->generateToken(); return response()->json(['data' => $user->toArray()], 201); }
И мы можем связать его с файлом маршрутов:
Route::post('register', 'Auth\RegisterController@register');
В приведенном выше разделе мы использовали метод модели User для создания токена. Это полезно, так как у нас есть только один способ генерации токенов. Добавьте следующий метод в вашу модель User:
class User extends Authenticatable { ... public function generateToken() { $this->api_token = str_random(60); $this->save(); return $this->api_token; } }
Вот и все. Теперь пользователь зарегистрирован, и благодаря проверке Laravel и стандартной аутентификации поля name
, email
, password
и password_confirmation
являются обязательными, а обратная связь обрабатывается автоматически. Проверьте метод validator()
внутри RegisterController
, чтобы увидеть, как реализуются правила.
Вот что мы получаем, когда достигаем этой конечной точки:
$ 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" } }
Создание конечной точки входа
Как и в случае с конечной точкой регистрации, мы можем отредактировать LoginController
(в папке Auth
) для поддержки аутентификации нашего API. Метод login
в AuthenticatesUsers
можно переопределить для поддержки нашего 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); }
И мы можем связать его с файлом маршрутов:
Route::post('login', 'Auth\LoginController@login');
Теперь, если предположить, что сидеры были запущены, вот что мы получим, когда отправим POST
-запрос на этот маршрут:
$ 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" } }
Чтобы отправить токен в запросе, вы можете сделать это, отправив атрибут api_token
в полезной нагрузке или в качестве токена носителя в заголовках запроса в виде Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw
.
Выход
С нашей текущей стратегией, если токен неверный или отсутствует, пользователь должен получить неаутентифицированный ответ (который мы реализуем в следующем разделе). Таким образом, для простой конечной точки выхода из системы мы отправим токен, и он будет удален из базы данных.
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); }
При использовании этой стратегии любой токен, который есть у пользователя, будет недействительным, и API откажет в доступе (используя промежуточное ПО, как описано в следующем разделе). Это необходимо согласовать с внешним интерфейсом, чтобы пользователь не оставался в системе без доступа к какому-либо содержимому.
Использование ПО промежуточного слоя для ограничения доступа
api_token
, мы можем переключить промежуточное ПО аутентификации в файле маршрутов:
Route::middleware('auth:api') ->get('/user', function (Request $request) { return $request->user(); });
Мы можем получить доступ к текущему пользователю, используя метод $request->user()
или через фасад 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
И получаем такой результат:
Это потому, что нам нужно отредактировать текущий unauthenticated
метод в нашем классе Handler. Текущая версия возвращает JSON, только если запрос имеет заголовок Accept: application/json
, поэтому давайте изменим его:
protected function unauthenticated($request, AuthenticationException $exception) { return response()->json(['error' => 'Unauthenticated'], 401); }
С этим исправлением мы можем вернуться к конечным точкам статьи, чтобы обернуть их промежуточным программным обеспечением auth:api
. Мы можем сделать это, используя группы маршрутов:
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'); });
Таким образом, нам не нужно устанавливать промежуточное ПО для каждого из маршрутов. Сейчас это не экономит много времени, но по мере роста проекта помогает сохранять маршруты СУХИМИ.
Тестирование наших конечных точек
Laravel включает интеграцию с PHPUnit из коробки с уже настроенным phpunit.xml
. Фреймворк также предоставляет нам несколько помощников и дополнительных утверждений, которые значительно облегчают нашу жизнь, особенно при тестировании API.
Существует ряд внешних инструментов, которые вы можете использовать для тестирования вашего API; однако тестирование внутри Laravel — гораздо лучшая альтернатива — мы можем получить все преимущества тестирования структуры и результатов API, сохраняя при этом полный контроль над базой данных. Например, для конечной точки списка мы могли бы запустить пару фабрик и утверждать, что ответ содержит эти ресурсы.
Для начала нам нужно настроить несколько параметров, чтобы использовать базу данных SQLite в памяти. Благодаря этому наши тесты будут работать молниеносно, но компромисс заключается в том, что некоторые команды миграции (например, ограничения) не будут работать должным образом в этой конкретной настройке. Я советую отказаться от SQLite при тестировании, когда вы начинаете получать ошибки миграции или если вы предпочитаете более сильный набор тестов вместо высокопроизводительных запусков.
Мы также будем запускать миграции перед каждым тестом. Эта настройка позволит нам создать базу данных для каждого теста, а затем уничтожить ее, избегая любой зависимости между тестами.
В нашем файле config/database.php
нам нужно настроить поле database
в конфигурации sqlite
на :memory:
:
... 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], ... ]
Затем включите SQLite в phpunit.xml
, добавив переменную среды 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>
После этого все, что осталось, — настроить наш базовый класс TestCase
для использования миграций и заполнения базы данных перед каждым тестом. Для этого нам нужно добавить трейт DatabaseMigrations
, а затем добавить вызов Artisan
в наш setUp()
. Вот класс после изменений:
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'); } }
И последнее, что я хотел бы сделать, это добавить тестовую команду в composer.json
:
"scripts": { "test" : [ "vendor/bin/phpunit" ], ... },
Тестовая команда будет доступна следующим образом:
$ composer test
Настройка фабрик для наших тестов
Фабрики позволят нам быстро создавать объекты с нужными данными для тестирования. Они находятся в папке database/factories
factories. Laravel поставляется с фабрикой для класса User
, поэтому давайте добавим ее для класса Article
:
$factory->define(App\Article::class, function (Faker\Generator $faker) { return [ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]; });
Библиотека Faker уже внедрена, чтобы помочь нам создать правильный формат случайных данных для наших моделей.
Наши первые тесты
Мы можем использовать методы утверждения Laravel, чтобы легко попасть в конечную точку и оценить ее ответ. Давайте создадим наш первый тест, тест входа в систему, используя следующую команду:
$ php artisan make:test Feature/LoginTest
И вот наш тест:
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', ], ]); } }
Эти методы проверяют пару простых случаев. Метод json()
достигает конечной точки, а другие утверждения говорят сами за себя. Одна деталь о assertJson()
: этот метод преобразует ответ в поиск аргумента в массиве, поэтому важен порядок. В этом случае вы можете связать несколько assertJson()
.
Теперь давайте создадим тест конечной точки регистрации и напишем пару для этой конечной точки:
$ 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.'], ]); } }
И, наконец, конечная точка выхода:
$ 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); } }
Важно отметить, что во время тестирования приложение Laravel не создается повторно при новом запросе. Это означает, что когда мы попадаем в промежуточное программное обеспечение аутентификации, оно сохраняет текущего пользователя внутри экземпляра
TokenGuard
, чтобы избежать повторного обращения к базе данных. Однако это разумный выбор — в данном случае это означает, что мы должны разделить тест выхода из системы на две части, чтобы избежать каких-либо проблем с ранее кэшированным пользователем.
Проверка конечных точек статьи также проста:
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'], ]); } }
Следующие шаги
Вот и все. Определенно есть место для улучшения — вы можете реализовать OAuth2 с пакетом Passport, интегрировать слой разбиения на страницы и преобразования (я рекомендую Fractal), список можно продолжить — но я хотел пройтись по основам создания и тестирования API в Laravel без внешние пакеты.
Разработка Laravel, безусловно, улучшила мой опыт работы с PHP, а простота тестирования укрепила мой интерес к фреймворку. Он не идеален, но достаточно гибок, чтобы вы могли решать свои проблемы.
Если вы разрабатываете общедоступный API, ознакомьтесь с 5 золотыми правилами отличного дизайна веб-API.