Laravel API 튜토리얼: RESTful API를 빌드하고 테스트하는 방법

게시 됨: 2022-03-11

모바일 개발 및 JavaScript 프레임워크의 등장으로 RESTful API를 사용하는 것은 데이터와 클라이언트 사이에 단일 인터페이스를 구축하는 가장 좋은 옵션입니다.

Laravel은 PHP 개발자 생산성을 염두에 두고 개발된 PHP 프레임워크입니다. Taylor Otwell이 작성하고 유지 관리하는 프레임워크는 매우 독단적이며 구성보다 규칙을 선호하여 개발자 시간을 절약하기 위해 노력합니다. 또한 이 프레임워크는 웹과 함께 발전하는 것을 목표로 하고 있으며 이미 작업 대기열, 즉시 사용 가능한 API 인증, 실시간 통신 등과 같은 웹 개발 세계의 여러 새로운 기능과 아이디어를 통합했습니다.

Laravel API 튜토리얼 - RESTful 웹 서비스 구축

이 튜토리얼에서는 인증과 함께 Laravel을 사용하여 강력한 API를 빌드하고 테스트할 수 있는 방법을 탐색합니다. 우리는 Laravel 5.4를 사용할 것이며 모든 코드는 GitHub에서 참조할 수 있습니다.

RESTful API

먼저 RESTful API가 정확히 무엇인지 이해해야 합니다. REST는 REpresentational State Transfer 의 약자이며 상호 작용을 위해 상태 비저장 프로토콜(일반적으로 HTTP)에 의존하는 애플리케이션 간의 네트워크 통신을 위한 아키텍처 스타일입니다.

HTTP 동사는 동작을 나타냅니다

RESTful API에서는 HTTP 동사를 작업으로 사용하고 끝점은 작업을 수행하는 리소스입니다. 의미론적 의미로 HTTP 동사를 사용할 것입니다.

  • GET : 리소스 검색
  • POST : 리소스 생성
  • PUT : 리소스 업데이트
  • DELETE : 리소스 삭제

HTTP 동사: GET, POST, PUT 및 DELETE는 RESTful API의 작업입니다.

업데이트 작업: PUT 대 POST

RESTful API는 많은 논쟁의 대상이며 POST , PATCH 또는 PUT 으로 업데이트하는 것이 가장 좋은지 또는 생성 작업이 PUT 동사에 가장 잘 남아 있는지에 대한 많은 의견이 있습니다. 이 기사에서는 HTTP RFC에 따라 PUT 을 업데이트 작업에 사용할 것입니다. PUT 은 특정 위치에서 리소스를 생성/업데이트하는 것을 의미합니다. PUT 동사에 대한 또 다른 요구 사항은 멱등성입니다. 이 경우 기본적으로 해당 요청을 1, 2 또는 1000번 보낼 수 있으며 결과는 동일합니다. 데이터베이스에서 하나의 업데이트된 리소스입니다.

자원

리소스는 작업의 대상이 됩니다(이 경우 기사 및 사용자). 리소스에는 고유한 엔드포인트가 있습니다.

  • /articles
  • /users

이 laravel API 튜토리얼에서 리소스는 데이터 모델에 대해 1:1 표현을 갖지만 요구 사항은 아닙니다. 둘 이상의 데이터 모델에 리소스가 표시되거나(또는 데이터베이스에 전혀 표시되지 않는) 리소스가 있을 수 있으며 사용자의 제한을 완전히 벗어난 모델을 만들 수 있습니다. 결국 애플리케이션에 적합한 방식으로 리소스와 모델을 설계하는 방법을 결정하게 됩니다.

일관성에 대한 참고 사항

REST와 같은 일련의 규칙을 사용하는 가장 큰 장점은 API를 사용하고 개발하기가 훨씬 더 쉽다는 것입니다. 일부 끝점은 매우 간단하므로 GET /get_article?id_article=12POST /delete_article?number=40 과 같은 끝점이 있는 것과는 반대로 API를 사용하고 유지 관리하기가 훨씬 더 쉽습니다. 나는 과거에 그런 끔찍한 API를 만들었고 여전히 그것을 싫어합니다.

그러나 Create/Retrieve/Update/Delete 스키마에 매핑하기 어려운 경우가 있습니다. URL에는 동사가 포함되어서는 안 되며 리소스가 테이블의 행일 필요는 없다는 점을 기억하십시오. 명심해야 할 또 다른 사항은 모든 리소스에 대해 모든 작업을 구현할 필요가 없다는 것입니다.

Laravel 웹 서비스 프로젝트 설정하기

모든 최신 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를 사용할 수도 있지만 이는 이 기사의 범위를 약간 벗어납니다. 더 알고 싶다면 홈스테드 문서를 참조하십시오.

첫 번째 모델과 마이그레이션인 기사를 시작하겠습니다. 기사에는 작성 날짜와 함께 제목과 본문 필드가 있어야 합니다. 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_atupdated_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_tablecreate_password_resets_table 의 두 가지 마이그레이션과 함께 제공됩니다. 우리는 password_resets 테이블을 사용하지 않을 것이지만 users 테이블을 준비하는 것이 도움이 될 것입니다.

이제 모델로 돌아가 이러한 속성을 $fillable 필드에 추가하여 Article::createArticle::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/RouteServiceProvider.php 에서 RouteServiceProvider 클래스를 편집할 수 있습니다).

이제 이 코드를 자체 컨트롤러로 이동해 보겠습니다.

 $ 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 스택 추적

JSON 응답을 반환하기 위해 app/Exceptions/Handler.php 에 있는 예외 처리기 클래스를 편집하여 이 문제를 해결할 수 있습니다.

 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 인증을 구현하는 방법은 여러 가지가 있지만(그 중 하나는 OAuth2를 구현하는 좋은 방법인 Passport입니다), 이 기사에서는 매우 단순화된 접근 방식을 취할 것입니다.

시작하려면 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

등록 끝점 만들기

RegisterController ( Auth 폴더에 있음)를 사용하여 등록 시 올바른 응답을 반환합니다. Laravel은 기본적으로 인증과 함께 제공되지만 원하는 응답을 반환하려면 여전히 약간의 조정이 필요합니다.

API가 영어로 된 경우 API 인증 대화는 다음과 같이 들릴 것입니다.

컨트롤러는 RegistersUsers 특성을 사용하여 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()); }

RegisterController 에서 registered() 메소드를 구현하기만 하면 됩니다. 메소드는 $request$user 를 수신하므로 이것이 우리가 원하는 전부입니다. 컨트롤러 내부의 메서드는 다음과 같습니다.

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

그리고 route 파일에 링크할 수 있습니다.

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

위의 섹션에서 우리는 토큰을 생성하기 위해 User 모델의 메소드를 사용했습니다. 이것은 토큰을 생성하는 방법이 하나뿐이므로 유용합니다. 사용자 모델에 다음 메소드를 추가하십시오.

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

그리고 그게 다야. 이제 사용자가 등록되었으며 Laravel의 유효성 검사 및 즉시 사용 가능한 인증 덕분에 name , email , passwordpassword_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" } }

로그인 엔드포인트 생성

등록 끝점과 마찬가지로 API 인증을 지원하도록 LoginController ( Auth 폴더에 있음)를 편집할 수 있습니다. 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 파일에 링크할 수 있습니다.

 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

그리고 우리는 다음과 같은 결과를 얻습니다.

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() 에 대한 한 가지 세부 사항: 이 메서드는 응답을 배열로 변환하여 인수를 검색하므로 순서가 중요합니다. 이 경우 여러 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 인스턴스 내부에 저장합니다. 그러나 현명한 선택입니다. 이 경우 이전에 캐시된 사용자와 관련된 문제를 피하기 위해 로그아웃 테스트를 두 개로 분할해야 합니다.

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를 구현하고, 페이지 매김 및 변환 레이어를 통합할 수 있습니다(Fractal 권장). 목록은 계속 진행되지만, 외부 패키지.

Laravel 개발은 PHP에 대한 제 경험을 확실히 향상시켰고 PHP를 사용한 테스트의 용이성은 프레임워크에 대한 저의 관심을 확고히 했습니다. 완벽하지는 않지만 문제를 해결할 수 있을 만큼 충분히 유연합니다.

공개 API를 설계하는 경우 훌륭한 웹 API 설계를 위한 5가지 황금 규칙을 확인하십시오.

관련 항목: 전체 사용자 인증 및 액세스 제어 – A Laravel Passport Tutorial, Pt. 1