319 lines
9.2 KiB
Markdown
319 lines
9.2 KiB
Markdown
# 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*
|