Skip to Content
API v2Testing

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 --parallel

Test 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:

VariableValuePurpose
APP_ENVtestingTesting environment
DB_CONNECTIONmysqlUses MySQL (not SQLite)
DB_DATABASEtestingSeparate test database
DB_DATABASE_BILLStesting_billsSeparate bills test database
MAIL_MAILERarrayCaptures emails in memory
QUEUE_CONNECTIONsyncJobs run synchronously
CACHE_STOREarrayIn-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 tables

Writing 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:static

Configuration in phpstan.neon:

  • Analyses app/ and Modules/
  • Excludes test files and some modules still being migrated (Yardi, CRMs, Perks, External)
Last updated on