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

9.2 KiB

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:

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

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:

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:

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:

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:

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:

./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:

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:

$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