IDEA Extended

This commit is contained in:
Jakub Zych
2026-02-04 01:06:15 +01:00
parent 7446e886a8
commit d8a4a4555c
11 changed files with 2189 additions and 0 deletions

View File

@@ -0,0 +1,467 @@
# 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*