Testing
Framework
The project uses Pest v4 (built on PHPUnit) with the Pest Laravel plugin. Tests are configured in phpunit.xml and tests/Pest.php.
Running Tests
# Run all tests
composer check:tests
# or
vendor/bin/pest
# Run a specific test file
vendor/bin/pest tests/Feature/Contact/ContactSubmissionTest.php
# Run a specific module's tests
vendor/bin/pest Modules/Bills/tests/Feature/
# Run with filter
vendor/bin/pest --filter="can process a contact form"
# Run in parallel
vendor/bin/pest --parallelTest Structure
Test Suites (phpunit.xml)
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
<directory>Modules/Events/tests/Feature</directory>
<directory>Modules/Rideshare/tests/Feature</directory>
<directory>Modules/Integrations/tests/Feature</directory>
<directory>Modules/Rental/tests/Feature</directory>
<directory>Modules/Perks/tests/Feature</directory>
<directory>Modules/Bills/tests/Feature</directory>
</testsuite>When adding a new module with tests, register its test directories in phpunit.xml.
Base Test Classes
tests/TestCase.php — Bare base test case (extends Illuminate\Foundation\Testing\TestCase).
tests/FeatureTestCase.php — For feature tests that need database and auth:
abstract class FeatureTestCase extends BaseTestCase
{
use CreatesJwt;
use RefreshDatabase;
}tests/CreatesJwt.php — Trait for authenticating test requests:
trait CreatesJwt
{
public function actingAsJwt(Authenticatable $user): self
{
return $this->withToken($this->generateJwtForUser($user));
}
}Pest Configuration (tests/Pest.php)
// Default test case for tests/ and Unit/
pest()
->extend(Tests\TestCase::class)
->in('tests', 'Unit');
// Architecture rules
arch()
->expect('App')
->toUseStrictTypes()
->not->toUse(['die', 'dd', 'dump']);
arch()->preset()->php();
arch()->preset()->security()->ignoring('md5');Test Environment
Configured in phpunit.xml:
| Variable | Value | Purpose |
|---|---|---|
APP_ENV | testing | Testing environment |
DB_CONNECTION | mysql | Uses MySQL (not SQLite) |
DB_DATABASE | testing | Separate test database |
DB_DATABASE_BILLS | testing_bills | Separate bills test database |
MAIL_MAILER | array | Captures emails in memory |
QUEUE_CONNECTION | sync | Jobs run synchronously |
CACHE_STORE | array | In-memory cache |
You must create testing and testing_bills MySQL databases locally before running tests.
Writing Feature Tests
Pattern: Authenticated Endpoint
<?php
use App\Models\User;
use Tests\FeatureTestCase;
uses(FeatureTestCase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->actingAsJwt($this->user);
});
test('it returns data for authenticated user', function () {
$response = $this->getJson(route('some.route'));
$response->assertStatus(200);
$response->assertJsonStructure(['data']);
});Pattern: Mocking External Services
Third-party services (HubSpot, Twilio) should always be mocked in tests:
beforeEach(function () {
$hubspotService = Mockery::mock(HubSpotService::class);
$this->app->instance(HubSpotService::class, $hubspotService);
$hubspotService->shouldReceive('updateOrCreateContact')
->once()
->andReturn(['ok' => true, 'contactId' => '123']);
});Pattern: Testing with Factories
Module factories create test data:
test('it returns signup details', function () {
$contract = V2Contract::factory()->create();
$tenant = V2Tenant::factory()->create([
'contract_id' => $contract->id,
'email' => $this->user->email,
'status' => 'COMPLETED',
]);
$response = $this->getJson(route('api.bills.tenant-details.show'));
$response->assertStatus(200);
$response->assertJson([
'contractId' => $contract->id,
]);
});Pattern: Database Assertions
$this->assertDatabaseHas('v2_tenants', [
'contract_id' => $contract->id,
'email' => 'john@example.com',
'status' => 'COMPLETED',
], 'bills'); // Specify the connection for bills DB tablesWriting Unit Tests
Unit tests go in tests/Unit/ or Modules/*/tests/Unit/. They test isolated logic without HTTP or database:
<?php
// Modules/Properties/tests/Unit/Actions/FormatHouseAddressActionTest.php
test('it formats house address correctly', function () {
// Test the action with mock data
});Full Check Suite
The composer check script runs all quality checks in order:
composer check
# Equivalent to:
# 1. vendor/bin/pest (tests)
# 2. vendor/bin/pint --test (code style)
# 3. vendor/bin/phpstan analyse --memory-limit=2G (static analysis)Static Analysis
PHPStan (via Larastan) runs at level 8 with Mockery support:
composer check:staticConfiguration in phpstan.neon:
- Analyses
app/andModules/ - Excludes test files and some modules still being migrated (Yardi, CRMs, Perks, External)