Tutorial de la API de Laravel: Cómo construir y probar una API RESTful
Publicado: 2022-03-11Con el auge del desarrollo móvil y los marcos de JavaScript, el uso de una API RESTful es la mejor opción para crear una interfaz única entre sus datos y su cliente.
Laravel es un marco PHP desarrollado teniendo en cuenta la productividad del desarrollador de PHP. Escrito y mantenido por Taylor Otwell, el marco es muy obstinado y se esfuerza por ahorrar tiempo al desarrollador al favorecer la convención sobre la configuración. El marco también tiene como objetivo evolucionar con la web y ya ha incorporado varias características e ideas nuevas en el mundo del desarrollo web, como colas de trabajo, autenticación API lista para usar, comunicación en tiempo real y mucho más.
En este tutorial, exploraremos las formas en que puede crear y probar una API robusta usando Laravel con autenticación. Usaremos Laravel 5.4 y todo el código está disponible como referencia en GitHub.
API RESTful
Primero, debemos comprender qué se considera exactamente una API RESTful. REST significa transferencia de estado representacional y es un estilo arquitectónico para la comunicación de red entre aplicaciones, que se basa en un protocolo sin estado (generalmente HTTP) para la interacción.
Los verbos HTTP representan acciones
En las API RESTful, usamos los verbos HTTP como acciones y los puntos finales son los recursos sobre los que se actúa. Usaremos los verbos HTTP por su significado semántico:
-
GET
: recuperar recursos -
POST
: crear recursos -
PUT
: actualizar recursos -
DELETE
: eliminar recursos
Acción de actualización: PUT frente a POST
Las API RESTful son un tema de mucho debate y hay muchas opiniones sobre si es mejor actualizar con POST
, PATCH
o PUT
, o si es mejor dejar la acción de creación al verbo PUT
. En este artículo, PUT
para la acción de actualización, ya que según HTTP RFC, PUT
significa crear/actualizar un recurso en una ubicación específica. Otro requisito para el verbo PUT
es la idempotencia, que en este caso básicamente significa que puede enviar esa solicitud 1, 2 o 1000 veces y el resultado será el mismo: un recurso actualizado en la base de datos.
Recursos
Los recursos serán los objetivos de las acciones, en nuestro caso Artículos y Usuarios, y tienen sus propios puntos finales:
-
/articles
-
/users
En este tutorial de API de Laravel, los recursos tendrán una representación 1:1 en nuestros modelos de datos, pero eso no es un requisito. Puede tener recursos representados en más de un modelo de datos (o no representados en absoluto en la base de datos) y modelos completamente fuera del alcance del usuario. Al final, puede decidir cómo diseñar los recursos y modelos de una manera que se ajuste a su aplicación.
Una nota sobre la consistencia
La mayor ventaja de usar un conjunto de convenciones como REST es que su API será mucho más fácil de consumir y desarrollar. Algunos puntos finales son bastante sencillos y, como resultado, su API será mucho más fácil de usar y mantener en lugar de tener puntos finales como GET /get_article?id_article=12
y POST /delete_article?number=40
. Creé terribles API como esa en el pasado y todavía me odio por eso.
Sin embargo, habrá casos en los que será difícil asignar a un esquema Crear/Recuperar/Actualizar/Eliminar. Recuerde que las URL no deben contener verbos y que los recursos no son necesariamente filas en una tabla. Otra cosa a tener en cuenta es que no tiene que implementar todas las acciones para todos los recursos.
Configuración de un proyecto de servicio web de Laravel
Al igual que con todos los marcos PHP modernos, necesitaremos Composer para instalar y manejar nuestras dependencias. Después de seguir las instrucciones de descarga (y agregar a la variable de entorno de su ruta), instale Laravel usando el comando:
$ composer global require laravel/installer
Una vez finalizada la instalación, puede montar una nueva aplicación como esta:
$ laravel new myapp
Para el comando anterior, debe tener ~/composer/vendor/bin
en su $PATH
. Si no quiere lidiar con eso, también puede crear un nuevo proyecto usando Composer:
$ composer create-project --prefer-dist laravel/laravel myapp
Con Laravel instalado, debería poder iniciar el servidor y probar si todo funciona:
$ php artisan serve Laravel development server started: <http://127.0.0.1:8000>
localhost:8000
en su navegador, debería ver esta página de muestra.Migraciones y Modelos
Antes de escribir su primera migración, asegúrese de tener una base de datos creada para esta aplicación y agregue sus credenciales al archivo .env
ubicado en la raíz del proyecto.
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret
También puedes usar Homestead, una caja de Vagrant especialmente diseñada para Laravel, pero eso está un poco fuera del alcance de este artículo. Si desea obtener más información, consulte la documentación de Homestead.
Comencemos con nuestro primer modelo y migración: el artículo. El artículo debe tener un título y un campo de cuerpo, así como una fecha de creación. Laravel proporciona varios comandos a través de Artisan, la herramienta de línea de comandos de Laravel, que nos ayudan a generar archivos y colocarlos en las carpetas correctas. Para crear el modelo Article, podemos ejecutar:
$ php artisan make:model Article -m
La opción -m
es la abreviatura de --migration
y le dice a Artisan que cree uno para nuestro modelo. Aquí está la migración generada:
<?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'); } }
Analicemos esto por un segundo:
- Los métodos
up()
ydown()
se ejecutarán cuando migremos y retrocedamos respectivamente; -
$table->increments('id')
configura un entero de incremento automático con el nombreid
; -
$table->timestamps()
configurará las marcas de tiempo para nosotros—created_at
yupdated_at
, pero no te preocupes por establecer un valor predeterminado, Laravel se encarga de actualizar estos campos cuando sea necesario. - Y finalmente,
Schema::dropIfExists()
, por supuesto, eliminará la tabla si existe.
Con eso fuera del camino, agreguemos dos líneas a nuestro método up()
:
public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('body'); $table->timestamps(); }); }
El método string()
crea una columna equivalente VARCHAR
mientras que text()
crea un equivalente TEXT
. Con eso hecho, sigamos adelante y migremos:
$ php artisan migrate
También puede usar la opción
--step
aquí, y separará cada migración en su propio lote para que pueda revertirlas individualmente si es necesario.
Laravel viene con dos migraciones, create_users_table
y create_password_resets_table
. No usaremos la tabla password_resets
, pero será útil tener la tabla de users
lista para nosotros.
Ahora regresemos a nuestro modelo y agreguemos esos atributos al campo $fillable
para que podamos usarlos en nuestros modelos Article::create
y Article::update
:
class Article extends Model { protected $fillable = ['title', 'body']; }
Los campos dentro de la propiedad
$fillable
se pueden asignar en masa usando los métodoscreate()
yupdate()
de Eloquent. También puede usar la propiedad$guarded
para permitir todas las propiedades excepto algunas.
Siembra de base de datos
La siembra de bases de datos es el proceso de llenar nuestra base de datos con datos ficticios que podemos usar para probarla. Laravel viene con Faker, una gran biblioteca para generar el formato correcto de datos ficticios para nosotros. Así que vamos a crear nuestra primera sembradora:
$ php artisan make:seeder ArticlesTableSeeder
Las semillas se ubicarán en el directorio /database/seeds
. Así es como se ve después de configurarlo para crear algunos artículos:
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, ]); } } }
Así que ejecutemos el comando semilla:
$ php artisan db:seed --class=ArticlesTableSeeder
Repitamos el proceso para crear una sembradora de usuarios:
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, ]); } } }
Podemos hacerlo más fácil agregando nuestras sembradoras a la clase principal DatabaseSeeder
dentro de la carpeta base de database/seeds
:
class DatabaseSeeder extends Seeder { public function run() { $this->call(ArticlesTableSeeder::class); $this->call(UsersTableSeeder::class); } }
De esta forma, podemos simplemente ejecutar $ php artisan db:seed
y ejecutará todas las clases llamadas en el método run()
.
Rutas y Controladores
Vamos a crear los puntos finales básicos para nuestra aplicación: crear, recuperar la lista, recuperar uno solo, actualizar y eliminar. En el archivo routes/api.php
, simplemente podemos hacer esto:
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; })
Las rutas dentro api.php
tendrán el prefijo /api/
y el middleware de aceleración de la API se aplicará automáticamente a estas rutas (si desea eliminar el prefijo, puede editar la clase RouteServiceProvider
en /app/Providers/RouteServiceProvider.php
).
Ahora vamos a mover este código a su propio controlador:
$ 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; } }
El archivo 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');
Podemos mejorar los puntos finales mediante el uso de un enlace de modelo de ruta implícito. De esta forma, Laravel inyectará la instancia del Article
en nuestros métodos y automáticamente devolverá un 404 si no se encuentra. Tendremos que hacer cambios en el archivo de rutas y en el controlador:
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); } }
Una nota sobre los códigos de estado HTTP y el formato de respuesta
También agregamos la llamada response()->json()
a nuestros puntos finales. Esto nos permite devolver explícitamente datos JSON, así como enviar un código HTTP que el cliente puede analizar. Los códigos más comunes que devolverá serán:
-
200
: Bien. El código de éxito estándar y la opción predeterminada. -
201
: Objeto creado. Útil para las acciones de lastore
. -
204
: Sin contenido. Cuando una acción se ejecutó con éxito, pero no hay contenido para devolver. -
206
: Contenido parcial. Útil cuando tiene que devolver una lista paginada de recursos. -
400
: Solicitud incorrecta. La opción estándar para solicitudes que no pasan la validación. -
401
: No autorizado. El usuario necesita estar autenticado. -
403
: Prohibido. El usuario está autenticado, pero no tiene los permisos para realizar una acción. -
404
: No encontrado. Esto será devuelto automáticamente por Laravel cuando no se encuentre el recurso. -
500
: error interno del servidor. Idealmente, no va a devolver esto explícitamente, pero si algo inesperado se rompe, esto es lo que recibirá su usuario. -
503
: Servicio no disponible. Bastante autoexplicativo, pero también otro código que la aplicación no devolverá explícitamente.
Envío de una respuesta 404 correcta
Si intentaste obtener un recurso que no existe, se generará una excepción y recibirás todo el seguimiento de la pila, así:
Podemos solucionar eso editando nuestra clase de controlador de excepciones, ubicada en app/Exceptions/Handler.php
, para devolver una respuesta 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); }
He aquí un ejemplo de la devolución:
{ data: "Resource not found" }
Si está utilizando Laravel para servir otras páginas, debe editar el código para que funcione con el encabezado Accept
; de lo contrario, los errores 404 de las solicitudes regulares también devolverán 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); }
En este caso, las solicitudes de la API necesitarán el encabezado Accept: application/json
.
Autenticación
Hay muchas formas de implementar la autenticación de API en Laravel (una de ellas es Passport, una excelente forma de implementar OAuth2), pero en este artículo adoptaremos un enfoque muy simplificado.
Para comenzar, necesitaremos agregar un campo api_token
a la tabla de users
:
$ php artisan make:migration --table=users adds_api_token_to_users_table
Y luego implementar la migración:
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']); }); }
Después de eso, solo ejecuta la migración usando:
$ php artisan migrate
Crear el punto final de registro
Haremos uso de RegisterController
(en la carpeta Auth
) para devolver la respuesta correcta al registrarse. Laravel viene con autenticación lista para usar, pero aún tenemos que modificarlo un poco para devolver la respuesta que queremos.
El controlador hace uso del rasgo RegistersUsers
para implementar el registro. Así es como funciona:
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()); }
Solo necesitamos implementar el método registered()
en nuestro RegisterController
. El método recibe $request
y $user
, así que eso es todo lo que queremos. Así es como debería verse el método dentro del controlador:

protected function registered(Request $request, $user) { $user->generateToken(); return response()->json(['data' => $user->toArray()], 201); }
Y podemos vincularlo en el archivo de rutas:
Route::post('register', 'Auth\RegisterController@register');
En la sección anterior, usamos un método en el modelo de usuario para generar el token. Esto es útil para que solo tengamos una única forma de generar los tokens. Agregue el siguiente método a su modelo de usuario:
class User extends Authenticatable { ... public function generateToken() { $this->api_token = str_random(60); $this->save(); return $this->api_token; } }
Y eso es. El usuario ahora está registrado y gracias a la validación de Laravel y la autenticación lista para usar, los campos de name
, email
, password
y password_confirmation
son obligatorios, y los comentarios se manejan automáticamente. Consulte el método validator()
dentro de RegisterController
para ver cómo se implementan las reglas.
Esto es lo que obtenemos cuando llegamos a ese punto 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" } }
Creación de un punto final de inicio de sesión
Al igual que el punto final de registro, podemos editar LoginController
(en la carpeta Auth
) para admitir nuestra autenticación API. El método de login
de sesión del rasgo AuthenticatesUsers
se puede anular para admitir nuestra 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); }
Y podemos vincularlo en el archivo de rutas:
Route::post('login', 'Auth\LoginController@login');
Ahora, suponiendo que se hayan ejecutado las sembradoras, esto es lo que obtenemos cuando enviamos una solicitud POST
a esa ruta:
$ 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" } }
Para enviar el token en una solicitud, puede hacerlo enviando un atributo api_token
en el payload o como un token portador en los encabezados de la solicitud en forma de Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw
.
Saliendo de tu cuenta
Con nuestra estrategia actual, si el token es incorrecto o falta, el usuario debería recibir una respuesta no autenticada (que implementaremos en la siguiente sección). Entonces, para un punto final de cierre de sesión simple, enviaremos el token y se eliminará de la base de datos.
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); }
Con esta estrategia, cualquier token que tenga el usuario no será válido y la API negará el acceso (usando middlewares, como se explica en la siguiente sección). Esto debe coordinarse con el front-end para evitar que el usuario permanezca conectado sin tener acceso a ningún contenido.
Uso de middlewares para restringir el acceso
Con el api_token
creado, podemos alternar el middleware de autenticación en el archivo de rutas:
Route::middleware('auth:api') ->get('/user', function (Request $request) { return $request->user(); });
Podemos acceder al usuario actual usando el método $request->user()
o a través de la fachada 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
Y obtenemos un resultado como este:
Esto se debe a que necesitamos editar el método unauthenticated
actual en nuestra clase Handler. La versión actual devuelve un JSON solo si la solicitud tiene el encabezado Accept: application/json
, así que cambiémoslo:
protected function unauthenticated($request, AuthenticationException $exception) { return response()->json(['error' => 'Unauthenticated'], 401); }
Con eso arreglado, podemos volver a los puntos finales del artículo para envolverlos en el middleware auth:api
. Podemos hacerlo usando grupos de rutas:
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'); });
De esta forma no tenemos que configurar el middleware para cada una de las rutas. No ahorra mucho tiempo en este momento, pero a medida que crece el proyecto, ayuda a mantener las rutas SECAS.
Probando nuestros puntos finales
Laravel incluye integración con PHPUnit lista para usar con un phpunit.xml
ya configurado. El marco también nos brinda varios ayudantes y aserciones adicionales que nos facilitan mucho la vida, especialmente para probar las API.
Hay una serie de herramientas externas que puede usar para probar su API; sin embargo, probar dentro de Laravel es una alternativa mucho mejor: podemos tener todos los beneficios de probar la estructura y los resultados de una API mientras retenemos el control total de la base de datos. Para el punto final de la lista, por ejemplo, podríamos ejecutar un par de fábricas y afirmar que la respuesta contiene esos recursos.
Para comenzar, necesitaremos modificar algunas configuraciones para usar una base de datos SQLite en memoria. Usar eso hará que nuestras pruebas se ejecuten a la velocidad del rayo, pero la contrapartida es que algunos comandos de migración (restricciones, por ejemplo) no funcionarán correctamente en esa configuración en particular. Aconsejo alejarse de SQLite en las pruebas cuando comience a recibir errores de migración o si prefiere un conjunto de pruebas más sólido en lugar de ejecuciones de alto rendimiento.
También ejecutaremos las migraciones antes de cada prueba. Esta configuración nos permitirá construir la base de datos para cada prueba y luego destruirla, evitando cualquier tipo de dependencia entre pruebas.
En nuestro archivo config/database.php
, necesitaremos configurar el campo de la base de database
en la configuración de sqlite
para :memory:
:
... 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], ... ]
Luego habilite SQLite en phpunit.xml
agregando la variable de entorno 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>
Con eso fuera del camino, todo lo que queda es configurar nuestra clase TestCase
base para usar migraciones y generar la base de datos antes de cada prueba. Para hacerlo, debemos agregar el rasgo DatabaseMigrations
y luego agregar una llamada Artisan
en nuestro método setUp()
. Aquí está la clase después de los cambios:
use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Support\Facades\Artisan; abstract class TestCase extends BaseTestCase { use CreatesApplication, DatabaseMigrations; public function setUp() { parent::setUp(); Artisan::call('db:seed'); } }
Una última cosa que me gusta hacer es agregar el comando de prueba a composer.json
:
"scripts": { "test" : [ "vendor/bin/phpunit" ], ... },
El comando de prueba estará disponible así:
$ composer test
Configuración de fábricas para nuestras pruebas
Las fábricas nos permitirán crear rápidamente objetos con los datos correctos para la prueba. Están ubicados en la carpeta base de database/factories
. Laravel sale de la caja con una fábrica para la clase User
, así que agreguemos una para la clase Article
:
$factory->define(App\Article::class, function (Faker\Generator $faker) { return [ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]; });
La biblioteca Faker ya está inyectada para ayudarnos a crear el formato correcto de datos aleatorios para nuestros modelos.
Nuestras primeras pruebas
Podemos usar los métodos de afirmación de Laravel para llegar fácilmente a un punto final y evaluar su respuesta. Vamos a crear nuestra primera prueba, la prueba de inicio de sesión, usando el siguiente comando:
$ php artisan make:test Feature/LoginTest
Y aquí está nuestra prueba:
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', ], ]); } }
Estos métodos prueban un par de casos simples. El método json()
llega al punto final y las otras afirmaciones se explican por sí mismas. Un detalle acerca de assertJson()
: este método convierte la respuesta en una matriz que busca el argumento, por lo que el orden es importante. Puede encadenar múltiples llamadas a assertJson()
en ese caso.
Ahora, creemos la prueba de punto final de registro y escribamos un par para ese punto 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.'], ]); } }
Y, por último, el punto final de cierre de sesión:
$ 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); } }
Es importante tener en cuenta que, durante la prueba, la aplicación Laravel no se vuelve a instanciar en una nueva solicitud. Lo que significa que cuando accedemos al middleware de autenticación, guarda al usuario actual dentro de la instancia de
TokenGuard
para evitar volver a acceder a la base de datos. Sin embargo, es una buena elección; en este caso, significa que tenemos que dividir la prueba de cierre de sesión en dos, para evitar cualquier problema con el usuario previamente almacenado en caché.
Probar los puntos finales del artículo también es sencillo:
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'], ]); } }
Próximos pasos
Eso es todo al respecto. Definitivamente hay margen de mejora: puede implementar OAuth2 con el paquete Passport, integrar una capa de paginación y transformación (recomiendo Fractal), la lista continúa, pero quería repasar los conceptos básicos para crear y probar una API en Laravel sin paquetes externos.
El desarrollo de Laravel ciertamente ha mejorado mi experiencia con PHP y la facilidad de probarlo ha solidificado mi interés en el marco. No es perfecto, pero es lo suficientemente flexible como para permitirle solucionar sus problemas.
Si está diseñando una API pública, consulte las 5 reglas de oro para un excelente diseño de API web.