Files
sides/.planning/codebase/TESTING.md
2026-05-28 16:25:22 +08:00

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*