Unit test membuktikan satu potongan logika; integration test membuktikan beberapa potongan menempel dengan benar; HTTP test membuktikan lapisan web berperilaku semestinya. Namun semua itu masih menyimak aplikasi dari dalam — lewat panggilan fungsi dan objek yang kita ciptakan sendiri. End-to-end testing (E2e) menguji dari luar: aplikasi dinyalakan utuh — server, router, database, JSON — lalu digerakkan seperti pengguna sungguhan melalui HTTP. Pada Chapter ini kita membangun skrip E2e yang benar-benar menghidupkan web server bawaan PHP, menembaknya dengan curl, menegaskan responsnya, lalu memadamkannya kembali, semuanya otomatis dan berulang.
Posisi E2e dalam Piramida Pengujian #
Semakin ke luar sebuah test menyentuh sistem, semakin nyata hal yang dibuktikannya — dan semakin mahal harganya. Test E2e menjalankan seluruh tumpukan (full stack), sehingga satu skenarionya bisa seratus kali lebih lambat daripada satu unit test. Karena itu posisinya di puncak piramida: jumlahnya sedikit, tetapi setiap satunya memverifikasi perjalanan yang paling dikhawatirkan gagal, misalnya “pengguna memesan lalu pesanannya muncul di daftar”.
| Jenjang | Kecepatan per test | Apa yang dibuktikan | Jumlah wajar |
|---|---|---|---|
| Unit | mikrodetik | satu fungsi/class terisolasi | ratusan hingga ribuan |
| Integration | milidetik | beberapa komponen bekerja bersama, misalnya query database | puluhan hingga ratusan |
| End-to-end | detik | perjalanan utuh pada sistem terpasang | belasan |
Aturan praktisnya: bug yang bisa ditangkap di jenjang lebih dalam harus ditangkap di sana. E2e bukan tempat menegaskan diskon 10% untuk 27 kombinasi harga — itu pekerjaan unit test dengan data provider. E2e menjawab satu pertanyaan besar: “apakah semua bagian yang sudah terbukti benar sendiri-sendiri benar juga ketika disatukan?”
Aplikasi yang Akan Diuji #
Toko Mini adalah aplikasi API kecil lengkap dengan database SQLite: endpoint /api/health untuk cek kesehatan, POST /api/pesanan untuk membuat pesanan, dan GET /api/pesanan untuk melihat daftarnya. Seluruh logika HTTP mengalir lewat superglobal $_SERVER, header(), dan php://input — persis aplikasi PHP sungguhan, tanpa satu pun lapisan yang diejek. Kode berikut adalah public/index.php, front controller yang akan digerakkan skrip E2e kita:
<?php
declare(strict_types=1);
// public/index.php — front controller Toko Mini.
// Dihidupkan web server bawaan PHP: php -S 127.0.0.1:8127 public/index.php
function koneksiDatabase(): PDO
{
$path = getenv('APP_DB') ?: sys_get_temp_dir() . '/toko-mini-e2e.sqlite';
return new PDO('sqlite:' . $path, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
function pasangSkema(PDO $pdo): void
{
$pdo->exec('CREATE TABLE IF NOT EXISTS pesanan (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pelanggan TEXT NOT NULL,
menu TEXT NOT NULL,
totalHarga INTEGER NOT NULL
)');
}
function jsonResponse(int $status, array $data): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
}
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if ($path === '/api/health') {
jsonResponse(200, ['status' => 'ok']);
return true;
}
if ($path === '/api/pesanan') {
$pdo = koneksiDatabase();
pasangSkema($pdo);
if ($method === 'POST') {
$body = json_decode((string) file_get_contents('php://input'), true);
if (!is_array($body)) {
jsonResponse(422, ['error' => 'Body JSON tidak valid']);
return true;
}
foreach (['pelanggan', 'menu'] as $field) {
if (!isset($body[$field]) || !is_string($body[$field]) || $body[$field] === '') {
jsonResponse(422, ['error' => "Field {$field} wajib diisi"]);
return true;
}
}
$stmt = $pdo->prepare(
'INSERT INTO pesanan (pelanggan, menu, totalHarga) VALUES (?, ?, ?)',
);
$stmt->execute([$body['pelanggan'], $body['menu'], 25000]);
jsonResponse(201, ['id' => (int) $pdo->lastInsertId()]);
return true;
}
$daftar = $pdo->query(
'SELECT id, pelanggan, menu, totalHarga FROM pesanan ORDER BY id',
)->fetchAll();
jsonResponse(200, ['data' => $daftar]);
return true;
}
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'Tidak ditemukan';
return true;Ada tiga keputusan yang relevan untuk pengujian. Pertama, lokasi berkas SQLite diambil dari variabel lingkungan APP_DB — skrip E2e dengan bebas mengarahkannya ke direktori sementara tanpa menyentuh kode aplikasi. Kedua, pasangSkema() memakai CREATE TABLE IF NOT EXISTS sehingga berkas database boleh dihapus dan dibuat ulang setiap kali suite berjalan. Ketiga, /api/health sengaja tidak menyentuh database sama sekali — endpoint ini akan kita pakai untuk mendeteksi bahwa server sudah siap, dan ia harus tetap murah meski database sedang bermasalah.
Skrip E2e dengan Server Sungguhan #
Inilah bintang Chapter ini: e2e.php, sebuah skrip mandiri yang menghidupkan public/index.php lewat php -S, menunggunya siap, menjalankan empat skenario HTTP, lalu memadamkannya. Simpan skrip berikut di direktori chapter, lalu jalankan dengan php e2e.php:
<?php
declare(strict_types=1);
// e2e.php — skrip end-to-end: menghidupkan server sungguhan,
// menembaknya lewat HTTP, lalu memadamkannya lagi.
final class Lari
{
public static int $gagal = 0;
}
function http(string $method, string $path, ?string $body = null): array
{
$ch = curl_init('http://127.0.0.1:8127' . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
}
$respons = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$status, $respons === false ? '' : (string) $respons];
}
function cek(string $label, int $harapanStatus, string $harapanBody, array $aktual): void
{
[$status, $body] = $aktual;
if ($status === $harapanStatus && $body === $harapanBody) {
echo "PASS $label -> $status $body\n";
return;
}
Lari::$gagal++;
echo "FAIL $label -> $status $body\n";
}
// 1. Mulai dari database bersih di direktori sementara
$dbPath = sys_get_temp_dir() . '/toko-mini-e2e.sqlite';
@unlink($dbPath);
putenv('APP_DB=' . $dbPath);
// 2. Hidupkan web server bawaan PHP sebagai proses anak
$server = proc_open(
[PHP_BINARY, '-S', '127.0.0.1:8127', 'public/index.php'],
[1 => ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']],
$pipes,
);
// Jaring pengaman: apa pun akhir skrip ini, server ikut dipadamkan
register_shutdown_function(static function () use ($server): void {
if (is_resource($server)) {
proc_terminate($server);
proc_close($server);
}
});
// 3. Tunggu server siap dengan polling — bukan sleep tetap
for ($i = 0; $i < 50; $i++) {
if (http('GET', '/api/health')[0] === 200) {
break;
}
usleep(100000);
}
// 4. Skenario perjalanan pengguna, ujung ke ujung
cek('GET /api/health', 200, '{"status":"ok"}', http('GET', '/api/health'));
cek('POST /api/pesanan', 201, '{"id":1}', http(
'POST',
'/api/pesanan',
'{"pelanggan":"Budi","menu":"Nasi Goreng Spesial"}',
));
cek(
'GET /api/pesanan',
200,
'{"data":[{"id":1,"pelanggan":"Budi","menu":"Nasi Goreng Spesial","totalHarga":25000}]}',
http('GET', '/api/pesanan'),
);
cek('GET /api/tidak-ada', 404, 'Tidak ditemukan', http('GET', '/api/tidak-ada'));
// 5. Padamkan server dan bersihkan berkas sementara
proc_terminate($server);
proc_close($server);
@unlink($dbPath);
if (Lari::$gagal > 0) {
echo Lari::$gagal, " skenario gagal\n";
exit(1);
}
echo "OK (4 skenario e2e lulus)\n";Output yang Dihasilkan:
PASS GET /api/health -> 200 {"status":"ok"}
PASS POST /api/pesanan -> 201 {"id":1}
PASS GET /api/pesanan -> 200 {"data":[{"id":1,"pelanggan":"Budi","menu":"Nasi Goreng Spesial","totalHarga":25000}]}
PASS GET /api/tidak-ada -> 404 Tidak ditemukan
OK (4 skenario e2e lulus)Mari kita bedah skrip ini langkah demi langkah:
- Helper
http(): membungkus ekstensi curl menjadi satu fungsi yang mengembalikan pasangan[$status, $body].CURLOPT_RETURNTRANSFERmembuat body ikut dikembalikan alih-alih langsung dicetak, danCURLINFO_RESPONSE_CODEmembaca status line respons. - Helper
cek(): assertion sederhana — jika status dan body persis sama dengan harapan, cetakPASS; jika tidak, catat keLari::$gagal. Menegaskan body secara utuh (bukan sekadarassertStringContainsString) membuat test peka terhadap field yang hilang atau bertipe berubah. - Langkah 1 — keadaan awal bersih: berkas SQLite lama dihapus dan
APP_DBdiarahkan ke/tmp. Tanpa langkah ini,idpesanan dari run sebelumnya membuat harapan{"id":1}meleset. - Langkah 2 — server sebagai proses anak:
proc_open()menjalankanphp -S 127.0.0.1:8127 public/index.php.PHP_BINARYdipakai agar skrip tetap benar di PHP manapun yang menjalankannya. Output server dibuang ke/dev/nullsupaya log request tidak mencemari hasil test. - Jaring pengaman:
register_shutdown_function()memadamkan server bahkan ketika skrip mati di tengah jalan. Tanpa jaring ini, satuexit()tak terduga meninggalkan proses yatim yang mengunci port. - Langkah 3 — polling kesiapan: loop mencoba
/api/healthhingga 50 kali dengan jeda 100 ms. Server biasanya siap dalam beberapa ratus milidetik; loop berhenti begitu mendapat 200, jadi total durasinya adaptif. - Langkah 4 — empat skenario: cek kesehatan, buat pesanan (201 + id), lihat daftar (200 + data yang baru dibuat), dan rute tak dikenal (404). Urutannya mengikuti cerita pengguna, bukan urutan berkas.
- Langkah 5 — pembersihan: server diterminasi,
proc_close()menunggu prosesnya benar-benar mati, dan berkas SQLite dihapus. Skrip keluar dengan kode 1 jika ada skenario gagal — sinyal yang nanti bisa dibaca pipeline CI.
Membawa Skenario ke Suite PHPUnit #
Skrip di atas sudah otomatis, tetapi ia hidup di luar suite PHPUnit. Pola berikut membawanya ke dalam: server dihidupkan sekali di setUpBeforeClass(), dua test menjalankan skenario, dan tearDownAfterClass() memadamkan semuanya — struktur yang sama dengan test yang memakai database
, hanya “fixture”-nya berupa proses server:
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class PesananE2eTest extends TestCase
{
private static $server;
private static string $dbPath;
public static function setUpBeforeClass(): void
{
self::$dbPath = sys_get_temp_dir() . '/toko-mini-phpunit.sqlite';
@unlink(self::$dbPath);
putenv('APP_DB=' . self::$dbPath);
self::$server = proc_open(
[PHP_BINARY, '-S', '127.0.0.1:8127', __DIR__ . '/../public/index.php'],
[1 => ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']],
$pipes,
);
for ($i = 0; $i < 50; $i++) {
if (self::http('GET', '/api/health')[0] === 200) {
return;
}
usleep(100000);
}
self::fail('Server tidak siap setelah 5 detik');
}
public static function tearDownAfterClass(): void
{
if (is_resource(self::$server)) {
proc_terminate(self::$server);
proc_close(self::$server);
}
@unlink(self::$dbPath);
}
public function testPesanLaluLihatDaftar(): void
{
[$status, $body] = self::http(
'POST',
'/api/pesanan',
'{"pelanggan":"Siti","menu":"Es Teh Manis"}',
);
$this->assertSame(201, $status);
$this->assertSame(['id' => 1], json_decode($body, true));
[$status, $body] = self::http('GET', '/api/pesanan');
$this->assertSame(200, $status);
$this->assertSame(
['data' => [['id' => 1, 'pelanggan' => 'Siti', 'menu' => 'Es Teh Manis', 'totalHarga' => 25000]]],
json_decode($body, true),
);
}
public function testRuteTakDikenalMenghasilkan404(): void
{
[$status, $body] = self::http('GET', '/api/tidak-ada');
$this->assertSame(404, $status);
$this->assertSame('Tidak ditemukan', $body);
}
private static function http(string $method, string $path, ?string $body = null): array
{
$ch = curl_init('http://127.0.0.1:8127' . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
}
$respons = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$status, $respons === false ? '' : (string) $respons];
}
}Dari direktori chapter, suite dijalankan seperti biasa:
php /home/z/my-project/bin/phpunit.phar testsBerikut output nyata dari perintah tersebut:
PHPUnit 12.5.39 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.23
.. 2 / 2 (100%)
Time: 00:00.208, Memory: 28.05 MB
OK (2 tests, 6 assertions)Dua test selesai seperlima detik — hampir seluruhnya habis untuk menghidupkan server. Perhatikan bahwa test pertama memanggil POST lalu GET dalam satu method: keduanya adalah satu cerita “pesan lalu lihat”, dan memecahnya menjadi dua method justru menuntut berbagi keadaan antar test. Assertion assertSame pada hasil json_decode() memverifikasi tipe data secara persisi — id benar-benar integer, bukan string "1".
Automasi Browser #
E2e kita menguji lewat HTTP, tetapi pengguna sesungguhnya memakai browser: mengisi form, menekan tombol, menunggu animasi selesai. Mengotomatiskan lapisan itu membutuhkan browser automation — alat yang menjalankan browser sungguhan (Chromium, Firefox, WebKit) dan mengendalikannya dari kode. Di industri, alat yang mendominasi saat ini adalah Playwright; karena Playwright berjalan di ekosistem Node.js, test-nya ditulis dalam JavaScript, terpisah dari suite PHP namun hidup di repositori yang sama:
// tests/e2e/pesan.spec.js — Playwright berjalan di Node.js, bukan PHP
const { test, expect } = require('@playwright/test');
test('pesan lalu lihat daftar', async ({ page }) => {
await page.goto('http://127.0.0.1:8080/pesan');
await page.fill('#pelanggan', 'Budi');
await page.fill('#menu', 'Nasi Goreng Spesial');
await page.click('text=Pesan Sekarang');
await expect(page.locator('.daftar-pesanan')).toContainText('Nasi Goreng Spesial');
});Pola umumnya: server PHP dinyalakan (oleh task runner Playwright atau skrip seperti e2e.php kita), lalu test browser menggerakkan halaman dan menegaskan apa yang terlihat. Komunitas PHP juga punya jalur tanpa pindah bahasa — Symfony Panther dan Laravel Dusk mengendalikan browser dari dalam test PHPUnit. Prinsip seleksinya sederhana: jika yang perlu dibuktikan adalah API, uji lewat HTTP seperti skrip kita; jika yang perlu dibuktikan adalah antarmuka di browser, investasikan browser automation. Keduanya tetap di puncak piramida: sedikit, lambat, dan berharga.
Paritas Lingkungan #
E2e hanya bermakna jika lingkungan test menyerupai produksi — inilah yang disebut environment parity. Semakin lebar jurangnya, semakin banyak bug yang lolos di test lalu meledak di produksi, dan sebaliknya. Minimal, tiga hal ini harus disamakan:
- Versi PHP dan ekstensinya — perilaku
strict_types, deprecation, hingga ekstensi yang tersedia bisa berbeda antar versi. - Konfigurasi runtime —
error_reporting,memory_limit, dan pengaturan OPcache yang berbeda mengubah hasil nyata. - Cara data di-seed — produksi memakai data pengguna; test harus memakai data tetap yang dibuat ulang setiap run, bukan sisa run sebelumnya.
Aplikasi kita menunjukkan paritas dalam skala kecil: SQLite yang sama, JSON yang sama, HTTP yang sama. Yang sengaja tidak disamakan hanyalah lokasi berkas database — dan itu pun diatur lewat APP_DB, variabel lingkungan yang di produksi akan menunjuk ke server database sesungguhnya.
Mengelola Flakiness #
Test yang kadang lulus kadang gagal tanpa perubahan kode disebut flaky, dan E2e adalah jenjang yang paling rentan padanya: jaringan, proses, dan kecepatan mesin semuanya ikut bermain. Flakiness merusak kepercayaan — tim yang terbiasa dengan “coba jalankan lagi, pasti lulus” akan berhenti memedulikan lampu merah. Kebiasaan-kebiasaan ini menjaganya tetap deterministik:
- Tunggu dengan polling, bukan sleep tetap — seperti loop kesiapan server kita.
- Mulai setiap run dari keadaan bersih — hapus dan buat ulang database fixture.
- Jangan menegaskan nilai yang berubah — header
Daterespons, timestamp, atau id acak; angka{"id":1}kita hanya deterministik karena database selalu dimulai kosong. - Satu port per suite — dua suite E2e berjalan paralel di port yang sama akan saling menembak; beri masing-masing port tetap yang berbeda.
- Gagal itu sinyal, bukan gangguan — saat test flaky terjadi, cari akar masalahnya; karantina (tandai dan pisahkan) hanya sementara, dan retry otomatis adalah pilihan terakhir karena menyembunyikan penyakit.
Ringkasan #
End-to-end testing menutup babak pengujian dari luar: aplikasi dinyalakan utuh dan digerakkan lewat HTTP sungguhan, dengan server sebagai proses anak yang dihidupkan dan dipadamkan otomatis, keadaan awal yang selalu bersih, dan skenario yang mengikuti perjalanan pengguna. Kita sudah melihat polanya — polling, jaring pengaman shutdown, cleanup menyeluruh — beserta kebiasaan menjaga paritas lingkungan dan melawan flakiness. Dengan seluruh jenjang piramida terpasang, pertanyaan berikutnya muncul alami: seberapa banyak kode yang benar-benar disentuh oleh semua test itu? Pertanyaan itulah yang dijawab oleh code coverage pada Chapter berikutnya.
Referensi #
- Web server bawaan PHP (
php -S): https://www.php.net/manual/en/features.commandline.php - Dokumentasi PHPUnit: https://docs.phpunit.de/en/12.x/
- Playwright untuk automasi browser: https://playwright.dev/
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
e2e.php
Unduh berkas<?php
declare(strict_types=1);
// e2e.php — skrip end-to-end: menghidupkan server sungguhan,
// menembaknya lewat HTTP, lalu memadamkannya lagi.
final class Lari
{
public static int $gagal = 0;
}
function http(string $method, string $path, ?string $body = null): array
{
$ch = curl_init('http://127.0.0.1:8127' . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
}
$respons = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$status, $respons === false ? '' : (string) $respons];
}
function cek(string $label, int $harapanStatus, string $harapanBody, array $aktual): void
{
[$status, $body] = $aktual;
if ($status === $harapanStatus && $body === $harapanBody) {
echo "PASS $label -> $status $body\n";
return;
}
Lari::$gagal++;
echo "FAIL $label -> $status $body\n";
}
// 1. Mulai dari database bersih di direktori sementara
$dbPath = sys_get_temp_dir() . '/toko-mini-e2e.sqlite';
@unlink($dbPath);
putenv('APP_DB=' . $dbPath);
// 2. Hidupkan web server bawaan PHP sebagai proses anak
$server = proc_open(
[PHP_BINARY, '-S', '127.0.0.1:8127', 'public/index.php'],
[1 => ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']],
$pipes,
);
// Jaring pengaman: apa pun akhir skrip ini, server ikut dipadamkan
register_shutdown_function(static function () use ($server): void {
if (is_resource($server)) {
proc_terminate($server);
proc_close($server);
}
});
// 3. Tunggu server siap dengan polling — bukan sleep tetap
for ($i = 0; $i < 50; $i++) {
if (http('GET', '/api/health')[0] === 200) {
break;
}
usleep(100000);
}
// 4. Skenario perjalanan pengguna, ujung ke ujung
cek('GET /api/health', 200, '{"status":"ok"}', http('GET', '/api/health'));
cek('POST /api/pesanan', 201, '{"id":1}', http(
'POST',
'/api/pesanan',
'{"pelanggan":"Budi","menu":"Nasi Goreng Spesial"}',
));
cek(
'GET /api/pesanan',
200,
'{"data":[{"id":1,"pelanggan":"Budi","menu":"Nasi Goreng Spesial","totalHarga":25000}]}',
http('GET', '/api/pesanan'),
);
cek('GET /api/tidak-ada', 404, 'Tidak ditemukan', http('GET', '/api/tidak-ada'));
// 5. Padamkan server dan bersihkan berkas sementara
proc_terminate($server);
proc_close($server);
@unlink($dbPath);
if (Lari::$gagal > 0) {
echo Lari::$gagal, " skenario gagal\n";
exit(1);
}
echo "OK (4 skenario e2e lulus)\n";
tests/PesananE2eTest.php
Unduh berkas<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class PesananE2eTest extends TestCase
{
private static $server;
private static string $dbPath;
public static function setUpBeforeClass(): void
{
self::$dbPath = sys_get_temp_dir() . '/toko-mini-phpunit.sqlite';
@unlink(self::$dbPath);
putenv('APP_DB=' . self::$dbPath);
self::$server = proc_open(
[PHP_BINARY, '-S', '127.0.0.1:8127', __DIR__ . '/../public/index.php'],
[1 => ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']],
$pipes,
);
for ($i = 0; $i < 50; $i++) {
if (self::http('GET', '/api/health')[0] === 200) {
return;
}
usleep(100000);
}
self::fail('Server tidak siap setelah 5 detik');
}
public static function tearDownAfterClass(): void
{
if (is_resource(self::$server)) {
proc_terminate(self::$server);
proc_close(self::$server);
}
@unlink(self::$dbPath);
}
public function testPesanLaluLihatDaftar(): void
{
[$status, $body] = self::http(
'POST',
'/api/pesanan',
'{"pelanggan":"Siti","menu":"Es Teh Manis"}',
);
$this->assertSame(201, $status);
$this->assertSame(['id' => 1], json_decode($body, true));
[$status, $body] = self::http('GET', '/api/pesanan');
$this->assertSame(200, $status);
$this->assertSame(
['data' => [['id' => 1, 'pelanggan' => 'Siti', 'menu' => 'Es Teh Manis', 'totalHarga' => 25000]]],
json_decode($body, true),
);
}
public function testRuteTakDikenalMenghasilkan404(): void
{
[$status, $body] = self::http('GET', '/api/tidak-ada');
$this->assertSame(404, $status);
$this->assertSame('Tidak ditemukan', $body);
}
private static function http(string $method, string $path, ?string $body = null): array
{
$ch = curl_init('http://127.0.0.1:8127' . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
}
$respons = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$status, $respons === false ? '' : (string) $respons];
}
}