# Testing Patterns **Analysis Date:** 2026-05-28 ## Test Framework **Runner:** - PHPUnit `^11.5.3` (installed via Composer) - Config: `src/phpunit.xml` **Assertion Library:** - PHPUnit native assertions (`$this->assertTrue()`, `$this->assertSame()`, `$this->assertNull()`, `$this->assertGuest()`) - Laravel test response assertions (`$response->assertStatus()`, `$response->assertOk()`, `$response->assertSessionHasNoErrors()`, `$response->assertRedirect()`, `$response->assertSessionHasErrorsIn()`) **Mocking:** - Mockery `^1.6` available as dependency - No mock usage detected in existing tests **Run Commands:** ```bash composer test # Runs: php artisan config:clear --ansi && php artisan test ./vendor/bin/phpunit # Direct PHPUnit invocation ``` ## phpunit.xml Configuration **File:** `src/phpunit.xml` **Key settings:** - `colors="true"` — colored output - `bootstrap="vendor/autoload.php"` - Test suites: `Unit` (`tests/Unit`) and `Feature` (`tests/Feature`) - Source inclusion: `app` directory - Environment overrides: - `APP_ENV=testing` - `DB_CONNECTION=sqlite` - `DB_DATABASE=:memory:` — in-memory SQLite for tests - `CACHE_STORE=array` - `SESSION_DRIVER=array` - `MAIL_MAILER=array` - `QUEUE_CONNECTION=sync` - `BCRYPT_ROUNDS=4` — faster password hashing - `BROADCAST_CONNECTION=null` - `PULSE_ENABLED=false`, `TELESCOPE_ENABLED=false`, `NIGHTWATCH_ENABLED=false` ## Test File Organization **Location:** - Feature tests: `src/tests/Feature/` - Unit tests: `src/tests/Unit/` **Naming:** - PascalCase class names matching the tested resource: `ExampleTest`, `ProfileTest`, `AuthenticationTest` - Method names: snake_case descriptive names: `test_the_application_returns_a_successful_response`, `test_profile_page_is_displayed` - Method prefix: `test_` prefix convention **Directory Structure:** ``` tests/ ├── Feature/ │ ├── Auth/ │ │ ├── AuthenticationTest.php (empty) │ │ ├── EmailVerificationTest.php (empty) │ │ ├── PasswordConfirmationTest.php (empty) │ │ ├── PasswordResetTest.php (empty) │ │ ├── PasswordUpdateTest.php (empty) │ │ └── RegistrationTest.php (empty) │ ├── ExampleTest.php │ └── ProfileTest.php ├── Unit/ │ └── ExampleTest.php └── TestCase.php ``` **Base class:** `src/tests/TestCase.php` ```php namespace Tests; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { // } ``` - Extends `Illuminate\Foundation\Testing\TestCase` - Empty — no custom setup or helper methods defined - All Feature tests extend `Tests\TestCase` ## Test Structure **Suite Organization:** Feature tests follow standard Laravel testing patterns: ```php namespace Tests\Feature; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ProfileTest extends TestCase { use RefreshDatabase; public function test_profile_page_is_displayed(): void { $user = User::factory()->create(); $response = $this ->actingAs($user) ->get('/profile'); $response->assertOk(); } } ``` Unit tests use plain PHPUnit: ```php namespace Tests\Unit; use PHPUnit\Framework\TestCase; class ExampleTest extends TestCase { public function test_that_true_is_true(): void { $this->assertTrue(true); } } ``` **Patterns:** - **Arrange-Act-Assert** structure clearly visible - Variable names: `$user`, `$response` - Method return type `: void` consistently used - Chained HTTP methods: `$this->actingAs($user)->patch('/profile', [...])->assertSessionHasNoErrors()` ## Database Testing **Approach:** - In-memory SQLite (`DB_DATABASE=:memory:`) for all tests - `RefreshDatabase` trait used in `ProfileTest` to migrate schema before each test - `User::factory()->create()` for creating test users **ProfileTest database patterns:** ```php public function test_profile_information_can_be_updated(): void { $user = User::factory()->create(); $response = $this ->actingAs($user) ->patch('/profile', [ 'name' => 'Test User', 'email' => 'test@example.com', ]); $response ->assertSessionHasNoErrors() ->assertRedirect('/profile'); $user->refresh(); $this->assertSame('Test User', $user->name); $this->assertSame('test@example.com', $user->email); $this->assertNull($user->email_verified_at); } public function test_user_can_delete_their_account(): void { $user = User::factory()->create(); $response = $this ->actingAs($user) ->delete('/profile', ['password' => 'password']); $response ->assertSessionHasNoErrors() ->assertRedirect('/'); $this->assertGuest(); $this->assertNull($user->fresh()); } ``` Key patterns: - `$user->refresh()` to reload model from database after update - `$user->fresh()` to check if record still exists (null after delete) - `$this->assertGuest()` to verify logout - `->assertSessionHasErrorsIn('userDeletion', 'password')` for error bag validation ## Fixtures and Factories **Factory location:** `src/database/factories/UserFactory.php` **UserFactory:** ```php class UserFactory extends Factory { protected static ?string $password; public function definition(): array { return [ 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), ]; } public function unverified(): static { return $this->state(fn (array $attributes) => [ 'email_verified_at' => null, ]); } } ``` - Uses `fake()` helper for Faker data generation - Static `$password` cached to hash only once - `unverified()` state method for email-unverified users - No factories exist for `station`, `rainfall`, `waterlevel`, `siren`, or `notification` tables ## Coverage **Requirements:** - No coverage target configured in `phpunit.xml` - No coverage report configuration present - No CI pipeline detected enforcing coverage thresholds **View Coverage:** ```bash ./vendor/bin/phpunit --coverage-html coverage/ # or with --coverage-text for terminal output ``` ## Test Types **Unit Tests:** - 1 file: `tests/Unit/ExampleTest.php` - Single trivial assertion: `$this->assertTrue(true)` - Extends `PHPUnit\Framework\TestCase` directly (no Laravel app boot) - No real unit tests for services, models, or utilities exist **Feature Tests:** - 2 populated files + 6 empty files: - `ExampleTest.php` — tests `/` returns 200 - `ProfileTest.php` — 5 tests covering profile page, update, email verification, delete, and wrong-password deletion - `tests/Feature/Auth/*Test.php` — 6 files, all **empty** (zero bytes) **E2E Tests:** - Not used. No Laravel Dusk or Playwright detected. ## Test Coverage Gaps **Critical gaps:** 1. **No controller tests:** `RainfallController`, `WaterLevelController`, `AdminController`, `SirenController`, `NotificationController`, `MapController`, `cctvController`, `LocaleController` — zero tests 2. **No API tests:** `Api/StationController`, `Api/AuthController`, `Api/AlertController` — zero tests 3. **No service tests:** `FcmService` — zero tests 4. **No export tests:** `HourlyRainfallExport`, `WaterLevelExport` — zero tests 5. **No middleware tests:** `AdminMiddleware`, `LocalizationMiddleware` — zero tests 6. **No model tests:** `User` model features (casts, fillable, hidden, password reset notification) — zero tests 7. **6 placeholder test files** in `tests/Feature/Auth/` are empty (created by Laravel Breeze scaffold but never populated) ## Common Patterns **Async Testing:** - Not applicable — Laravel is synchronous; no async patterns in tests **Error Testing:** ```php public function test_correct_password_must_be_provided_to_delete_account(): void { $user = User::factory()->create(); $response = $this ->actingAs($user) ->from('/profile') ->delete('/profile', [ 'password' => 'wrong-password', ]); $response ->assertSessionHasErrorsIn('userDeletion', 'password') ->assertRedirect('/profile'); $this->assertNotNull($user->fresh()); } ``` Patterns: - `->from('/profile')` to set previous URL for redirect-back tests - `assertSessionHasErrorsIn('userDeletion', 'password')` for error bag validation - `->assertRedirect('/profile')` to check redirect destination - `$this->assertNotNull($user->fresh())` to verify model was NOT deleted **Authentication Testing:** ```php $user = User::factory()->create(); $response = $this ->actingAs($user) ->get('/profile'); $response->assertOk(); ``` **Session Assertions:** - `assertSessionHasNoErrors()` — no validation errors - `assertSessionHasErrorsIn('userDeletion', 'password')` — specific error in error bag - Session flash checked via redirect assertion ## CI Configuration - No CI pipeline configuration detected (no `.github/workflows/`, `.gitlab-ci.yml`, `Jenkinsfile`, etc.) - No code quality checks enforced in CI - No automated test running pipeline detected --- *Testing analysis: 2026-05-28*