# Testing Patterns

**Analysis Date:** 2026-02-04

**Reference:** This document captures WinterCMS testing patterns (PHP and JavaScript) that should inform the Scala rewrite's test structure and conventions.

## Test Framework

**PHP Testing:**

**Runner:**
- PHPUnit 9.5.8+
- Config: `phpunit.xml` at root with module-specific configs in `modules/*/phpunit.xml`
- Bootstrap: `modules/system/tests/bootstrap/app.php`
- Base classes in `modules/system/tests/bootstrap/`

**JavaScript Testing:**

**Runner:**
- Jest
- Config: `modules/system/tests/js/jest.config.js`
- Uses jsdom environment (default behavior, Node.js runtime not specified)

**Run Commands:**
```bash
composer test              # Run all PHPUnit tests (stop on first failure)
composer lint              # Check PHP syntax
composer sniff             # Code standards check (PSR-1, PSR-2, PSR-4)
npm test                   # Run Jest tests (in js directory)
npm test -- --watch       # Watch mode for JavaScript
```

## Test File Organization

**PHP Tests:**

**Location:**
- Tests co-located with source code in `modules/*/tests/` subdirectories
- Test subdirectories mirror source structure (e.g., `modules/backend/tests/widgets/`, `modules/backend/tests/models/`, `modules/backend/tests/classes/`)
- Module-level phpunit.xml files in each module

**Naming:**
- Test files end with `Test.php` (e.g., `FormTest.php`, `AuthManagerTest.php`, `ImportModelTest.php`)
- Test class names: Source class name + `Test` suffix (e.g., `Form` → `FormTest`)
- Test methods start with `test` prefix (e.g., `testRestrictedFieldWithUserWithNoPermissions()`)

**Structure:**
```
modules/
├── backend/
│   ├── tests/
│   │   ├── widgets/
│   │   │   ├── FormTest.php
│   │   │   ├── ListsTest.php
│   │   │   └── FilterWidgetTest.php
│   │   ├── models/
│   │   │   ├── ImportModelTest.php
│   │   │   └── ExportModelTest.php
│   │   ├── classes/
│   │   │   ├── AuthManagerTest.php
│   │   │   └── WidgetManagerTest.php
│   │   ├── formwidgets/
│   │   │   ├── CheckboxTest.php
│   │   │   └── ColorPickerTest.php
│   │   ├── fixtures/
│   │   │   └── models/
│   │   │       └── UserFixture.php
│   │   └── traits/
│   │       └── WidgetMakerTest.php
│   └── phpunit.xml
```

**JavaScript Tests:**

**Location:**
- Tests in `modules/system/tests/js/cases/` directory
- Structure mirrors feature organization
- Tests for Snowboard framework (AJAX library), form framework, and utilities

**Naming:**
- Test files: `*.test.js` suffix (e.g., `DataAttribute.test.js`, `Snowboard.test.js`)
- Suite names: Descriptive string (e.g., "Data Attribute Request AJAX library")
- Test cases: Descriptive string starting with "can" or "should" (e.g., "can parse request data")

**Structure:**
```
modules/system/tests/js/
├── jest.config.js
└── cases/
    ├── snowboard/
    │   ├── ajax/
    │   │   ├── DataAttribute.test.js
    │   │   └── Request.test.js
    │   ├── main/
    │   │   ├── Snowboard.test.js
    │   │   └── PluginLoader.test.js
    │   └── extras/
    │       └── DataConfig.test.js
    └── framework/
        └── FormParent.test.js
```

## Test Structure

**PHP Suite Organization:**

**Base Test Case:**
```php
namespace System\Tests\Bootstrap;

class TestCase extends \Illuminate\Foundation\Testing\TestCase
{
    public function createApplication()
    {
        // Creates Laravel application with testing configuration
    }
}
```

**Plugin Test Case:**
- Extends base `TestCase`
- Auto-detects plugin being tested
- Automatically loads plugin dependencies
- Uses `InteractsWithAuthentication` trait
- Runs all migrations for test context

**Example Test Structure** (`modules/backend/tests/classes/AuthManagerTest.php` lines 1-76):
```php
class AuthManagerTest extends TestCase
{
    protected AuthManager $instance;
    protected $existingPermissions = [];

    public function setUp(): void
    {
        $this->createApplication();
        $this->instance = AuthManager::instance();
        $this->existingPermissions = $this->instance->listPermissions();

        $this->instance->registerPermissions('Winter.TestCase', [
            'test.permission_one' => [
                'label' => 'Test Permission 1',
                'tab' => 'Test',
                'order' => 200
            ],
        ]);
    }

    protected function tearDown(): void
    {
        AuthManager::forgetInstance();
    }

    public function testListPermissions()
    {
        $permissions = $this->listNewPermissions();
        $this->assertCount(2, $permissions);
        $this->assertEquals([
            'test.permission_one',
            'test.permission_two'
        ], $permissions);
    }
}
```

**Patterns Observed:**

**Setup/Teardown:**
- `setUp(): void` method runs before each test (creates fresh application state)
- `tearDown(): void` method cleans up singleton instances
- Database runs in-memory (SQLite `:memory:`) for test speed
- Fresh encryption key generated per test run
- All migrations run in setUp via `Artisan::call('winter:up')`

**Assertion Patterns:**
- PHPUnit 9 assertions: `$this->assertCount()`, `$this->assertEquals()`, `$this->assertNull()`, `$this->assertNotNull()`
- Compatibility shims for PHPUnit 8/9 differences in `TestCase` base class (e.g., `assertFileNotExists()`, `assertRegExp()`)

**Model Testing Pattern** (`modules/backend/tests/widgets/FormTest.php` lines 33-66):
```php
public function testRestrictedFieldWithUserWithNoPermissions()
{
    $user = new UserFixture;
    $this->actingAs($user);

    $form = $this->restrictedFormFixture();
    $form->render();
    $this->assertNull($form->getField('testRestricted'));
}
```

## JavaScript Test Structure

**Suite and Test Organization:**
```javascript
describe('Data Attribute Request AJAX library', function () {
    it('can parse request data', function (done) {
        FakeDom
            .new()
            .addScript([
                'modules/system/assets/js/build/manifest.js',
                'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
            ])
            .render()
            .then(
                (dom) => {
                    const DataAttributeSingleton = dom.window.Snowboard.attributeRequest();

                    expect(
                        DataAttributeSingleton.parseData('{foo: "bar"}')
                    ).toEqual({ foo: 'bar' });

                    done();
                }
            );
    });
});
```

**Patterns:**
- Uses FakeDom helper for DOM testing
- Scripts loaded explicitly (no auto-import)
- Async tests use `done()` callback (callback-style async)
- Expectations: `expect().toEqual()` for value comparison
- Jest global functions: `describe()`, `it()`, `jest.setTimeout()`

## Mocking

**PHP Mocking:**

**Framework:** Mockery (mockery/mockery 1.4.4+)

**Patterns:**
- Extends/uses reflection helpers in base `TestCase`:
  ```php
  protected static function callProtectedMethod($object, $name, $params = [])
  protected static function getProtectedProperty($object, $name)
  protected static function setProtectedProperty($object, $name, $value)
  ```
- Trait `InteractsWithAuthentication` provides auth mocking for controller tests
- Database transactions rolled back after each test (not explicitly visible but default Laravel behavior)

**Authentication Mocking:**
```php
use Backend\Tests\Concerns\InteractsWithAuthentication;

$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
```

**What to Mock:**
- Database queries (use fixtures instead)
- External services (mail, events)
- Singleton instances (reset in tearDown)

**What NOT to Mock:**
- Models and their relations (use fixtures)
- Framework components (use real instances)
- Business logic methods (test actual behavior)

**JavaScript Mocking:**

**Framework:** Jest built-in mocking

**Patterns:**
- `clearMocks: true` in jest.config.js clears mock state before each test
- Mock creation via `jest.mock()`
- Used for module imports, timers, API calls

## Fixtures and Factories

**PHP Test Fixtures:**

**Location:**
- `modules/backend/tests/fixtures/models/UserFixture.php`
- Located in `fixtures/` subdirectory parallel to test files

**Test Data Pattern:**
```php
class UserFixture extends User
{
    public function withPermission($permission, $value = true)
    {
        $this->permissions[$permission] = $value;
        return $this;
    }
}
```

**Usage:**
- Fixtures are model subclasses that don't persist
- Can build chains of factory methods
- Used for controller/widget tests requiring user context
- No factory library (plain class extension)

**Database Fixtures:**
- Run migrations in test setup to create schema
- Use model factories for initial test data where needed

## Coverage

**Requirements:**
- Coverage tracking configured in `phpunit.xml` but not enforced
- Coverage collected from `./modules/` directory
- Excludes routes, migrations, test fixtures

**View Coverage:**
```bash
# Generate coverage report (requires xdebug)
phpunit --coverage-html=coverage/
```

**Report Configuration** (`phpunit.xml` lines 14-26):
```xml
<coverage>
    <include>
        <directory suffix=".php">./modules/</directory>
    </include>
    <exclude>
        <file>./modules/backend/routes.php</file>
        <file>./modules/cms/routes.php</file>
        <file>./modules/system/routes.php</file>
        <directory suffix=".php">./modules/backend/database</directory>
        <directory suffix=".php">./modules/cms/database</directory>
        <directory suffix=".php">./modules/system/database</directory>
    </exclude>
</coverage>
```

## Test Types

**Unit Tests:**
- Scope: Single class/method in isolation
- Examples: `AuthManagerTest`, `ImportModelTest`
- Uses mocking for dependencies
- Fast execution (milliseconds)
- Tests business logic and edge cases

**Integration Tests:**
- Scope: Multiple components working together
- Examples: `FormTest`, `FilterWidgetTest` (form rendering with widgets)
- Uses real database (in-memory SQLite)
- Tests workflows and component interaction
- Slower execution (seconds per suite)

**E2E Tests:**
- Framework: Not observed in WinterCMS core (JavaScript e2e exists but minimal coverage)
- Would require: Full browser automation (Selenium/Cypress)
- Not standard in this codebase

**Fixture-Based Tests:**
- Use model fixtures for state setup
- Examples: `testRestrictedFieldWithUserWithRightPermissions()` - user fixture with permission
- Reduce mock overhead

## Common Patterns

**Async Testing (PHP):**

**Event-Based Async:**
```php
// No explicit async patterns in WinterCMS PHP tests
// Uses synchronous execution with database transactions
```

**Database Isolation:**
- Transactions rolled back after each test (Laravel default)
- Fresh database state per test via setUp()

**Async Testing (JavaScript):**

**Promise-Based:**
```javascript
FakeDom.new()
    .addScript([...])
    .render()
    .then((dom) => {
        // Assertions here
        done();
    });
```

**Callback-Based:**
```javascript
it('test name', function (done) {
    asyncOperation(() => {
        expect(...).toEqual(...);
        done();
    });
});
```

**Error Testing:**

**PHP Exception Testing:**
```php
public function testThrowsException()
{
    $this->expectException(SystemException::class);
    $this->expectExceptionMessage('Expected message');

    // Code that throws
}
```

**Validation Error Testing:**
```php
public function testValidationRules()
{
    $user = new User;
    $user->email = 'invalid';

    if (!$user->validate()) {
        $this->assertCount(1, $user->errors());
    }
}
```

**JavaScript Error Testing:**
```javascript
it('throws on invalid input', () => {
    expect(() => {
        parser.parse(null);
    }).toThrow();
});
```

## Plugin Testing

**Plugin Test Case** (`modules/system/tests/bootstrap/PluginTestCase.php`):
- Extends base `TestCase`
- Auto-detects plugin under test via namespace
- Loads all plugin dependencies
- Runs migrations in test-specific configuration
- Resets singleton instances between tests

**Plugin Test Setup:**
```php
abstract class PluginTestCase extends TestCase
{
    use InteractsWithAuthentication;

    public function setUp(): void
    {
        PluginManager::forgetInstance();
        UpdateManager::forgetInstance();
        parent::setUp();

        Artisan::call('winter:up');  // Run all migrations

        $pluginCode = $this->guessPluginCode();
        if (!is_null($pluginCode)) {
            $this->instantiatePlugin($pluginCode, false);
        }
    }
}
```

**Plugin Test Isolation:**
- Each plugin can run its own test suite
- Test database in-memory and transaction-isolated
- Dependencies automatically resolved and loaded

---

*Testing analysis: 2026-02-04*
*Reference: WinterCMS test patterns (PHP PHPUnit + JavaScript Jest) for Scala rewrite guidance*
