# Testing Standards & Critical Patterns

## 🚨 Critical SQLite Setup (The "Proven Pattern")
Use this EXACT pattern for database tests to avoid "no such table" errors.

```php
use Illuminate\Foundation\Testing\RefreshDatabase;
// 1. MUST be at file level — NO ->in() when used inside an individual test file
uses(RefreshDatabase::class);

// 2. Setup MUST be file-level beforeEach (NOT inside describe)
beforeEach(function () {
    // 3. Configure In-Memory SQLite explicitly
    config(['database.default' => 'tenant_connection']);
    config(['database.connections.tenant_connection' => [
        'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '',
    ]]);

    // 4. Purge & Reconnect
    DB::purge('tenant_connection');
    DB::reconnect('tenant_connection');

    // 5. Explicitly Drop & Create with Connection
    Schema::connection('tenant_connection')->dropIfExists('table_name');
    Schema::connection('tenant_connection')->create('table_name', function ($table) {
        $table->id();
        // ...
    });
});
```

## Mocking Standards

### Eloquent Models (BANNED)
**❌ BAD:** `Mockery::mock('alias:App\Models\Invoice')` or `Invoice::shouldReceive('chunk')`.
**✅ GOOD:** Use Database Factories (`Invoice::factory()->create()`). Eloquent magic methods, iterators (`chunk`), and scopes are fundamentally incompatible with Mockery. Attempting to mock models wastes development hours and leads to fragile tests.

### Static Classes & Helpers
**❌ BAD:** `MyHelper::shouldReceive(...)` (Fails for non-facades)
**✅ GOOD:** `Mockery::mock('alias:App\Helpers\MyHelper')`

```php
$helper = Mockery::mock('alias:App\Helpers\IpaasHelper');
$helper->shouldReceive('staticMethod')->once()->andReturn('value');
```

### Logging
**❌ BAD:** `Log::fake()` (Swallows logs, hard to debug)
**✅ GOOD:** `Log::spy()` (Allows verification but keeps working)

### Cleanup (Mandatory Order)
In `afterEach`, you MUST close Mockery *before* flushing the container to avoid contamination.
```php
afterEach(function () {
    Mockery::close(); // FIRST
    app()->forgetInstance(MyService::class); // SECOND
});
```

## State Management & Multi-Tenancy

### Static Poisoning
Services utilizing static cache properties (e.g. `protected static $cache = []`) contaminate the global PHP process across tests. 
**❌ BAD:** Allowing static arrays to persist between tests, causing bleeding state.
**✅ GOOD:** Refactor to Dependency Injection (Service Pattern) OR explicitly flush the static cache using `ReflectionClass` inside `beforeEach()`.

### Multi-Tenancy (No Such Table Fix)
For tenant-context tests, failure to define the tenant context causes SQLite to default to `:memory:` and throw `no such table` errors when models attempt to use the `tenant_connection`. ALWAYS enforce the trait:
```php
use Tests\Traits\SetupMultiTenancyForTests;
uses(SetupMultiTenancyForTests::class);

beforeEach(function () {
    $this->context = $this->setupMultiTenancy(accountId: 'test', subdomain: 'test.app');
});
```

## Anti-Patterns
1. **NEVER** put DB setup inside `describe()` blocks.
2. **NEVER** use implicit connections (`Schema::create`). Always use `Schema::connection('name')->create`.
3. **NEVER** use `overload:` mocks if possible. Use Dependency Injection or `alias:` to avoid "Cannot redeclare class" errors.

## 🧠 Strategic Testing Patterns (The "SuiteX Wisdom")

### 1. Integration vs. Unit Balance
**❌ BAD:** Heavily mocking internal service calls for orchestration logic.
**✅ GOOD:** Use **Real Service Classes** with SQLite. Only mock external boundaries (APIs, Mail, S3).
* **Reason:** Heavy mocks hide integration bugs. If `ImportJobCoordinator` calls `DependencyResolver`, use the real `DependencyResolver`.

### 2. Exception vs. Graceful Handling
**❌ BAD:** Testing for exceptions on code that has evolved to return default values.
**✅ GOOD:** If code captures `try/catch` and returns `0` or `false`, update the expectation to `toBe(0)`, not `toThrow()`.

### 3. Service Pattern for Overload Mocks
**❌ BAD:** Using `overload:` mocks to intercept `new ClassName()`.
**✅ GOOD:** Refactor production code to accept the dependency (Dependency Injection).
```php
// Code
public function handle(?ExternalService $service = null) {
    $service = $service ?? new ExternalService(); // Test can inject mock
}
```

### 4. Database Migrations
**❌ BAD:** Updating a Model `$fillable` without updating the Test `Schema::create`.
**✅ GOOD:** Every DB field added to production MUST be added to the `Schema::create` closure in the test `beforeEach`.

### 5. Column-Model-Test Triple Check (Mandatory for `select()` tests)
When a model defines a static column list (e.g., `getKanbanColumns()`, `getReportColumns()`),
the test `Schema::create` MUST include ALL those columns. Any missing column causes a SQLite
`no such column` error that the controller's try/catch swallows as a 500.

**✅ Pattern:**
```php
// Comment is mandatory — links test schema to production model:
// Schema MUST match {ModelName}::getKanbanColumns()
$this->ensureTableExists('tenant_connection', 'table_name', function ($table) {
    $table->id();
    $table->string('title')->nullable();       // in getKanbanColumns()
    $table->integer('entity')->nullable();     // FK required for eager load
    $table->integer('subsidiary')->nullable(); // in getKanbanColumns()
    // ...
});
```

**Pre-commit verification:**
```bash
# List production columns
php artisan tinker --execute="print_r({Model}::getKanbanColumns());"
# Compare with test schema grep
grep 'table->' tests/Feature/.../YourTest.php
# Any column in production but NOT in test = blocking bug before push
```

---

### 6. ⚠️ Anti-Pattern: Schema-From-Memory (SQLite `beforeEach`)

**Rule:** When writing the `Schema::create` block in a `beforeEach`, you MUST open and read the actual migration file to copy column definitions. **Never write column names from memory.**

**Why:** Column names guessed from memory produce "phantom schemas" — the existing tests pass because they don't reference the wrong columns, but any future test that accesses those fields fails with `SQLSTATE[HY000]: no such column: field_type`. This is a silent time bomb.

**❌ BAD — Written from memory (invented columns):**
```php
// ⚠️ Column names NOT verified against migration — high risk
Schema::connection('tenant_connection')->create('your_table', function ($table) {
    $table->string('guessed_column_name')->nullable();  // ❌ may not exist
    $table->string('another_guess')->nullable();        // ❌ may not exist
});
```

**✅ GOOD — Copied directly from migration, with reference:**
```php
// Schema MUST match YourModel — verified against:
// database/migrations/XXXX_create_your_table.php
Schema::connection('tenant_connection')->create('your_table', function ($table) {
    $table->string('real_column', 50);                        // ✅ from migration
    $table->boolean('another_real_column')->default(true);    // ✅ from migration
    $table->json('json_column')->nullable();                  // ✅ from migration
    $table->string('policy_column', 50)->default('default');  // ✅ from migration
});
```

**Enforcement checklist before committing any `beforeEach` with `Schema::create`:**
- [ ] Open the corresponding migration file (`database/migrations/`)
- [ ] Copy column names AND types verbatim — do not paraphrase
- [ ] Add a comment referencing the migration file path
- [ ] Any column in the migration but missing in the test = blocking bug

---

### 7. ⚠️ Anti-Pattern: `uses()->in()` Global Scope in Individual Test Files

**Rule:** In individual test files (`*Test.php`), ALWAYS use `uses(Trait::class)` **WITHOUT** `->in()`.
The `->in('Directory')` suffix is a **project-wide directive** — it applies the trait to every test file in that directory tree.

**❌ BAD — Inside an individual `*Test.php` file:**
```php
// This silently applies RefreshDatabase to ALL tests in tests/Unit/ (80+ files)
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Unit');
```

**✅ GOOD — File-scoped only:**
```php
// Applies ONLY to this specific test file
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
```

**When IS `->in()` correct?**
Only in `tests/Pest.php` or a dedicated setup file, where you intentionally want directory-wide configuration:
```php
// tests/Pest.php — project-level config file (intentional)
uses(RefreshDatabase::class)->in('Feature'); // ✅ affects all Feature tests by design
```

**Key signal:** If you are writing `uses()->in()` inside a file named `*Test.php`, it is almost certainly wrong.
---

---

### 8. ⚠️ Anti-Pattern: Redundant `uses()` Declaration Conflicting with Global `Pest.php` (BUG-K)

**Rule:** If `tests/Pest.php` already applies a base test case class to a directory via `uses(Tests\TestCase::class)->in('Feature')`, any individual test file inside that directory MUST NOT declare `uses(Tests\TestCase::class)` again. Pest treats this as a duplicate `uses()` conflict and aborts the **entire test suite** with exit code 255 — not only the offending file.

**Why this is catastrophic:** Unlike a compile error or a single failing test, a duplicate `uses()` conflict kills every test in the run before any test executes. CI shows a complete failure with no per-test results, which can be misdiagnosed as a runner environment issue.

**❌ BAD — individual file re-declares what Pest.php already applies globally:**
```php
// tests/Pest.php already has:
// uses(Tests\TestCase::class)->in('Feature');

// tests/Feature/CloudStorage/UploadFileTest.php
uses(Tests\TestCase::class); // ← CONFLICT: Pest sees two uses() for this file → exit 255
```

**✅ GOOD — let the global declaration from Pest.php do its job:**
```php
// tests/Feature/CloudStorage/UploadFileTest.php
// No uses() declaration needed — inherited from tests/Pest.php global config

it('uploads a file', function () {
    // TestCase methods ($this->actingAs, etc.) are available from the global uses()
});
```

**When IS an explicit `uses()` correct?**
Only when the file needs an ADDITIONAL trait that is not in the global config:
```php
// ✅ CORRECT — adding a trait ON TOP of the globally-applied TestCase
uses(Tests\Traits\SetupMultiTenancyForTests::class); // only the extra trait
```

**Pre-commit checklist:**
- [ ] Open `tests/Pest.php` and identify which classes are applied globally via `->in()`.
- [ ] For any new test file, verify its directory is NOT already covered by those global declarations.
- [ ] If covered: remove any explicit `uses(GloballyAppliedClass::class)` from the new file.

---

### 9. ⚠️ Strategic Pattern: Negative Path Verification (Fail-Closed)

**Rule:** When testing validators, transformers, or ACL logic, you MUST include at least one **Negative Scenario** for "Unknown/Unsupported" inputs.

**Why:** This verifies the system adheres to the [Fail-Closed Logic](000-core-standards.md#fail-closed-logic) core standard. Without a negative test, a "Fail-Open" bug (silently accepting unknown data) can go unnoticed even if happy paths pass.

**✅ TDD Pattern:**
```php
test('Scenario X: Unknown Type Rejection', function () {
    $payload = ['type' => 'unsupported_magic_value'];
    
    // Expect the system to REJECT explicitly
    expect(fn() => Validator::execute($payload))
        ->toThrow(ValidationException::class, 'Unsupported type');
});
```

**Enforcement:**
- [ ] Every validator test suite MUST have an "Unknown/Unsupported" test case.
- [ ] Verify that the rejection reason is specific (e.g., mention the unsupported type).
