Laravel APIチュートリアル:RESTfulAPIを構築してテストする方法

公開: 2022-03-11

モバイル開発とJavaScriptフレームワークの台頭により、RESTful APIを使用することは、データとクライアントの間に単一のインターフェースを構築するための最良のオプションです。

Laravelは、PHP開発者の生産性を念頭に置いて開発されたPHPフレームワークです。 Taylor Otwellによって作成および保守されているフレームワークは、非常に意見が分かれており、設定より規約を優先することで開発者の時間を節約するよう努めています。 このフレームワークは、Webとともに進化することも目的としており、ジョブキュー、すぐに使用できるAPI認証、リアルタイム通信など、Web開発の世界にいくつかの新しい機能やアイデアがすでに組み込まれています。

LaravelAPIチュートリアル-RESTfulWebサービスの構築

このチュートリアルでは、認証付きのLaravelを使用して堅牢なAPIを構築およびテストする方法について説明します。 Laravel 5.4を使用します。すべてのコードは、GitHubで参照できます。

RESTful API

まず、RESTfulAPIと正確に見なされるものを理解する必要があります。 RESTはREpresentationalStateTransferの略で、アプリケーション間のネットワーク通信のアーキテクチャスタイルであり、相互作用をステートレスプロトコル(通常はHTTP)に依存します。

HTTP動詞はアクションを表します

RESTful APIでは、アクションとしてHTTP動詞を使用し、エンドポイントは作用を受けるリソースです。 意味的な意味のためにHTTP動詞を使用します。

  • GET :リソースを取得する
  • POST :リソースを作成する
  • PUT :リソースを更新する
  • DELETE :リソースを削除します

HTTP動詞:GET、POST、PUT、およびDELETEはRESTfulAPIのアクションです

更新アクション:PUTとPOST

RESTful APIは多くの議論の対象であり、 POSTPATCH 、またはPUTで更新するのが最適かどうか、または作成アクションをPUT動詞に任せるのが最適かどうかについては多くの意見があります。 この記事では、更新アクションにPUTを使用します。これは、HTTP RFCによると、 PUTは特定の場所でリソースを作成/更新することを意味します。 PUT動詞のもう1つの要件はべき等です。これは、基本的に、その要求を1、2、または1000回送信でき、結果は同じになることを意味します。つまり、データベース内の1つの更新されたリソースです。

資力

リソース(この場合はArticlesとUsers)がアクションのターゲットになり、それらには独自のエンドポイントがあります。

  • /articles
  • /users

このlaravelapiチュートリアルでは、リソースはデータモデル上で1:1の表現になりますが、これは必須ではありません。 リソースを複数のデータモデルで表現したり(またはデータベースでまったく表現しなかったり)、ユーザーのモデルを完全に立ち入り禁止にすることができます。 最終的には、アプリケーションに適した方法でリソースとモデルを設計する方法を決定できます。

一貫性に関する注記

RESTなどの一連の規則を使用する最大の利点は、APIの使用と開発がはるかに簡単になることです。 一部のエンドポイントは非常に単純であり、その結果、 GET /get_article?id_article=12POST /delete_article?number=40などのエンドポイントを使用する場合とは対照的に、APIの使用と保守がはるかに簡単になります。 私は過去にそのようなひどいAPIを構築しましたが、それでも自分自身を嫌っています。

ただし、作成/取得/更新/削除スキーマへのマッピングが難しい場合があります。 URLには動詞を含めるべきではなく、リソースは必ずしもテーブルの行である必要はないことに注意してください。 覚えておくべきもう1つのことは、すべてのリソースに対してすべてのアクションを実装する必要がないということです。

LaravelWebサービスプロジェクトの設定

最新のすべてのPHPフレームワークと同様に、依存関係をインストールして処理するには、Composerが必要です。 ダウンロード手順に従った後(そしてパス環境変数に追加した後)、次のコマンドを使用してLaravelをインストールします。

 $ composer global require laravel/installer

インストールが完了したら、次のような新しいアプリケーションをスキャフォールディングできます。

 $ laravel new myapp

上記のコマンドでは、 $PATH~/composer/vendor/binが含まれている必要があります。 これに対処したくない場合は、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を開くと、Laravelのサンプルページが表示されます。

ブラウザでlocalhost:8000を開くと、このサンプルページが表示されます。

移行とモデル

最初の移行を実際に作成する前に、このアプリ用にデータベースが作成されていることを確認し、プロジェクトのルートにある.envファイルにその資格情報を追加してください。

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

Laravel用に特別に作成されたVagrantボックスであるHomesteadを使用することもできますが、それはこの記事の範囲から少し外れています。 詳細については、Homesteadのドキュメントを参照してください。

最初のモデルと移行であるArticleから始めましょう。 記事には、タイトルと本文フィールド、および作成日が必要です。 Laravelは、Artisan(Laravelのコマンドラインツール)を介していくつかのコマンドを提供します。これらのコマンドは、ファイルを生成して正しいフォルダーに配置するのに役立ちます。 Articleモデルを作成するには、次のコマンドを実行します。

 $ 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_atupdated_atのタイムスタンプを設定しますが、デフォルトの設定について心配する必要はありません。Laravelは、必要に応じてこれらのフィールドを更新します。
  • そして最後に、 Schema::dropIfExists()は、もちろん、テーブルが存在する場合はそれを削除します。

それが邪魔にならないように、 up()メソッドに2行を追加しましょう。

 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_tablecreate_password_resets_tableの2つの移行が付属しています。 password_resetsテーブルは使用しませんが、 usersテーブルを用意しておくと役に立ちます。

次に、モデルに戻り、これらの属性を$fillableフィールドに追加して、 Article::createモデルとArticle::updateモデルで使用できるようにします。

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

$fillableプロパティ内のフィールドは、Eloquentのcreate() )メソッドと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

このプロセスを繰り返して、ユーザーシーダーを作成しましょう。

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

database/seedsフォルダー内のメインのDatabaseSeederクラスにシーダーを追加することで簡単にできます。

 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スロットリングミドルウェアがこれらのルートに自動的に適用されます(プレフィックスを削除する場合は、/ app / Providers / /app/Providers/RouteServiceProvider.phpRouteServiceProviderクラスを編集できます)。

次に、このコードを独自のコントローラーに移動しましょう。

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

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応答を送信する

存在しないリソースをフェッチしようとすると、例外がスローされ、次のようにスタックトレース全体を受け取ります。

NotFoundHttpExceptionスタックトレース

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が必要です。

認証

LaravelでAPI認証を実装する方法はたくさんありますが(そのうちの1つはPassportで、OAuth2を実装するための優れた方法です)、この記事では、非常に単純化されたアプローチを採用します。

開始するには、 usersテーブルにapi_tokenフィールドを追加する必要があります。

 $ 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

レジスタエンドポイントの作成

Authフォルダー内の) RegisterControllerを使用して、登録時に正しい応答を返します。 Laravelにはすぐに使用できる認証が付属していますが、必要な応答を返すために少し調整する必要があります。

APIが英語の場合、これはAPI認証の会話がどのように聞こえるかです。

コントローラは、トレイト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()); }

RegisterControllerregistered()メソッドを実装する必要があります。 このメソッドは$request$userを受け取るので、本当に必要なのはそれだけです。 コントローラ内でのメソッドの外観は次のとおりです。

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

そして、それをルートファイルにリンクすることができます:

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

上記のセクションでは、Userモデルのメソッドを使用してトークンを生成しました。 これは、トークンを生成する方法が1つしかない場合に便利です。 次のメソッドをユーザーモデルに追加します。

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

以上です。 これでユーザーが登録され、Laravelの検証とすぐに使用できる認証のおかげで、 nameemailpassword 、およびpassword_confirmationフィールドが必要になり、フィードバックが自動的に処理されます。 RegisterController内のvalidator()メソッドをチェックアウトして、ルールがどのように実装されているかを確認します。

そのエンドポイントに到達したときに得られるものは次のとおりです。

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

ログインエンドポイントの作成

登録エンドポイントと同様に、( Authフォルダー内の) LoginControllerを編集して、API認証をサポートできます。 AuthenticatesUsersトレイトのlogin方法は、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を作成したら、routesファイルの認証ミドルウェアを切り替えることができます。

 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

そして、次のような結果が得られます。

InvalidArgumentExceptionスタックトレース

これは、Handlerクラスの現在のunauthenticatedれていないメソッドを編集する必要があるためです。 現在のバージョンは、リクエストにAccept: application/jsonヘッダーがある場合にのみ、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.xmlがすでに設定されているPHPUnitとの統合が含まれています。 このフレームワークは、特にAPIのテストのために、私たちの生活をはるかに楽にするいくつかのヘルパーと追加のアサーションも提供します。

APIのテストに使用できる外部ツールは多数あります。 ただし、Laravel内でのテストは、はるかに優れた代替手段です。データベースの完全な制御を維持しながら、API構造と結果をテストすることのすべての利点を得ることができます。 たとえば、リストエンドポイントの場合、いくつかのファクトリを実行し、応答にそれらのリソースが含まれていることを表明できます。

開始するには、インメモリSQLiteデータベースを使用するためにいくつかの設定を微調整する必要があります。 これを使用すると、テストが非常に高速に実行されますが、トレードオフとして、一部の移行コマンド(制約など)がその特定のセットアップで正しく機能しないことがあります。 移行エラーが発生し始めたとき、またはパフォーマンスの高い実行ではなく、より強力なテストセットが必要な場合は、テストでSQLiteから離れることをお勧めします。

また、各テストの前に移行を実行します。 この設定により、テストごとにデータベースを構築してから破棄できるため、テスト間の依存関係を回避できます。

config/database.phpファイルで、 sqlite構成のdatabaseフィールドを:memory:に設定する必要があります。

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

次に、環境変数DB_CONNECTIONを追加して、 phpunit.xmlでSQLiteを有効にします。

 <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トレイトを追加してから、 setUp()メソッドにArtisan呼び出しを追加する必要があります。 変更後のクラスは次のとおりです。

 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フォルダーにあります。 LaravelはUserクラス用のファクトリが付属しているので、 Articleクラス用にファクトリを追加しましょう。

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

モデルのランダムデータの正しい形式を作成するために、Fakerライブラリがすでに挿入されています。

私たちの最初のテスト

Laravelのassertメソッドを使用して、エンドポイントに簡単に到達し、その応答を評価できます。 次のコマンドを使用して、最初のテストであるログインテストを作成しましょう。

 $ 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()に関する1つの詳細:このメソッドは、応答を配列に変換して引数を検索するため、順序が重要です。 その場合、複数の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インスタンス内に保存され、データベースに再度アクセスするのを防ぐことができます。 ただし、賢明な選択です。この場合、以前にキャッシュされたユーザーの問題を回避するために、ログアウトテストを2つに分割する必要があります。

Articleエンドポイントのテストも簡単です。

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

次のステップ

これですべてです。 間違いなく改善の余地があります。PassportパッケージでOAuth2を実装し、ページ付けと変換レイヤーを統合できます(フラクタルをお勧めします)。リストは続きますが、LaravelでAPIを作成してテストするための基本を学びたかったのです。外部パッケージ。

Laravelの開発は確かに私のPHPの経験を改善し、それを使ったテストの容易さはフレームワークへの私の興味を固めました。 完璧ではありませんが、問題を回避するのに十分な柔軟性があります。

パブリックAPIを設計している場合は、優れたWebAPI設計のための5つのゴールデンルールを確認してください。

関連:完全なユーザー認証とアクセス制御– Laravel Passportチュートリアル、Pt。 1