IDEA Extended
This commit is contained in:
467
.planning/codebase/TESTING.md
Normal file
467
.planning/codebase/TESTING.md
Normal 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*
|
||||
Reference in New Issue
Block a user