Testing the platform
How the PHPUnit suite is organised, how to run it on XAMPP or Linux, and how to write a feature test the house way.
Testing the platform
The platform ships with a large PHPUnit suite — ~700 test methods across ~90 files, all green — organized into tests/Unit and tests/Feature. This page covers how to run it and how to add to it correctly.
Who this is for
Developers changing platform code, and anyone verifying an install before go-live.
Difficulty: Medium · Time: about 10 minutes.
Running the suite
On Windows/XAMPP (PHP may not be on PATH — call it explicitly):
D:/Xampp/php/php.exe vendor/bin/phpunit
Anywhere else:
php vendor/bin/phpunit
Useful variations:
php vendor/bin/phpunit --filter=MobileApiTest # one test class
php vendor/bin/phpunit --testsuite=Feature # one suite
php vendor/bin/phpunit --filter=test_lead_index # one method
The suite runs against a separate database phpunit.xml points tests at the leadhub_testing MySQL database (with CACHE_STORE=array, SESSION_DRIVER=array, MAIL_MAILER=array, QUEUE_CONNECTION=sync, low bcrypt rounds). That database must exist and be migrated before the first run. Connection credentials live in your local phpunit.xml/environment — they are intentionally not documented here, and must never be committed to docs, tickets or chat.
How tests are organised
| Location | What lives there |
|---|---|
tests/Unit |
Pure logic — value objects, services with fakes, helpers |
tests/Feature |
HTTP + DB behavior — panels, API endpoints, jobs, webhooks |
tests/TestCase.php |
Base class (Laravel's CreatesApplication conventions) |
The house convention is Illuminate\Foundation\Testing\DatabaseTransactions rather than database refreshes: each test runs inside a transaction that is rolled back afterwards. The schema stays intact and tests stay fast — but it means the testing database must already be migrated, and anything you create in a test is invisible to parallel processes.
Because every tenant-owned model is behind the fail-closed BelongsToTenant scope, tests must establish tenant context the same way real requests do — bind app('current_tenant'), sign in a user whose tenant_id is set, or assert the empty-result behavior deliberately. This is the most common gotcha when a new feature test "sees nothing". See Multi-tenancy & isolation.
Factories
Model factories live in database/factories/:
UserFactory, TenantFactory, LeadFactory, LeadSourceConnectionFactory, WebhookLogFactory, AiSdrAgentFactory, ChatbotConfigFactory.
Writing a feature test (the house way)
A typical feature test — factory setup, tenant context, HTTP assertion, transaction rollback:
<?php
namespace Tests\Feature;
use App\Models\Lead;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class LeadListTest extends TestCase
{
use DatabaseTransactions;
public function test_leads_belong_to_their_tenant(): void
{
$tenant = Tenant::factory()->create();
$lead = Lead::factory()->create(['tenant_id' => $tenant->id]);
$this->assertDatabaseHas('leads', [
'id' => $lead->id,
'tenant_id' => $tenant->id,
]);
}
public function test_guest_cannot_open_the_admin_panel(): void
{
$this->get('/admin')->assertRedirect();
}
}
For API tests (device-token or API-key surfaces), follow tests/Feature/MobileApiTest.php — it shows the project's shape for mobile endpoints: create the tenant/device/user, authenticate the way the real client does, and assert the {"data": …} / {"error": {"message","code"}} envelopes.
Conventions that keep the suite green
DatabaseTransactionson every DB-touching test — no truncation, no ordering dependence.- Factories over hand-written arrays — extend an existing factory instead of inventing rows.
- Assert envelopes, not internals — mobile tests assert the documented
error.codevalues; panel tests assert HTTP status/redirects rather than Filament internals. - Bind tenant context when a test touches scoped models, mirroring
ApiKeyAuthentication/DeviceAuthenticatebehavior. - No external network calls — third-party systems are exercised through fakes/stubs (queue
sync, mailarray, cachearray).
Related guides
Was this page helpful?
Thanks — noted. ✦