Files
summercms-initial-research/.planning/codebase/TESTING.md
2026-02-04 01:06:15 +01:00

13 KiB

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:

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., FormFormTest)
  • 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:

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):

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):

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:

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

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:

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:

# Generate coverage report (requires xdebug)
phpunit --coverage-html=coverage/

Report Configuration (phpunit.xml lines 14-26):

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

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

FakeDom.new()
    .addScript([...])
    .render()
    .then((dom) => {
        // Assertions here
        done();
    });

Callback-Based:

it('test name', function (done) {
    asyncOperation(() => {
        expect(...).toEqual(...);
        done();
    });
});

Error Testing:

PHP Exception Testing:

public function testThrowsException()
{
    $this->expectException(SystemException::class);
    $this->expectExceptionMessage('Expected message');

    // Code that throws
}

Validation Error Testing:

public function testValidationRules()
{
    $user = new User;
    $user->email = 'invalid';

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

JavaScript Error Testing:

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:

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