Menjalankan Test Suite
Halaman ini membahas test suite milik package PandaBear sendiri: cara menjalankannya, bagaimana harness dibangun, dan test seperti apa yang harus ditambahkan ketika framework berubah. Ini berbeda dari Testing, yang membahas cara menguji Panel di application pengguna. Halaman tersebut mendokumentasikan helper yang dikirim package; halaman ini mendokumentasikan 1.260 test yang menjaga helper dan framework tetap benar.
Contoh minimal
composer install
vendor/bin/pest2
Contoh hasil:
Tests: 1260 passed (3648 assertions)
Duration: 66.99s2
Tidak dibutuhkan database server, .env, atau npm. phpunit.xml menetapkan DB_CONNECTION=testing dan tests/TestCase.php mengikatnya ke sqlite :memory:, sehingga fresh clone dapat menjalankan seluruh suite tanpa konfigurasi tambahan.
Harness
Tiga file menentukan environment tempat test berjalan.
phpunit.xml mendefinisikan satu suite untuk seluruh directory tests, dengan src sebagai coverage source:
<testsuites>
<testsuite name="PandaPanel Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include><directory>src</directory></include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="testing"/>
</php>2
3
4
5
6
7
8
9
10
11
12
tests/Pest.php menetapkan base class, database trait, dan satu constant:
TestCase::prepareWritableDirectories();
define('SETTINGS_PAGES', [
ProfileSettings::class,
SecuritySettings::class,
AppearanceSettings::class,
]);
pest()
->extend(TestCase::class)
->use(RefreshDatabase::class)
->in('Feature');2
3
4
5
6
7
8
9
10
11
12
prepareWritableDirectories() berjalan sebelum application pertama dibangun karena Laravel dapat menulis package manifest ketika application masih dalam proses construction.
SETTINGS_PAGES sengaja ditulis ulang dan tidak membaca Panel::SETTINGS_PAGES. Jika discovery test membaca constant yang sama dengan code under test, test akan selalu setuju dengan implementation, bahkan ketika implementation salah. Test exact page list menggunakan constant independen ini untuk mengurangi built-in page.
tests/TestCase.php extends Orchestra\Testbench\TestCase dan menentukan lima bagian utama:
| Method | Yang ditentukan |
|---|---|
getPackageProviders($app) | Inertia, Fortify, PandaPanelServiceProvider, dan Fortify provider dari example application |
applicationBasePath() | base_path() menjadi package root agar icon registry, generator stub, dan published TypeScript memakai file sebenarnya |
resolveApplicationConfiguration($app) | useAppPath(examples/app), useDatabasePath(examples/database), bootstrap Testbench, dan storage di build/testbench/storage |
defineEnvironment($app) | sqlite :memory:, array session/cache/mail, sync queue, FakeVite, private local disk, dan dua example Panel di config |
defineRoutes($router) / defineDatabaseMigrations() | Route/migration example application ditambah Fortify, Passkeys, dan package |
Sisi application berada di examples/. Menggunakan example sebagai test application memastikan contoh-contoh nyata terus diuji: App\Models\User, App\Panels\Admin, App\Panels\App, dan App\Policies\UserPolicy bukan dokumentasi mati.
Ada satu omission yang disengaja. defineEnvironment() tidak mendaftarkan guest redirect:
// Tidak ada setup guest redirect di sini. Dulu harness mendaftarkannya
// sebagai pengganti bootstrap/app.php milik application; sekarang package
// mendaftarkannya sendiri. Membiarkan harness kosong memastikan setiap auth
// test benar-benar menguji behavior package, bukan behavior yang dibuat test.2
3
4
Harness yang menyiapkan sendiri hal yang sedang diuji hanya membuktikan harness bekerja, bukan package.
Lokasi test
tests/
├── Pest.php
├── TestCase.php
├── Feature/Panel/ 91 file — satu per behavior
│ └── Negative/ 11 file — satu per kategori hal yang tidak boleh terjadi
└── Fixtures/Panel/ class yang diregistrasikan test2
3
4
5
6
Tidak ada tests/Unit. Hampir semua behavior framework melewati HTTP request, schema build, atau registry. Unit test terhadap fluent setter saja hanya membuktikan bahwa PHP dapat mengassign property.
Nama file test mengikuti behavior, bukan class. Contoh: ResourceQueryTest, TableSearchAndSortTest, PanelIsolationTest. Satu behavior sering melibatkan lebih dari satu class.
Fixture berada di tests/Fixtures/Panel/ dan merupakan implementasi nyata contract: Resource, Page, Widget, Panel, Policy, Plugin. Fixture yang dibuat untuk membuktikan suatu method tidak boleh dijalankan sebaiknya melempar exception dari method tersebut agar regression gagal keras. Tests\Fixtures\Panel\ForbiddenFixtureResource adalah pattern-nya:
public static function canViewAny(): bool
{
return false;
}
public static function navigationItem(PanelContract $panel): ?NavigationItem
{
return NavigationItem::make(
label: 'Secrets',
href: '/'.$panel->getPath().'/secrets',
badge: static fn (): int => throw new RuntimeException('Badge evaluated for an unauthorized item.'),
sort: 30,
group: 'User Management',
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
Closure badge yang melempar membuktikan authorization filter terjadi sebelum badge evaluation. Jika fixture hanya mengembalikan value normal, unauthorized item dapat bocor ke serialization dan test tetap terlihat hijau.
Menjalankan sebagian test
vendor/bin/pest # semua test
vendor/bin/pest tests/Feature/Panel/Negative # satu directory
vendor/bin/pest tests/Feature/Panel/TableFilterTest.php
vendor/bin/pest --filter=ResourceUrl # berdasarkan file/test name
vendor/bin/pest --filter='refuses a member the index'
vendor/bin/pest --compact # satu karakter per test
vendor/bin/pest --bail # stop pada failure pertama
vendor/bin/pest --dirty # hanya file dengan perubahan lokal
vendor/bin/pest --coverage # membutuhkan Xdebug atau PCOV2
3
4
5
6
7
8
9
composer test dan composer test-coverage hanya wrapper untuk command yang sama.
Apa yang harus ditambahkan oleh sebuah perubahan
Feature baru minimal harus memiliki test yang gagal jika feature tersebut dihapus. Itu hanya floor; standard suite lebih tinggi:
- Unauthorized access harus 403 pada setiap route, termasuk write verb dan action endpoint — bukan hanya index.
- Search hanya mencocokkan whitelisted column, dan sort column yang tidak dikenal/non-sortable harus diabaikan.
perPageharus di-clamp dan invalid filter value ditolak.- Semua record di-resolve melalui
Resource::query(), sehingga key di luar scope menghasilkan 404 di setiap endpoint ber-key. - Create hanya memvalidasi dan menyimpan declared field; update tidak menyentuh password yang tidak diubah.
- Bulk selection dengan satu forbidden record tidak mengubah apa pun.
- Serialized table/form tidak boleh berisi closure atau class name, karena payload tersebut dikirim ke browser.
- List route tidak boleh melakukan query per row.
assertOk() pada list page tidak cukup karena page yang membocorkan seluruh tenant tetap dapat menghasilkan 200. Assert behavior sebenarnya:
use Illuminate\Support\Facades\DB;
it('issues no query per row', function (): void {
$this->actingAs(User::factory()->admin()->create());
User::factory()->count(20)->create();
$queries = 0;
DB::listen(function () use (&$queries): void {
$queries++;
});
$this->get('/admin/users')->assertOk();
expect($queries)->toBeLessThan(10);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gunakan helper yang memang dikirim package
Test suite package menggunakan helper yang sama dengan yang dikirim untuk application, karena helper tersebut harus melewati TableSchema, TableQuery, FormSchema, dan Action yang sebenarnya. Helper yang menghitung jawaban dengan implementasi kedua dapat tetap hijau saat framework rusak.
panelTable(UserResource::class)->assertCanSeeRecord($user)->assertCount(2);
panelForm(UserResource::class)->assertFieldIsRequired('name');
panelTableActions(UserResource::class)->assertCanNotRun('purgeUnverified');2
3
Helper di-autoload melalui composer.json autoload.files, jadi tidak membutuhkan import atau base test class. Semua signature ada di Testing helpers.
Constraint paling penting untuk helper baru pada src/Testing/: helper tidak boleh menghitung answer melalui logic kedua. Ia harus menggunakan code path yang sama dengan framework.
Tiga fakta yang sering menghabiskan waktu debugging
// Inertia menempatkan `flash` sejajar dengan `props`, bukan di dalam `props`.
$toast = $this->get('/admin/users')->viewData('page')['flash']['toast'] ?? null;2
// Partial reload harus membawa asset version atau response menjadi 409.
$version = $this->get('/admin')->viewData('page')['version'];
$this->get('/admin', [
'X-Inertia' => 'true',
'X-Inertia-Version' => $version,
'X-Inertia-Partial-Component' => 'panel/Dashboard',
'X-Inertia-Partial-Data' => 'widgetData',
])->assertOk();2
3
4
5
6
7
8
9
use PandaPanel\Core\PanelManager;
// Memanggil page controller/helper langsung memerlukan current Panel context
// yang normalnya dipasang oleh ResolvePanel.
app(PanelManager::class)->setCurrentPanel(panel('admin'));2
3
4
5
Negative suite
tests/Feature/Panel/Negative/ menyatakan invariant framework sebagai hal-hal yang tidak boleh terjadi. Perubahan pada authorization, query parameter, file download, atau schema whitelist harus memiliki test di sini selain positive behavior test.
| File | Menjamin bahwa |
|---|---|
HostileTableInputTest | sort, filter, group, search, atau page parameter yang tidak pernah dideklarasikan schema diabaikan |
PrivilegeEscalationTest | guessed URL, handwritten POST, atau swapped id tidak dapat melewati policy |
ScopeBypassTest | record yang dikeluarkan Resource::query() tidak dapat dicapai melalui endpoint dengan key |
SchemaEscapeTest | create/edit hanya menulis field yang dideklarasikan form |
MalformedInputTest | malformed input mendapat response terkontrol dan tidak crash |
FileAndDataAccessTest | export, import report, atau notification hanya dimiliki satu user |
SpreadsheetFormulaTest | CSV cell tidak menjadi formula spreadsheet |
DistributionTest | Composer archive membawa package.json, tidak membawa lockfile, dan command yang disebut docs memang tersedia |
SchemaMistakeTest | schema yang tidak mungkin valid gagal saat build dengan pesan yang menunjuk nama salah |
UnreachableDeclarationTest | declaration yang targetnya tidak dapat merespons gagal dengan pesan yang jelas |
SilentAbsenceTest | Resource yang hilang memiliki penyebab yang dapat dibaca |
Standard negative test: hapus guard lalu pastikan test gagal. Security test yang tetap hijau setelah guard dihapus tidak membuktikan perlindungan apa pun. Lihat Security dan Negative security tests.
Test terhadap file, bukan runtime behavior
FrontendContractTest mengassert isi file. Ini tidak umum, tetapi sengaja dilakukan karena beberapa failure frontend bersifat silent dan tidak terlihat dari server response. Panel Page tanpa declared layout misalnya tetap dapat menghasilkan 200 tetapi dirender di shell host yang salah.
it('declares a layout on every published panel page', function (): void {
$without = [];
foreach (panelPageFiles() as $path) {
if (! str_contains(File::get($path), 'defineOptions({ layout:')) {
$without[] = str_replace(base_path().'/', '', $path);
}
}
expect($without)->toBe([]);
});2
3
4
5
6
7
8
9
10
11
IconRegistryTest adalah contoh kedua. Test berjalan melalui icon Panel, Navigation Item, Resource icon, serta record/bulk Action dan memastikan setiap nama tersedia pada resources/js/panel/icons/registry.ts. Icon yang tidak terdaftar tidak menghasilkan error; ia hanya tidak tampil. Gunakan Action::getIcon(), bukan toArray(), karena toArray(null) dapat menghasilkan null untuk action yang hidden/unauthorized saat record tidak tersedia, dan test akhirnya tidak mengumpulkan icon apa pun.
Hal yang perlu diperhatikan
Event::fake()tanpa argument dapat merusak Panel test. Ia mematikan model event yang dibutuhkan Resource.fakePanelNotifications()hanya mem-fake satu event class untuk alasan tersebut.- Fixture Panel perlu
->settings(false). Panel yang dibangun langsung di test tidak memiliki registered routes untuk tiga built-in Account Page; navigation builder dapat melempar "Route not defined". - Register Panel dua kali melempar exception. Registry dapat bertahan antar-test dalam process yang sama; gunakan
PanelManager::has(). - Route yang diregistrasikan dalam test membutuhkan
Route::getRoutes()->refreshNameLookups()sebelumRoute::has()atauroute()dapat melihat route name baru. - Transaction level bersifat relatif. Suite sudah memiliki transaction rollback sendiri. Jadi wrapped call berarti baseline
DB::transactionLevel()+ 1, bukan selalu level 1. Gate::define()tidak menimpa registered policy. Untuk menguji unauthorized Page di-drop, ganti policy denganGate::policy(Model::class, FixturePolicy::class).- Jangan assert rendered output
@vite. Dalam development hasilnya dev-server URL; setelah build menjadi hashed filename. BindTests\Fixtures\Panel\RecordingVitedan assert captured entrypoints. defaultPerPage(2)tanpa 2 diperPageOptions()fallback ke option pertama. Ini sering menjebak fixture.- PHPStan tidak menganalisis
tests/. Trait yang hanya digunakan fixture dapat dianggap unused darisrc/; tempatkan shared behavior pada source yang benar.
Lihat juga
- Local development — setup yang diasumsikan command di halaman ini
- Coding standards — dua check lain dalam
composer ci - Security — invariant yang dilindungi negative suite dan cara melaporkan vulnerability
- Pull requests — apa yang harus dibawa perubahan selain test
- Test setup — helper yang sama dari sisi application
- Testing helpers, tables, forms, actions
- Negative security tests
- Frontend contract tests
- CI matrix