Tutorial da API Laravel: Como construir e testar uma API RESTful

Publicados: 2022-03-11

Com a ascensão do desenvolvimento móvel e das estruturas JavaScript, usar uma API RESTful é a melhor opção para criar uma interface única entre seus dados e seu cliente.

Laravel é um framework PHP desenvolvido com a produtividade do desenvolvedor PHP em mente. Escrito e mantido por Taylor Otwell, o framework é muito opinativo e se esforça para economizar tempo do desenvolvedor, favorecendo a convenção sobre a configuração. A estrutura também visa evoluir com a web e já incorporou vários novos recursos e ideias no mundo do desenvolvimento web, como filas de tarefas, autenticação de API pronta para uso, comunicação em tempo real e muito mais.

Tutorial da API Laravel - Construindo um serviço Web RESTful

Neste tutorial, exploraremos as maneiras de criar e testar uma API robusta usando Laravel com autenticação. Estaremos usando o Laravel 5.4, e todo o código está disponível para referência no GitHub.

APIs RESTful

Primeiro, precisamos entender o que exatamente é considerado uma API RESTful. REST significa REpresentational State Transfer e é um estilo de arquitetura para comunicação de rede entre aplicativos, que depende de um protocolo sem estado (geralmente HTTP) para interação.

Verbos HTTP representam ações

Nas APIs RESTful, usamos os verbos HTTP como ações e os endpoints são os recursos sobre os quais atuamos. Usaremos os verbos HTTP para seu significado semântico:

  • GET : recupera recursos
  • POST : criar recursos
  • PUT : atualiza recursos
  • DELETE : exclui recursos

Verbos HTTP: GET, POST, PUT e DELETE são ações em APIs RESTful

Ação de atualização: PUT vs. POST

APIs RESTful são uma questão de muito debate e há muitas opiniões por aí sobre se é melhor atualizar com POST , PATCH ou PUT , ou se a ação de criação é melhor deixar para o verbo PUT . Neste artigo, usaremos PUT para a ação de atualização, pois de acordo com o HTTP RFC, PUT significa criar/atualizar um recurso em um local específico. Outro requisito para o verbo PUT é a idempotência, que neste caso basicamente significa que você pode enviar essa requisição 1, 2 ou 1000 vezes e o resultado será o mesmo: um recurso atualizado no banco de dados.

Recursos

Os recursos serão os alvos das ações, no nosso caso Artigos e Usuários, e eles têm seus próprios endpoints:

  • /articles
  • /users

Neste tutorial da API laravel, os recursos terão uma representação 1:1 em nossos modelos de dados, mas isso não é um requisito. Você pode ter recursos representados em mais de um modelo de dados (ou não representados no banco de dados) e modelos completamente fora dos limites para o usuário. No final, você decide como arquitetar recursos e modelos de maneira adequada ao seu aplicativo.

Uma nota sobre consistência

A maior vantagem de usar um conjunto de convenções como REST é que sua API será muito mais fácil de consumir e desenvolver. Alguns endpoints são bastante diretos e, como resultado, sua API será muito mais fácil de usar e manter em vez de ter endpoints como GET /get_article?id_article=12 e POST /delete_article?number=40 . Eu construí APIs terríveis como essa no passado e ainda me odeio por isso.

No entanto, haverá casos em que será difícil mapear para um esquema Criar/Recuperar/Atualizar/Excluir. Lembre-se de que as URLs não devem conter verbos e que os recursos não são necessariamente linhas em uma tabela. Outra coisa a ter em mente é que você não precisa implementar todas as ações para todos os recursos.

Configurando um projeto de serviço Web Laravel

Como em todos os frameworks PHP modernos, precisaremos do Composer para instalar e lidar com nossas dependências. Depois de seguir as instruções de download (e adicionar à variável de ambiente do seu caminho), instale o Laravel usando o comando:

 $ composer global require laravel/installer

Após a conclusão da instalação, você pode criar um novo aplicativo como este:

 $ laravel new myapp

Para o comando acima, você precisa ter ~/composer/vendor/bin em seu $PATH . Se você não quiser lidar com isso, você também pode criar um novo projeto usando o Composer:

 $ composer create-project --prefer-dist laravel/laravel myapp

Com o Laravel instalado, você deve conseguir iniciar o servidor e testar se tudo está funcionando:

 $ php artisan serve Laravel development server started: <http://127.0.0.1:8000> 

Ao abrir o localhost:8000 no seu navegador, você deverá ver a página de amostra do Laravel

Ao abrir localhost:8000 em seu navegador, você deverá ver esta página de amostra.

Migrações e modelos

Antes de realmente escrever sua primeira migração, certifique-se de ter um banco de dados criado para este aplicativo e adicione suas credenciais ao arquivo .env localizado na raiz do projeto.

 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret

Você também pode usar Homestead, uma caixa Vagrant especialmente criada para Laravel, mas isso está um pouco fora do escopo deste artigo. Se quiser saber mais, consulte a documentação do Homestead.

Vamos começar com nosso primeiro modelo e migração — o artigo. O artigo deve ter um título e um campo de corpo, além de uma data de criação. O Laravel fornece vários comandos através do Artisan—ferramenta de linha de comando do Laravel—que nos ajudam gerando arquivos e colocando-os nas pastas corretas. Para criar o modelo Article, podemos executar:

 $ php artisan make:model Article -m

A opção -m é a abreviação de --migration e diz ao Artisan para criar um para o nosso modelo. Aqui está a migração gerada:

 <?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'); } }

Vamos dissecar isso por um segundo:

  • Os métodos up() e down() serão executados quando migrarmos e revertermos, respectivamente;
  • $table->increments('id') configura um inteiro de incremento automático com o nome id ;
  • $table->timestamps() irá configurar os timestamps para nós— created_at e updated_at , mas não se preocupe em definir um padrão, o Laravel se encarrega de atualizar esses campos quando necessário.
  • E, finalmente, Schema::dropIfExists() irá, é claro, descartar a tabela se ela existir.

Com isso fora do caminho, vamos adicionar duas linhas ao nosso método up() :

 public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('body'); $table->timestamps(); }); }

O método string() cria uma coluna equivalente a VARCHAR enquanto text() cria um equivalente TEXT . Feito isso, vamos em frente e migrar:

 $ php artisan migrate

Você também pode usar a opção --step aqui, e ela separará cada migração em seu próprio lote para que você possa revertê-las individualmente, se necessário.

O Laravel pronto para uso vem com duas migrações, create_users_table e create_password_resets_table . Não usaremos a tabela password_resets , mas ter a tabela users pronta para nós será útil.

Agora vamos voltar ao nosso modelo e adicionar esses atributos ao campo $fillable para que possamos usá-los em nossos modelos Article::create e Article::update :

 class Article extends Model { protected $fillable = ['title', 'body']; }

Os campos dentro da propriedade $fillable podem ser atribuídos em massa usando os métodos create() e update() do Eloquent. Você também pode usar a propriedade $guarded , para permitir todas, exceto algumas propriedades.

Semeadura do banco de dados

Semeadura de banco de dados é o processo de preencher nosso banco de dados com dados fictícios que podemos usar para testá-lo. O Laravel vem com o Faker, uma ótima biblioteca para gerar o formato correto de dados fictícios para nós. Então vamos criar nosso primeiro seeder:

 $ php artisan make:seeder ArticlesTableSeeder

Os seeders estarão localizados no diretório /database/seeds . Veja como fica depois de configurá-lo para criar alguns artigos:

 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, ]); } } }

Então, vamos executar o comando seed:

 $ php artisan db:seed --class=ArticlesTableSeeder

Vamos repetir o processo para criar um semeador de usuários:

 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 facilitar adicionando nossos seeders à classe principal DatabaseSeeder dentro da pasta database/seeds :

 class DatabaseSeeder extends Seeder { public function run() { $this->call(ArticlesTableSeeder::class); $this->call(UsersTableSeeder::class); } }

Dessa forma, podemos simplesmente executar $ php artisan db:seed e ele executará todas as classes chamadas no método run() .

Rotas e Controladores

Vamos criar os endpoints básicos para nosso aplicativo: criar, recuperar a lista, recuperar um único, atualizar e excluir. No arquivo routes/api.php , podemos simplesmente fazer isso:

 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; })

As rotas dentro api.php serão prefixadas com /api/ e o middleware de limitação de API será aplicado automaticamente a essas rotas (se você quiser remover o prefixo, você pode editar a classe RouteServiceProvider em /app/Providers/RouteServiceProvider.php ).

Agora vamos mover este código para seu próprio Controller:

 $ 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; } }

O arquivo 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 melhorar os endpoints usando a vinculação de modelo de rota implícita. Dessa forma, o Laravel injetará a instância do Article em nossos métodos e retornará automaticamente um 404 caso não seja encontrado. Teremos que fazer alterações no arquivo de rotas e no 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); } }

Uma nota sobre códigos de status HTTP e o formato de resposta

Também adicionamos a chamada response()->json() aos nossos endpoints. Isso nos permite retornar dados JSON explicitamente, bem como enviar um código HTTP que pode ser analisado pelo cliente. Os códigos mais comuns que você retornará serão:

  • 200 : OK. O código de sucesso padrão e a opção padrão.
  • 201 : Objeto criado. Útil para as ações da store .
  • 204 : Sem conteúdo. Quando uma ação foi executada com sucesso, mas não há conteúdo para retornar.
  • 206 : Conteúdo parcial. Útil quando você precisa retornar uma lista paginada de recursos.
  • 400 : Pedido incorreto. A opção padrão para solicitações que não passam na validação.
  • 401 : Não autorizado. O usuário precisa ser autenticado.
  • 403 : Proibido. O usuário é autenticado, mas não tem permissão para executar uma ação.
  • 404 : Não encontrado. Isso será retornado automaticamente pelo Laravel quando o recurso não for encontrado.
  • 500 : Erro interno do servidor. Idealmente, você não retornará isso explicitamente, mas se algo inesperado ocorrer, é isso que seu usuário receberá.
  • 503 : Serviço indisponível. Bastante autoexplicativo, mas também outro código que não será retornado explicitamente pelo aplicativo.

Enviando uma resposta 404 correta

Se você tentou buscar um recurso inexistente, você receberá uma exceção e receberá todo o stacktrace, assim:

Stacktrace NotFoundHttpException

Podemos corrigir isso editando nossa classe de manipulador de exceção, localizada em app/Exceptions/Handler.php , para retornar uma resposta 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); }

Veja um exemplo do retorno:

 { data: "Resource not found" }

Se você estiver usando o Laravel para servir outras páginas, precisará editar o código para trabalhar com o cabeçalho Accept , caso contrário, erros 404 de solicitações regulares também retornarão um 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); }

Nesse caso, as solicitações da API precisarão do cabeçalho Accept: application/json .

Autenticação

Existem muitas maneiras de implementar a autenticação de API no Laravel (uma delas sendo o Passport, uma ótima maneira de implementar o OAuth2), mas neste artigo, adotaremos uma abordagem bem simplificada.

Para começar, precisaremos adicionar um campo api_token à tabela users :

 $ php artisan make:migration --table=users adds_api_token_to_users_table

E então implemente a migração:

 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']); }); }

Depois disso, basta executar a migração usando:

 $ php artisan migrate

Criando o endpoint de registro

Usaremos o RegisterController (na pasta Auth ) para retornar a resposta correta no momento do registro. O Laravel vem com autenticação pronta para uso, mas ainda precisamos ajustá-lo um pouco para retornar a resposta que queremos.

Se as APIs estivessem em inglês, é assim que uma conversa de autenticação de API soaria

O controlador faz uso da característica RegistersUsers para implementar o registro. Veja 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()); }

Só precisamos implementar o método registered() em nosso RegisterController . O método recebe o $request e o $user , então isso é tudo o que queremos. Veja como o método deve ficar dentro do controlador:

 protected function registered(Request $request, $user) { $user->generateToken(); return response()->json(['data' => $user->toArray()], 201); }

E podemos vinculá-lo no arquivo de rotas:

 Route::post('register', 'Auth\RegisterController@register');

Na seção acima, usamos um método no modelo User para gerar o token. Isso é útil para que tenhamos apenas uma única maneira de gerar os tokens. Adicione o seguinte método ao seu modelo de usuário:

 class User extends Authenticatable { ... public function generateToken() { $this->api_token = str_random(60); $this->save(); return $this->api_token; } }

E é isso. O usuário já está cadastrado e graças à validação do Laravel e à autenticação pronta para uso, os campos name , email , password e password_confirmation são obrigatórios, e o feedback é tratado automaticamente. Confira o método validator() dentro do RegisterController para ver como as regras são implementadas.

Aqui está o que obtemos quando atingimos esse ponto 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" } }

Criando um endpoint de login

Assim como o endpoint de registro, podemos editar o LoginController (na pasta Auth ) para dar suporte à nossa autenticação de API. O método de login do atributo AuthenticatesUsers pode ser substituído para oferecer suporte à nossa 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); }

E podemos vinculá-lo no arquivo de rotas:

 Route::post('login', 'Auth\LoginController@login');

Agora, supondo que os seeders foram executados, aqui está o que obtemos quando enviamos uma solicitação POST para essa rota:

 $ 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 o token em uma solicitação, você pode fazer isso enviando um atributo api_token no payload ou como token de portador nos cabeçalhos da solicitação na forma de Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw .

Sair

Com nossa estratégia atual, se o token estiver errado ou ausente, o usuário deverá receber uma resposta não autenticada (que implementaremos na próxima seção). Portanto, para um endpoint de logout simples, enviaremos o token e ele será removido do banco de dados.

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); }

Usando essa estratégia, qualquer token que o usuário tenha será inválido e a API negará o acesso (usando middlewares, conforme explicado na próxima seção). Isso precisa ser coordenado com o front-end para evitar que o usuário permaneça logado sem ter acesso a nenhum conteúdo.

Usando middlewares para restringir o acesso

Com o api_token criado, podemos alternar o middleware de autenticação no arquivo de rotas:

 Route::middleware('auth:api') ->get('/user', function (Request $request) { return $request->user(); });

Podemos acessar o usuário atual usando o método $request->user() ou através da 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

E obtemos um resultado assim:

Um Stacktrace InvalidArgumentException

Isso ocorre porque precisamos editar o método unauthenticated atual em nossa classe Handler. A versão atual retorna um JSON apenas se a solicitação tiver o cabeçalho Accept: application/json , então vamos alterá-lo:

 protected function unauthenticated($request, AuthenticationException $exception) { return response()->json(['error' => 'Unauthenticated'], 401); }

Com isso corrigido, podemos voltar aos endpoints do artigo para envolvê-los no middleware auth:api . Podemos fazer isso usando grupos de rotas:

 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'); });

Dessa forma, não precisamos definir o middleware para cada uma das rotas. Não economiza muito tempo agora, mas à medida que o projeto cresce, ajuda a manter as rotas DRY.

Testando nossos endpoints

Laravel inclui integração com PHPUnit pronta para uso com um phpunit.xml já configurado. O framework também nos fornece vários helpers e assertions extras que facilitam muito nossa vida, principalmente para testar APIs.

Existem várias ferramentas externas que você pode usar para testar sua API; no entanto, testar dentro do Laravel é uma alternativa muito melhor - podemos ter todos os benefícios de testar uma estrutura e resultados de API, mantendo o controle total do banco de dados. Para o endpoint da lista, por exemplo, poderíamos executar algumas fábricas e afirmar que a resposta contém esses recursos.

Para começar, precisaremos ajustar algumas configurações para usar um banco de dados SQLite na memória. Usar isso fará com que nossos testes sejam executados rapidamente, mas a desvantagem é que alguns comandos de migração (restrições, por exemplo) não funcionarão corretamente nessa configuração específica. Aconselho a se afastar do SQLite nos testes quando você começar a receber erros de migração ou se preferir um conjunto mais forte de testes em vez de execuções de alto desempenho.

Também executaremos as migrações antes de cada teste. Essa configuração nos permitirá construir o banco de dados para cada teste e depois destruí-lo, evitando qualquer tipo de dependência entre os testes.

Em nosso config/database.php , precisaremos configurar o campo database na configuração do sqlite para :memory: :

 ... 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], ... ]

Em seguida, habilite o SQLite no phpunit.xml adicionando a variável de ambiente 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>

Com isso fora do caminho, tudo o que resta é configurar nossa classe base TestCase para usar migrações e propagar o banco de dados antes de cada teste. Para fazer isso, precisamos adicionar o atributo DatabaseMigrations e, em seguida, adicionar uma chamada Artisan em nosso método setUp() . Aqui está a classe após as alterações:

 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'); } }

Uma última coisa que gosto de fazer é adicionar o comando test ao composer.json :

 "scripts": { "test" : [ "vendor/bin/phpunit" ], ... },

O comando de teste estará disponível assim:

 $ composer test

Configurando Fábricas para Nossos Testes

As fábricas nos permitirão criar rapidamente objetos com os dados certos para teste. Eles estão localizados na pasta database/factories . O Laravel já vem com uma fábrica para a classe User , então vamos adicionar uma para a classe Article :

 $factory->define(App\Article::class, function (Faker\Generator $faker) { return [ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]; });

A biblioteca Faker já está injetada para nos ajudar a criar o formato correto de dados aleatórios para nossos modelos.

Nossos primeiros testes

Podemos usar os métodos assert do Laravel para atingir facilmente um endpoint e avaliar sua resposta. Vamos criar nosso primeiro teste, o teste de login, usando o seguinte comando:

 $ php artisan make:test Feature/LoginTest

E aqui está o nosso teste:

 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', ], ]); } }

Esses métodos testam alguns casos simples. O método json() atinge o endpoint e os outros asserts são bastante autoexplicativos. Um detalhe sobre assertJson() : este método converte a resposta em um array procura pelo argumento, então a ordem é importante. Você pode encadear várias chamadas assertJson() nesse caso.

Agora, vamos criar o teste de endpoint de registro e escrever um par para esse endpoint:

 $ 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.'], ]); } }

E por último, o endpoint de logout:

 $ 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); } }

É importante observar que, durante o teste, o aplicativo Laravel não é instanciado novamente em uma nova solicitação. O que significa que quando atingimos o middleware de autenticação, ele salva o usuário atual dentro da instância do TokenGuard para evitar atingir o banco de dados novamente. Uma escolha sábia, no entanto - neste caso, significa que temos que dividir o teste de logout em dois, para evitar problemas com o usuário armazenado em cache anteriormente.

Testar os endpoints do artigo também é simples:

 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 passos

Isso é tudo o que há para isso. Definitivamente, há espaço para melhorias - você pode implementar o OAuth2 com o pacote Passport, integrar uma camada de paginação e transformação (eu recomendo Fractal), a lista continua - mas eu queria passar pelo básico de criar e testar uma API em Laravel sem pacotes externos.

O desenvolvimento do Laravel certamente melhorou minha experiência com PHP e a facilidade de testar com ele solidificou meu interesse pelo framework. Não é perfeito, mas é flexível o suficiente para permitir que você resolva seus problemas.

Se você estiver projetando uma API pública, confira 5 regras de ouro para um ótimo design de API da Web.

Relacionado: Autenticação completa do usuário e controle de acesso – Um tutorial do Laravel Passport, Pt. 1