468 lines
13 KiB
Markdown
468 lines
13 KiB
Markdown
# 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*
|