Tutorial Laravel API: Cara Membuat dan Menguji API RESTful
Diterbitkan: 2022-03-11Dengan munculnya pengembangan seluler dan kerangka kerja JavaScript, menggunakan RESTful API adalah opsi terbaik untuk membangun satu antarmuka antara data Anda dan klien Anda.
Laravel adalah kerangka kerja PHP yang dikembangkan dengan mempertimbangkan produktivitas pengembang PHP. Ditulis dan dikelola oleh Taylor Otwell, kerangka kerja ini sangat berpendirian dan berusaha untuk menghemat waktu pengembang dengan lebih menyukai konvensi daripada konfigurasi. Kerangka kerja ini juga bertujuan untuk berkembang dengan web dan telah memasukkan beberapa fitur dan ide baru di dunia pengembangan web—seperti antrian pekerjaan, otentikasi API di luar kotak, komunikasi waktu nyata, dan banyak lagi.
Dalam tutorial ini, kita akan menjelajahi cara Anda dapat membangun—dan menguji—API yang kuat menggunakan Laravel dengan autentikasi. Kami akan menggunakan Laravel 5.4, dan semua kode tersedia untuk referensi di GitHub.
API RESTful
Pertama, kita perlu memahami apa sebenarnya yang dianggap sebagai RESTful API. REST adalah singkatan dari REpresentational State Transfer dan merupakan gaya arsitektur untuk komunikasi jaringan antar aplikasi, yang bergantung pada protokol stateless (biasanya HTTP) untuk interaksi.
Kata Kerja HTTP Mewakili Tindakan
Dalam RESTful API, kami menggunakan kata kerja HTTP sebagai tindakan, dan titik akhir adalah sumber daya yang ditindaklanjuti. Kami akan menggunakan kata kerja HTTP untuk arti semantiknya:
-
GET
: ambil sumber daya -
POST
: buat sumber daya -
PUT
: perbarui sumber daya -
DELETE
: hapus sumber daya
Perbarui Tindakan: PUT vs. POST
API RESTful adalah masalah yang diperdebatkan dan ada banyak pendapat di luar sana tentang apakah yang terbaik untuk memperbarui dengan POST
, PATCH
, atau PUT
, atau jika tindakan buat sebaiknya diserahkan kepada kata kerja PUT
. Pada artikel ini kita akan menggunakan PUT
untuk tindakan pembaruan, karena menurut RFC HTTP, PUT
berarti membuat/memperbarui sumber daya di lokasi tertentu. Persyaratan lain untuk kata kerja PUT
adalah idempotence, yang dalam hal ini pada dasarnya berarti Anda dapat mengirim permintaan itu 1, 2 atau 1000 kali dan hasilnya akan sama: satu sumber daya yang diperbarui dalam database.
Sumber daya
Sumber daya akan menjadi target tindakan, dalam kasus kami Artikel dan Pengguna, dan mereka memiliki titik akhir sendiri:
-
/articles
-
/users
Dalam tutorial api laravel ini, sumber daya akan memiliki representasi 1:1 pada model data kami, tetapi itu bukan keharusan. Anda dapat memiliki sumber daya yang diwakili dalam lebih dari satu model data (atau tidak diwakili sama sekali dalam database) dan model yang sepenuhnya terlarang bagi pengguna. Pada akhirnya, Anda dapat memutuskan bagaimana merancang sumber daya dan model dengan cara yang sesuai dengan aplikasi Anda.
Catatan tentang Konsistensi
Keuntungan terbesar menggunakan seperangkat konvensi seperti REST adalah bahwa API Anda akan lebih mudah digunakan dan dikembangkan. Beberapa titik akhir cukup mudah dan, sebagai hasilnya, API Anda akan jauh lebih mudah digunakan dan dipelihara daripada memiliki titik akhir seperti GET /get_article?id_article=12
dan POST /delete_article?number=40
. Saya telah membangun API yang mengerikan seperti itu di masa lalu dan saya masih membenci diri saya sendiri karenanya.
Namun, akan ada kasus di mana akan sulit untuk memetakan ke skema Buat/Ambil/Perbarui/Hapus. Ingat bahwa URL tidak boleh mengandung kata kerja dan sumber daya tidak harus berupa baris dalam tabel. Hal lain yang perlu diingat adalah Anda tidak harus mengimplementasikan setiap tindakan untuk setiap sumber daya.
Menyiapkan Proyek Layanan Web Laravel
Seperti semua framework PHP modern, kita memerlukan Composer untuk menginstal dan menangani dependensi kita. Setelah Anda mengikuti petunjuk pengunduhan (dan menambahkan ke variabel lingkungan jalur Anda), instal Laravel menggunakan perintah:
$ composer global require laravel/installer
Setelah instalasi selesai, Anda dapat membuat scaffolding aplikasi baru seperti ini:
$ laravel new myapp
Untuk perintah di atas, Anda harus memiliki ~/composer/vendor/bin
di $PATH
Anda. Jika Anda tidak ingin berurusan dengan itu, Anda juga dapat membuat proyek baru menggunakan Komposer:
$ composer create-project --prefer-dist laravel/laravel myapp
Dengan Laravel terinstal, Anda seharusnya dapat memulai server dan menguji apakah semuanya berfungsi:
$ php artisan serve Laravel development server started: <http://127.0.0.1:8000>
localhost:8000
di browser Anda, Anda akan melihat halaman contoh ini.Migrasi dan Model
Sebelum benar-benar menulis migrasi pertama Anda, pastikan Anda memiliki database yang dibuat untuk aplikasi ini dan tambahkan kredensialnya ke file .env
yang terletak di root proyek.
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret
Anda juga dapat menggunakan Homestead, kotak Vagrant yang dibuat khusus untuk Laravel, tetapi itu sedikit di luar cakupan artikel ini. Jika Anda ingin tahu lebih banyak, lihat dokumentasi Homestead.
Mari kita mulai dengan model dan migrasi pertama kita—Artikel. Artikel harus memiliki judul dan bidang isi, serta tanggal pembuatan. Laravel menyediakan beberapa perintah melalui Artisan—alat baris perintah Laravel—yang membantu kita dengan membuat file dan meletakkannya di folder yang benar. Untuk membuat model Article, kita dapat menjalankan:
$ php artisan make:model Article -m
Opsi -m
adalah kependekan dari --migration
dan opsi ini memberi tahu Artisan untuk membuatnya untuk model kita. Inilah migrasi yang dihasilkan:
<?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'); } }
Mari kita membedah ini sebentar:
- Metode
up()
dandown()
akan dijalankan saat kita melakukan migrasi dan rollback masing-masing; -
$table->increments('id')
menyiapkan bilangan bulat yang bertambah otomatis dengan namaid
; -
$table->timestamps()
akan menyiapkan stempel waktu untuk kita—created_at
danupdated_at
, tetapi jangan khawatir tentang pengaturan default, Laravel menangani pembaruan bidang ini saat diperlukan. - Dan akhirnya,
Schema::dropIfExists()
tentu saja akan menjatuhkan tabel jika ada.
Dengan menyingkir, mari tambahkan dua baris ke metode up()
kita:
public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('body'); $table->timestamps(); }); }
Metode string()
membuat kolom ekuivalen VARCHAR
sementara text()
membuat ekuivalen TEXT
. Setelah itu selesai, mari kita lanjutkan dan migrasi:
$ php artisan migrate
Anda juga dapat menggunakan opsi
--step
di sini, dan ini akan memisahkan setiap migrasi ke dalam kumpulannya sendiri sehingga Anda dapat memutarnya kembali satu per satu jika diperlukan.
Laravel out of the box hadir dengan dua migrasi, create_users_table
dan create_password_resets_table
. Kami tidak akan menggunakan tabel password_resets
, tetapi menyiapkan tabel users
untuk kami akan sangat membantu.
Sekarang mari kembali ke model kita dan tambahkan atribut tersebut ke bidang $fillable
sehingga kita dapat menggunakannya di Article::create
dan Article::update
model:
class Article extends Model { protected $fillable = ['title', 'body']; }
Bidang di dalam properti
$fillable
dapat ditetapkan secara massal menggunakan metodecreate()
danupdate()
Eloquent. Anda juga dapat menggunakan properti$guarded
, untuk mengizinkan semua kecuali beberapa properti.
Pembibitan Basis Data
Penyemaian basis data adalah proses mengisi basis data kita dengan data dummy yang dapat kita gunakan untuk mengujinya. Laravel hadir dengan Faker, perpustakaan hebat untuk menghasilkan format data dummy yang benar untuk kita. Jadi mari kita buat seeder pertama kita:
$ php artisan make:seeder ArticlesTableSeeder
Seeder akan ditempatkan di direktori /database/seeds
. Berikut tampilannya setelah kita mengaturnya untuk membuat beberapa artikel:
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, ]); } } }
Jadi mari kita jalankan perintah seed:
$ php artisan db:seed --class=ArticlesTableSeeder
Mari kita ulangi proses untuk membuat seeder Pengguna:
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, ]); } } }
Kita dapat membuatnya lebih mudah dengan menambahkan seeder ke kelas DatabaseSeeder
utama di dalam folder database/seeds
:
class DatabaseSeeder extends Seeder { public function run() { $this->call(ArticlesTableSeeder::class); $this->call(UsersTableSeeder::class); } }
Dengan cara ini, kita cukup menjalankan $ php artisan db:seed
dan itu akan menjalankan semua kelas yang dipanggil dalam metode run()
.
Rute dan Pengontrol
Mari buat titik akhir dasar untuk aplikasi kita: buat, ambil daftar, ambil satu, perbarui, dan hapus. Pada file routes/api.php
, kita cukup melakukan ini:
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; })
Rute di dalam api.php
akan diawali dengan /api/
dan middleware pelambatan API akan secara otomatis diterapkan ke rute ini (jika Anda ingin menghapus awalan, Anda dapat mengedit kelas RouteServiceProvider
di /app/Providers/RouteServiceProvider.php
).
Sekarang mari kita pindahkan kode ini ke Controller-nya sendiri:
$ 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; } }
File 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');
Kami dapat meningkatkan titik akhir dengan menggunakan pengikatan model rute implisit. Dengan cara ini, Laravel akan menyuntikkan instance Article
dalam metode kami dan secara otomatis mengembalikan 404 jika tidak ditemukan. Kita harus membuat perubahan pada file route dan controller:
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); } }
Catatan tentang Kode Status HTTP dan Format Respons
Kami juga telah menambahkan panggilan response()->json()
ke titik akhir kami. Ini memungkinkan kami secara eksplisit mengembalikan data JSON serta mengirim kode HTTP yang dapat diuraikan oleh klien. Kode paling umum yang akan Anda kembalikan adalah:
-
200
: Oke. Kode sukses standar dan opsi default. -
201
: Objek dibuat. Berguna untuk tindakanstore
. -
204
: Tidak ada konten. Ketika suatu tindakan berhasil dijalankan, tetapi tidak ada konten untuk dikembalikan. -
206
: Konten sebagian. Berguna ketika Anda harus mengembalikan daftar sumber daya yang diberi halaman. -
400
: Permintaan buruk. Opsi standar untuk permintaan yang gagal lolos validasi. -
401
: Tidak sah. Pengguna perlu diautentikasi. -
403
: Dilarang. Pengguna diautentikasi, tetapi tidak memiliki izin untuk melakukan suatu tindakan. -
404
: Tidak ditemukan. Ini akan dikembalikan secara otomatis oleh Laravel ketika sumber daya tidak ditemukan. -
500
: Kesalahan server internal. Idealnya Anda tidak akan secara eksplisit mengembalikan ini, tetapi jika sesuatu yang tidak terduga rusak, inilah yang akan diterima pengguna Anda. -
503
: Layanan tidak tersedia. Cukup jelas, tetapi juga kode lain yang tidak akan dikembalikan secara eksplisit oleh aplikasi.
Mengirim Respons 404 yang Benar
Jika Anda mencoba mengambil sumber daya yang tidak ada, Anda akan mendapatkan pengecualian dan Anda akan menerima seluruh stacktrace, seperti ini:
Kami dapat memperbaikinya dengan mengedit kelas exception handler kami, yang terletak di app/Exceptions/Handler.php
, untuk mengembalikan respons 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); }
Berikut contoh pengembaliannya:
{ data: "Resource not found" }
Jika Anda menggunakan Laravel untuk menyajikan halaman lain, Anda harus mengedit kode agar berfungsi dengan header Accept
, jika tidak, 404 kesalahan dari permintaan reguler juga akan mengembalikan 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); }
Dalam hal ini, permintaan API akan membutuhkan header Accept: application/json
.
Autentikasi
Ada banyak cara untuk mengimplementasikan Otentikasi API di Laravel (salah satunya adalah Paspor, cara yang bagus untuk mengimplementasikan OAuth2), tetapi dalam artikel ini, kami akan mengambil pendekatan yang sangat disederhanakan.
Untuk memulai, kita perlu menambahkan bidang api_token
ke tabel users
:
$ php artisan make:migration --table=users adds_api_token_to_users_table
Dan kemudian terapkan migrasi:
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']); }); }
Setelah itu, jalankan saja migrasi menggunakan:
$ php artisan migrate
Membuat Titik Akhir Registrasi
Kami akan menggunakan RegisterController
(di folder Auth
) untuk mengembalikan respons yang benar saat pendaftaran. Laravel hadir dengan otentikasi di luar kotak, tetapi kita masih perlu sedikit mengubahnya untuk mengembalikan respons yang kita inginkan.
Pengontrol memanfaatkan sifat RegistersUsers
untuk mengimplementasikan registrasi. Berikut cara kerjanya:
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()); }
Kita hanya perlu mengimplementasikan metode registered()
di RegisterController
kita. Metode menerima $request
dan $user
, jadi hanya itu yang kami inginkan. Begini tampilan metode di dalam pengontrol:

protected function registered(Request $request, $user) { $user->generateToken(); return response()->json(['data' => $user->toArray()], 201); }
Dan kami dapat menautkannya di file rute:
Route::post('register', 'Auth\RegisterController@register');
Pada bagian di atas, kami menggunakan metode pada model Pengguna untuk menghasilkan token. Ini berguna sehingga kami hanya memiliki satu cara untuk menghasilkan token. Tambahkan metode berikut ke model Pengguna Anda:
class User extends Authenticatable { ... public function generateToken() { $this->api_token = str_random(60); $this->save(); return $this->api_token; } }
Dan itu saja. Pengguna sekarang terdaftar dan berkat validasi Laravel dan otentikasi yang siap pakai, bidang name
, email
, password
, dan password_confirmation
sandi_konfirmasi diperlukan, dan umpan balik ditangani secara otomatis. Periksa metode validator()
di dalam RegisterController
untuk melihat bagaimana aturan diterapkan.
Inilah yang kami dapatkan ketika kami mencapai titik akhir itu:
$ 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" } }
Membuat Titik Akhir Masuk
Sama seperti titik akhir pendaftaran, kita dapat mengedit LoginController
(di folder Auth
) untuk mendukung otentikasi API kita. Metode login
dari sifat AuthenticatesUsers
dapat diganti untuk mendukung API kami:
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); }
Dan kami dapat menautkannya di file rute:
Route::post('login', 'Auth\LoginController@login');
Sekarang, dengan asumsi seeder telah dijalankan, inilah yang kami dapatkan ketika kami mengirim permintaan POST
ke rute itu:
$ 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" } }
Untuk mengirimkan token dalam request, Anda dapat melakukannya dengan mengirimkan atribut api_token
di payload atau sebagai bearer token di header request dalam bentuk Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw
.
Keluar
Dengan strategi kami saat ini, jika token salah atau hilang, pengguna harus menerima respons yang tidak diautentikasi (yang akan kami terapkan di bagian selanjutnya). Jadi untuk titik akhir logout sederhana, kami akan mengirimkan token dan itu akan dihapus di database.
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); }
Dengan menggunakan strategi ini, token apa pun yang dimiliki pengguna akan menjadi tidak valid, dan API akan menolak akses (menggunakan middlewares, seperti yang dijelaskan di bagian selanjutnya). Ini perlu dikoordinasikan dengan front-end untuk menghindari pengguna tetap masuk tanpa memiliki akses ke konten apa pun.
Menggunakan Middlewares untuk Membatasi Akses
Dengan api_token
yang dibuat, kita dapat mengaktifkan middleware otentikasi di file rute:
Route::middleware('auth:api') ->get('/user', function (Request $request) { return $request->user(); });
Kita dapat mengakses pengguna saat ini menggunakan metode $request->user()
atau melalui fasad 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
Dan kita mendapatkan hasil seperti ini:
Ini karena kita perlu mengedit metode yang unauthenticated
saat ini di kelas Handler kita. Versi saat ini mengembalikan JSON hanya jika permintaan memiliki header Accept: application/json
, jadi mari kita ubah:
protected function unauthenticated($request, AuthenticationException $exception) { return response()->json(['error' => 'Unauthenticated'], 401); }
Dengan perbaikan itu, kita dapat kembali ke titik akhir artikel untuk membungkusnya di middleware auth:api
. Kita dapat melakukannya dengan menggunakan grup rute:
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'); });
Dengan cara ini kita tidak perlu mengatur middleware untuk setiap rute. Ini tidak menghemat banyak waktu sekarang, tetapi seiring pertumbuhan proyek, ini membantu menjaga rute KERING.
Menguji Titik Akhir Kami
Laravel menyertakan integrasi dengan PHPUnit di luar kotak dengan phpunit.xml
yang sudah disiapkan. Kerangka kerja ini juga memberi kita beberapa pembantu dan pernyataan tambahan yang membuat hidup kita lebih mudah, terutama untuk menguji API.
Ada sejumlah alat eksternal yang dapat Anda gunakan untuk menguji API Anda; namun, pengujian di dalam Laravel adalah alternatif yang jauh lebih baik—kita dapat memperoleh semua manfaat dari pengujian struktur dan hasil API sambil mempertahankan kontrol penuh atas database. Untuk titik akhir daftar, misalnya, kita dapat menjalankan beberapa pabrik dan menegaskan bahwa respons berisi sumber daya tersebut.
Untuk memulai, kita perlu mengubah beberapa pengaturan untuk menggunakan database SQLite dalam memori. Menggunakan itu akan membuat pengujian kami berjalan secepat kilat, tetapi kerugiannya adalah beberapa perintah migrasi (batasan, misalnya) tidak akan berfungsi dengan baik dalam pengaturan tertentu. Saya menyarankan untuk menjauh dari SQLite dalam pengujian ketika Anda mulai mendapatkan kesalahan migrasi atau jika Anda lebih suka serangkaian pengujian yang lebih kuat daripada kinerja yang berjalan.
Kami juga akan menjalankan migrasi sebelum setiap pengujian. Pengaturan ini akan memungkinkan kita untuk membangun database untuk setiap pengujian dan kemudian menghancurkannya, menghindari segala jenis ketergantungan antar pengujian.
Dalam file config/database.php
kita, kita perlu mengatur bidang database
dalam konfigurasi sqlite
ke :memory:
:
... 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], ... ]
Kemudian aktifkan SQLite di phpunit.xml
dengan menambahkan variabel lingkungan 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>
Dengan itu, yang tersisa hanyalah mengonfigurasi kelas TestCase
dasar kami untuk menggunakan migrasi dan menyemai database sebelum setiap pengujian. Untuk melakukannya, kita perlu menambahkan sifat DatabaseMigrations
, dan kemudian menambahkan panggilan Artisan
pada metode setUp()
kita. Inilah kelas setelah perubahan:
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'); } }
Satu hal terakhir yang ingin saya lakukan adalah menambahkan perintah tes ke composer.json
:
"scripts": { "test" : [ "vendor/bin/phpunit" ], ... },
Perintah tes akan tersedia seperti ini:
$ composer test
Menyiapkan Pabrik untuk Pengujian Kami
Pabrik akan memungkinkan kita membuat objek dengan cepat dengan data yang tepat untuk pengujian. Mereka berada di folder database/factories
. Laravel keluar dari kotak dengan pabrik untuk kelas User
, jadi mari tambahkan satu untuk kelas Article
:
$factory->define(App\Article::class, function (Faker\Generator $faker) { return [ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]; });
Pustaka Faker sudah disuntikkan untuk membantu kami membuat format data acak yang benar untuk model kami.
Tes Pertama Kami
Kita dapat menggunakan metode assert Laravel untuk mencapai titik akhir dengan mudah dan mengevaluasi responsnya. Mari kita buat tes pertama kita, tes login, menggunakan perintah berikut:
$ php artisan make:test Feature/LoginTest
Dan inilah ujian kami:
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', ], ]); } }
Metode ini menguji beberapa kasus sederhana. Metode json()
mencapai titik akhir dan pernyataan lainnya cukup jelas. Satu detail tentang assertJson()
: metode ini mengubah respons menjadi pencarian array untuk argumen, jadi urutannya penting. Anda dapat menghubungkan beberapa panggilan assertJson()
dalam kasus itu.
Sekarang, mari buat tes titik akhir register dan tulis pasangan untuk titik akhir itu:
$ 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.'], ]); } }
Dan terakhir, titik akhir 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); } }
Penting untuk dicatat bahwa, selama pengujian, aplikasi Laravel tidak dipakai lagi pada permintaan baru. Artinya, ketika kita menekan middleware otentikasi, itu menyimpan pengguna saat ini di dalam instance
TokenGuard
untuk menghindari memukul database lagi. Namun, pilihan yang bijak—dalam hal ini, itu berarti kita harus membagi tes logout menjadi dua, untuk menghindari masalah apa pun dengan pengguna yang di-cache sebelumnya.
Menguji titik akhir Artikel juga mudah:
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'], ]); } }
Langkah selanjutnya
Itu saja. Pasti ada ruang untuk perbaikan—Anda dapat mengimplementasikan OAuth2 dengan paket Passport, mengintegrasikan pagination dan lapisan transformasi (saya sarankan Fractal), daftarnya terus berlanjut—tetapi saya ingin membahas dasar-dasar pembuatan dan pengujian API di Laravel tanpa paket eksternal.
Pengembangan Laravel tentu saja meningkatkan pengalaman saya dengan PHP dan kemudahan pengujian dengannya telah memperkuat minat saya pada kerangka kerja. Ini tidak sempurna, tetapi cukup fleksibel untuk memungkinkan Anda mengatasi masalahnya.
Jika Anda mendesain API publik, lihat 5 Aturan Emas untuk Desain API Web yang Hebat.