Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:43:33 +08:00
commit 624998dbc3
27 changed files with 6036 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "typo3-testing",
"description": "Create and manage TYPO3 extension tests with comprehensive testing infrastructure. Includes unit, functional, and acceptance testing support, PHPUnit configurations, Docker setup, CI/CD templates, and automated test generation following TYPO3 best practices.",
"version": "1.1.0-20251114",
"author": {
"name": "Netresearch DTT GmbH",
"email": "info@netresearch.de"
},
"skills": [
"./"
]
}

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Temporary
*.tmp
*.bak

20
LICENSE Normal file
View File

@@ -0,0 +1,20 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[Full GPL-2.0 license text - same as typo3-docs-skill]
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# typo3-testing
Create and manage TYPO3 extension tests with comprehensive testing infrastructure. Includes unit, functional, and acceptance testing support, PHPUnit configurations, Docker setup, CI/CD templates, and automated test generation following TYPO3 best practices.

256
SKILL.md Normal file
View File

@@ -0,0 +1,256 @@
---
name: typo3-testing
version: 1.1.0
description: Create, configure, and manage TYPO3 extension tests (unit, functional, acceptance) following official TYPO3 testing framework patterns. Use when setting up tests, writing test cases, configuring PHPUnit, managing fixtures, or integrating CI/CD pipelines for TYPO3 extensions. Covers PHPUnit 11/12, TYPO3 v12/v13 LTS, modern dependency injection testing patterns, and comprehensive quality tooling (PHPStan level 10, Rector, php-cs-fixer).
license: Complete terms in LICENSE.txt
---
# TYPO3 Testing Skill
## Purpose
This skill helps developers create, configure, and manage TYPO3 extension tests following official TYPO3 testing framework patterns and community best practices.
## Quick Start Decision Tree
### 1. What do you need?
```
├─ Create new test
│ ├─ Unit test (no database, fast)
│ ├─ Functional test (with database)
│ └─ Acceptance test (browser-based E2E)
├─ Setup testing infrastructure
│ ├─ Basic (unit + functional)
│ └─ Full (+ acceptance with Docker)
├─ Add CI/CD
│ ├─ GitHub Actions
│ └─ GitLab CI
├─ Manage fixtures
│ ├─ Create fixture
│ └─ Update fixture
└─ Understand testing patterns
└─ Check references/
```
### 2. Which test type?
**Unit Tests** → Use when:
- Testing pure logic without external dependencies
- No database access needed
- Fast execution required (milliseconds)
- Examples: Validators, Value Objects, Utilities
**Functional Tests** → Use when:
- Testing database interactions
- Full TYPO3 instance needed
- Testing repositories, controllers, hooks
- Slower execution acceptable (seconds)
**Acceptance Tests** → Use when:
- Testing complete user workflows
- Browser interaction required
- Frontend functionality validation
- Slowest execution (minutes)
### 3. Infrastructure exists?
**No** → Run setup script:
```bash
# Basic setup (unit + functional)
scripts/setup-testing.sh
# With acceptance testing
scripts/setup-testing.sh --with-acceptance
```
**Yes** → Generate test:
```bash
# Generate test class
scripts/generate-test.sh <type> <ClassName>
# Examples:
scripts/generate-test.sh unit UserValidator
scripts/generate-test.sh functional ProductRepository
scripts/generate-test.sh acceptance LoginCest
```
## Commands
### Setup Testing Infrastructure
```bash
scripts/setup-testing.sh [--with-acceptance]
```
Creates:
- composer.json dependencies
- Build/phpunit/ configs
- Build/Scripts/runTests.sh
- Tests/ directory structure
- .github/workflows/ CI configs (optional)
- docker-compose.yml (with --with-acceptance)
### Generate Test Class
```bash
scripts/generate-test.sh <type> <ClassName>
```
Creates:
- Test class file
- Fixture file (for functional tests)
- AGENTS.md in test directory (if missing)
### Validate Setup
```bash
scripts/validate-setup.sh
```
Checks:
- Required composer dependencies
- PHPUnit configuration files
- Test directory structure
- Docker availability (for acceptance tests)
## Test Execution
### via runTests.sh (Recommended)
```bash
# Unit tests
Build/Scripts/runTests.sh -s unit
# Functional tests
Build/Scripts/runTests.sh -s functional
# Acceptance tests
Build/Scripts/runTests.sh -s acceptance
# All quality checks
Build/Scripts/runTests.sh -s lint
Build/Scripts/runTests.sh -s phpstan
Build/Scripts/runTests.sh -s cgl
```
### via Composer
```bash
# All tests
composer ci:test
# Specific suites
composer ci:test:php:unit
composer ci:test:php:functional
# Quality tools
composer ci:test:php:lint
composer ci:test:php:phpstan
composer ci:test:php:cgl
```
## References
Detailed documentation for each testing aspect:
- [Unit Testing](references/unit-testing.md) - UnitTestCase patterns, mocking, assertions
- [Functional Testing](references/functional-testing.md) - FunctionalTestCase, fixtures, database
- [Acceptance Testing](references/acceptance-testing.md) - Codeception, Selenium, page objects
- [JavaScript Testing](references/javascript-testing.md) - CKEditor plugins, data-* attributes, frontend tests
- [Test Runners](references/test-runners.md) - runTests.sh orchestration patterns
- [CI/CD Integration](references/ci-cd.md) - GitHub Actions, GitLab CI workflows
- [Quality Tools](references/quality-tools.md) - PHPStan, Rector, php-cs-fixer
## Templates
Ready-to-use configuration files and examples:
- `templates/AGENTS.md` - AI assistant context template for test directories
- `templates/UnitTests.xml` - PHPUnit config for unit tests
- `templates/FunctionalTests.xml` - PHPUnit config for functional tests
- `templates/FunctionalTestsBootstrap.php` - Bootstrap for functional tests
- `templates/runTests.sh` - Test orchestration script
- `templates/github-actions-tests.yml` - GitHub Actions workflow
- `templates/docker-compose.yml` - Docker services for acceptance tests
- `templates/codeception.yml` - Codeception configuration
- `templates/example-tests/` - Example test classes
## Fixture Management (Functional Tests)
### Create Fixture
1. Create CSV file in `Tests/Functional/Fixtures/`
2. Define database table structure
3. Add test data rows
4. Import in test via `$this->importCSVDataSet()`
### Fixture Strategy
- Keep fixtures minimal (only required data)
- One fixture per test scenario
- Use descriptive names (e.g., `ProductWithCategories.csv`)
- Document fixture purpose in AGENTS.md
## CI/CD Integration
### GitHub Actions
```bash
# Add workflow
cp templates/github-actions-tests.yml .github/workflows/tests.yml
# Customize matrix (PHP versions, TYPO3 versions, databases)
```
### GitLab CI
See `references/ci-cd.md` for GitLab CI example configuration.
## Test Organization Standards
**When organizing tests**, apply these patterns:
1. Group tests by feature or domain, not by test type
2. Name unit and functional tests with `*Test.php` suffix
3. Name acceptance tests with `*Cest.php` suffix
4. Keep fixtures minimal, reusable, and well-documented
5. Use specific assertions (assertSame, assertInstanceOf) over generic assertEquals
6. Ensure each test can run independently without side effects
7. Apply setUp() and tearDown() methods consistently across test classes
8. Document test strategy in AGENTS.md to explain what each directory tests
## Troubleshooting
**Tests not found:**
- Check PHPUnit XML testsuites configuration
- Verify test class extends correct base class
- Check file naming convention (*Test.php)
**Database errors in functional tests:**
- Verify database driver in FunctionalTests.xml
- Check fixture CSV format (proper escaping)
- Ensure bootstrap file is configured
**Acceptance tests fail:**
- Verify Docker and Docker Compose installed
- Check Selenium service is running
- Review Codeception configuration
## Reference Material Usage
**When understanding TYPO3 testing patterns**, read [TYPO3 Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/) for:
- Official testing framework usage
- Best practices and patterns
- Version-specific requirements
**When working with test framework internals**, check [TYPO3 Testing Framework](https://github.com/typo3/testing-framework) for:
- Framework API reference
- Base test case implementations
- Fixture handling utilities
**When seeking reference implementations**, study [Tea Extension](https://github.com/TYPO3BestPractices/tea) for:
- Production-quality test examples
- Complete testing infrastructure setup
- Best practice patterns in action
**When writing PHPUnit tests**, consult [PHPUnit Documentation](https://phpunit.de/documentation.html) for:
- Assertion methods
- Test doubles and mocking
- Configuration options
**When implementing acceptance tests**, reference [Codeception Documentation](https://codeception.com/docs/) for:
- Page object patterns
- Browser automation
- E2E test scenarios

View File

@@ -0,0 +1,44 @@
# SKILL.md Refactoring Summary
**Date:** 2025-11-14
**Version Change:** 1.0.0 → 1.1.0
**Skill:** typo3-testing
## Changes Applied
### Pattern 2: Converted "## Best Practices" to Imperative Form
- **Before:** "## Best Practices" with numbered list
- **After:** "## Test Organization Standards" with imperative "When organizing tests" format
- **Changes:**
- Added "When organizing tests, apply these patterns:" trigger
- Converted each practice to action-oriented instruction
- Maintained all 8 best practices with clearer directives
### Pattern 4: Converted "## Resources" to Imperative Usage
- **Before:** Simple bullet list with URLs and descriptions
- **After:** "## Reference Material Usage" with trigger-based usage instructions
- **Changes:**
- "When understanding TYPO3 testing patterns" → TYPO3 Testing Documentation
- "When working with test framework internals" → TYPO3 Testing Framework
- "When seeking reference implementations" → Tea Extension
- "When writing PHPUnit tests" → PHPUnit Documentation
- "When implementing acceptance tests" → Codeception Documentation
- Each entry specifies what information to extract
## Impact Analysis
**Readability:** Improved - clearer when to apply each practice
**Consistency:** Aligned with typo3-ddev-skill and typo3-docs-skill patterns
**Usability:** Enhanced - readers know when to consult each resource
**Structure:** Maintained complete information while improving clarity
## Files Modified
- `/SKILL.md` (lines 1-257)
## Verification
- Version number updated in YAML frontmatter: ✓
- Best Practices converted to imperative form: ✓
- Resources converted to usage-based format: ✓
- No broken links: ✓

137
plugin.lock.json Normal file
View File

@@ -0,0 +1,137 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:netresearch/claude-code-marketplace:skills/typo3-testing",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "6173b9432bed4017bbcaa1c1eb4d26708b84538a",
"treeHash": "9a26ffc3c5fb57676595accbcbe5a79d217ac8fad7233ec6a0fb4754b0f4e455",
"generatedAt": "2025-11-28T10:27:19.129054Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "typo3-testing",
"description": "Create and manage TYPO3 extension tests with comprehensive testing infrastructure. Includes unit, functional, and acceptance testing support, PHPUnit configurations, Docker setup, CI/CD templates, and automated test generation following TYPO3 best practices.",
"version": "1.1.0-20251114"
},
"content": {
"files": [
{
"path": "LICENSE",
"sha256": "62e5db1ab91125b874e6408a65301f10802d8204c06b3047ec07f84d7db3f471"
},
{
"path": "README.md",
"sha256": "f28a4c09a65697ddbcd61ba2f1fb25300f9962a060a2b24f218ef973c04eba65"
},
{
"path": ".gitignore",
"sha256": "db6cc676b541ea3d6987d9e5d662e998f36a146eba3abd14b5e65cab025fc389"
},
{
"path": "SKILL.md",
"sha256": "8d3779f65441ab80e7c3c5f3b6f4cdee483e9d1eced56a7820330cccb79ad488"
},
{
"path": "references/quality-tools.md",
"sha256": "d186766c200a72755ea9456677a8a26632b6fa5093aad847f9c4463ea56aac15"
},
{
"path": "references/test-runners.md",
"sha256": "ea26c2ddcc6abc23a5206665aa39c053250d09cc21c3b172f9c08f78727f4017"
},
{
"path": "references/acceptance-testing.md",
"sha256": "2eb3373a49f4bd584d6af026406fe89b77bf53eaf6250a5e739036fe681152b1"
},
{
"path": "references/functional-testing.md",
"sha256": "7a5f94e262f76b533328e6522cfce2730e03c5d4718b20dc82bf9427e59bca4f"
},
{
"path": "references/unit-testing.md",
"sha256": "d284a11aea0f9f74c65de21028ad1e0312a23d8f2b4a6f02d71383908a8f7a24"
},
{
"path": "references/javascript-testing.md",
"sha256": "d912bf3a55c3fde7d5b35dd6b8d217a2c92dc56cbce2aa833b97cbf4f930c090"
},
{
"path": "references/ci-cd.md",
"sha256": "cc63f64bb0b3cc59e08c145a27e5f4a22a74ffb23c7d07d11b43276e60872fb5"
},
{
"path": "claudedocs/refactoring-summary.md",
"sha256": "23e8c66839a7c3dc5f277a82eccb2d74dcea57ebf992ca3e01a519270613e7f5"
},
{
"path": "scripts/validate-setup.sh",
"sha256": "c44fa4a6b8b0e88e6d2bbbdd4415279635a6478dd9f1e599cb27f890fef8ef92"
},
{
"path": "scripts/setup-testing.sh",
"sha256": "e19c01d4c34abd3baa6afd6674e15095d1666909bac602273ac666f34a50e1db"
},
{
"path": "scripts/generate-test.sh",
"sha256": "e72909650ac0cc12e9483e0ec7a84da863dce99882fa12410035460fc1c93cd3"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "db557a6fe92994c05bf347cc7f7685c5130da1c4e8e246443037a12b3808c426"
},
{
"path": "templates/FunctionalTestsBootstrap.php",
"sha256": "60caec69cc3792c3f22184c627eb6f597e57a387c1646e2277148e877f66aa0c"
},
{
"path": "templates/UnitTests.xml",
"sha256": "61cd16d938ba8fbb7d49c21a444fb86766ce65449f66e3d9a12135451a174786"
},
{
"path": "templates/github-actions-tests.yml",
"sha256": "8da189186487e10e5d5ddf0c51302138c169220a2c11306c4eab345230853d41"
},
{
"path": "templates/AGENTS.md",
"sha256": "9a0c9055ce3404dca8f3913515de25698030855e5583367450aae2eb22c56e17"
},
{
"path": "templates/FunctionalTests.xml",
"sha256": "8b73dedafa787328d263b46661bb4df91850e60ed12d2a33c5f493b828e79f9a"
},
{
"path": "templates/docker/codeception.yml",
"sha256": "85c24f53e5a1f7e959312d141fd62261f0440be3ca70a45896082e0b5353dd2b"
},
{
"path": "templates/docker/docker-compose.yml",
"sha256": "2693efec58eb5ddebd218d2a452581f21c02397fa214fea3ae46e677516881dd"
},
{
"path": "templates/example-tests/ExampleAcceptanceCest.php",
"sha256": "30e158149508c15ecb75de850d8b6f9b09baad0014d6ab0183aba249dbd03d10"
},
{
"path": "templates/example-tests/ExampleFunctionalTest.php",
"sha256": "81de444c741d8f05ef9abba8b005d32474da9b77a0a0aefe559350acd5f2b428"
},
{
"path": "templates/example-tests/ExampleUnitTest.php",
"sha256": "2c68c617a1c70f8a896c252c4aa0ed5578af5bcc7c0e7cc49257866da05fb11c"
}
],
"dirSha256": "9a26ffc3c5fb57676595accbcbe5a79d217ac8fad7233ec6a0fb4754b0f4e455"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,468 @@
# Acceptance Testing in TYPO3
Acceptance tests verify complete user workflows through browser automation using Codeception and Selenium.
## When to Use Acceptance Tests
- Testing complete user journeys (login → browse → checkout)
- Frontend functionality validation
- Cross-browser compatibility
- JavaScript-heavy interactions
- Visual regression testing
## Requirements
- Docker and Docker Compose
- Codeception
- Selenium (ChromeDriver or Firefox)
- Web server (Nginx/Apache)
## Setup
### 1. Install Codeception
```bash
composer require --dev codeception/codeception codeception/module-webdriver codeception/module-asserts
```
### 2. Initialize Codeception
```bash
vendor/bin/codecept bootstrap
```
### 3. Docker Compose
Create `Build/docker-compose.yml`:
```yaml
version: '3.8'
services:
web:
image: php:8.2-apache
volumes:
- ../../:/var/www/html
ports:
- "8000:80"
environment:
- TYPO3_CONTEXT=Testing
depends_on:
- db
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: typo3_test
ports:
- "3306:3306"
selenium:
image: selenium/standalone-chrome:latest
ports:
- "4444:4444"
shm_size: 2gb
```
### 4. Codeception Configuration
Create `codeception.yml`:
```yaml
paths:
tests: Tests/Acceptance
output: var/log/acceptance
data: Tests/Acceptance/_data
support: Tests/Acceptance/_support
envs: Tests/Acceptance/_envs
actor_suffix: Tester
extensions:
enabled:
- Codeception\Extension\RunFailed
params:
- .env.testing
suites:
acceptance:
actor: AcceptanceTester
modules:
enabled:
- WebDriver:
url: http://web:8000
browser: chrome
host: selenium
port: 4444
- \\Helper\\Acceptance
```
## Test Structure
### Basic Test (Cest)
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Acceptance;
use Vendor\Extension\Tests\Acceptance\AcceptanceTester;
final class LoginCest
{
public function _before(AcceptanceTester $I): void
{
// Runs before each test
$I->amOnPage('/');
}
public function loginAsBackendUser(AcceptanceTester $I): void
{
$I->amOnPage('/typo3');
$I->fillField('username', 'admin');
$I->fillField('password', 'password');
$I->click('Login');
$I->see('Dashboard');
$I->seeInCurrentUrl('/typo3/module/dashboard');
}
public function loginFailsWithWrongPassword(AcceptanceTester $I): void
{
$I->amOnPage('/typo3');
$I->fillField('username', 'admin');
$I->fillField('password', 'wrong_password');
$I->click('Login');
$I->see('Login error');
$I->seeInCurrentUrl('/typo3');
}
}
```
### Page Objects Pattern
Create reusable page objects:
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Acceptance\PageObject;
use Vendor\Extension\Tests\Acceptance\AcceptanceTester;
final class LoginPage
{
public static string $URL = '/typo3';
public static string $usernameField = '#username';
public static string $passwordField = '#password';
public static string $loginButton = 'button[type="submit"]';
private AcceptanceTester $tester;
public function __construct(AcceptanceTester $I)
{
$this->tester = $I;
}
public function login(string $username, string $password): void
{
$I = $this->tester;
$I->amOnPage(self::$URL);
$I->fillField(self::$usernameField, $username);
$I->fillField(self::$passwordField, $password);
$I->click(self::$loginButton);
}
}
```
Use page object in test:
```php
public function loginWithPageObject(AcceptanceTester $I): void
{
$loginPage = new LoginPage($I);
$loginPage->login('admin', 'password');
$I->see('Dashboard');
}
```
## Common Actions
### Navigation
```php
// Navigate to URL
$I->amOnPage('/products');
$I->amOnUrl('https://example.com/page');
// Click links
$I->click('Products');
$I->click('#menu-products');
$I->click(['link' => 'View Details']);
```
### Form Interaction
```php
// Fill fields
$I->fillField('email', 'user@example.com');
$I->fillField('#password', 'secret');
// Select options
$I->selectOption('country', 'Germany');
$I->selectOption('category', ['Books', 'Electronics']);
// Checkboxes and radio buttons
$I->checkOption('terms');
$I->uncheckOption('newsletter');
// Submit forms
$I->submitForm('#contact-form', [
'name' => 'John Doe',
'email' => 'john@example.com',
]);
```
### Assertions
```php
// See text
$I->see('Welcome');
$I->see('Product added', '.success-message');
$I->dontSee('Error');
// See elements
$I->seeElement('.product-list');
$I->seeElement('#add-to-cart');
$I->dontSeeElement('.error-message');
// URL checks
$I->seeInCurrentUrl('/checkout');
$I->seeCurrentUrlEquals('/thank-you');
// Field values
$I->seeInField('email', 'user@example.com');
// Number of elements
$I->seeNumberOfElements('.product-item', 10);
```
### JavaScript
```php
// Execute JavaScript
$I->executeJS('window.scrollTo(0, document.body.scrollHeight);');
// Wait for JavaScript
$I->waitForJS('return document.readyState === "complete"', 5);
// Wait for element
$I->waitForElement('.product-list', 10);
$I->waitForElementVisible('#modal', 5);
// AJAX requests
$I->waitForAjaxLoad();
```
### Screenshots
```php
// Take screenshot
$I->makeScreenshot('product_page');
// Screenshot on failure (automatic in codeception.yml)
$I->makeScreenshot('FAILED_' . $test->getName());
```
## Data Management
### Using Fixtures
```php
public function _before(AcceptanceTester $I): void
{
// Reset database
$I->resetDatabase();
// Import fixtures
$I->importFixture('products.sql');
}
```
### Test Data
Create data providers:
```php
protected function productData(): array
{
return [
['name' => 'Product A', 'price' => 10.00],
['name' => 'Product B', 'price' => 20.00],
];
}
/**
* @dataProvider productData
*/
public function createsProduct(AcceptanceTester $I, \Codeception\Example $example): void
{
$I->amOnPage('/admin/products/new');
$I->fillField('name', $example['name']);
$I->fillField('price', $example['price']);
$I->click('Save');
$I->see($example['name']);
}
```
## Browser Configuration
### Multiple Browsers
```yaml
# codeception.yml
suites:
acceptance:
modules:
config:
WebDriver:
browser: '%BROWSER%'
# Run with different browsers
BROWSER=chrome vendor/bin/codecept run acceptance
BROWSER=firefox vendor/bin/codecept run acceptance
```
### Headless Mode
```yaml
WebDriver:
capabilities:
chromeOptions:
args: ['--headless', '--no-sandbox', '--disable-gpu']
```
## Running Tests
### Basic Execution
```bash
# All acceptance tests
vendor/bin/codecept run acceptance
# Specific test
vendor/bin/codecept run acceptance LoginCest
# Specific method
vendor/bin/codecept run acceptance LoginCest:loginAsBackendUser
# With HTML report
vendor/bin/codecept run acceptance --html
```
### Via runTests.sh
```bash
Build/Scripts/runTests.sh -s acceptance
```
### With Docker Compose
```bash
# Start services
docker-compose -f Build/docker-compose.yml up -d
# Run tests
vendor/bin/codecept run acceptance
# Stop services
docker-compose -f Build/docker-compose.yml down
```
## Best Practices
1. **Use Page Objects**: Reusable page representations
2. **Wait Strategically**: Use `waitFor*` methods for dynamic content
3. **Independent Tests**: Each test can run standalone
4. **Descriptive Names**: Clear test method names
5. **Screenshot on Failure**: Automatic debugging aid
6. **Minimal Setup**: Only necessary fixtures and data
7. **Stable Selectors**: Use IDs or data attributes, not fragile CSS
## Common Pitfalls
**No Waits for Dynamic Content**
```php
$I->click('Load More');
$I->see('Product 11'); // May fail if AJAX is slow
```
**Proper Waits**
```php
$I->click('Load More');
$I->waitForElement('.product-item:nth-child(11)', 5);
$I->see('Product 11');
```
**Brittle Selectors**
```php
$I->click('div.container > div:nth-child(3) > button'); // Fragile
```
**Stable Selectors**
```php
$I->click('[data-test="add-to-cart"]'); // Stable
$I->click('#product-add-button'); // Better
```
**Large Test Scenarios**
```php
// Don't test entire user journey in one test
public function completeUserJourney() { /* 50 steps */ }
```
**Focused Tests**
```php
public function addsProductToCart() { /* 5 steps */ }
public function proceedsToCheckout() { /* 7 steps */ }
```
## Debugging
### Interactive Mode
```bash
vendor/bin/codecept run acceptance --debug
vendor/bin/codecept run acceptance --steps
```
### Pause Execution
```php
$I->pauseExecution(); // Opens interactive shell
```
### HTML Reports
```bash
vendor/bin/codecept run acceptance --html
# View report at Tests/Acceptance/_output/report.html
```
## Resources
- [Codeception Documentation](https://codeception.com/docs/)
- [WebDriver Module](https://codeception.com/docs/modules/WebDriver)
- [Page Objects](https://codeception.com/docs/06-ReusingTestCode#pageobjects)
- [TYPO3 Acceptance Testing](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/AcceptanceTests.html)

523
references/ci-cd.md Normal file
View File

@@ -0,0 +1,523 @@
# CI/CD Integration for TYPO3 Testing
Continuous Integration and Continuous Deployment workflows for automated TYPO3 extension testing.
## GitHub Actions
### Basic Workflow
Create `.github/workflows/tests.yml`:
```yaml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
lint:
name: Lint PHP
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Install dependencies
run: composer install --no-progress
- name: Run linting
run: composer ci:test:php:lint
phpstan:
name: PHPStan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Install dependencies
run: composer install --no-progress
- name: Run PHPStan
run: composer ci:test:php:phpstan
unit:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- name: Install dependencies
run: composer install --no-progress
- name: Run unit tests
run: composer ci:test:php:unit
- name: Upload coverage
# Upload coverage for all PHP versions
uses: codecov/codecov-action@v3
functional:
name: Functional Tests
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
database: ['mysqli', 'pdo_mysql', 'postgres', 'sqlite']
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: typo3_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: typo3_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.database == 'postgres' && 'pdo_pgsql' || 'mysqli' }}
- name: Install dependencies
run: composer install --no-progress
- name: Run functional tests
run: |
export typo3DatabaseDriver=${{ matrix.database }}
export typo3DatabaseHost=127.0.0.1
export typo3DatabaseName=typo3_test
export typo3DatabaseUsername=${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
export typo3DatabasePassword=${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
composer ci:test:php:functional
```
### Matrix Strategy
Test multiple PHP and TYPO3 versions:
```yaml
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
typo3: ['12.4', '13.0']
exclude:
- php: '8.1'
typo3: '13.0' # TYPO3 v13 requires PHP 8.2+
steps:
- name: Install TYPO3 v${{ matrix.typo3 }}
run: |
composer require "typo3/cms-core:^${{ matrix.typo3 }}" --no-update
composer update --no-progress
```
### Caching Dependencies
```yaml
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: composer-${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
restore-keys: |
composer-${{ runner.os }}-${{ matrix.php }}-
composer-${{ runner.os }}-
- name: Install dependencies
run: composer install --no-progress --prefer-dist
```
### Code Coverage
```yaml
- name: Run tests with coverage
run: vendor/bin/phpunit -c Build/phpunit/UnitTests.xml --coverage-clover coverage.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
```
## GitLab CI
### Basic Pipeline
Create `.gitlab-ci.yml`:
```yaml
variables:
COMPOSER_CACHE_DIR: ".composer-cache"
MYSQL_ROOT_PASSWORD: "root"
MYSQL_DATABASE: "typo3_test"
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .composer-cache/
stages:
- lint
- analyze
- test
.php:
image: php:${PHP_VERSION}-cli
before_script:
- apt-get update && apt-get install -y git zip unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --no-progress
lint:
extends: .php
stage: lint
variables:
PHP_VERSION: "8.2"
script:
- composer ci:test:php:lint
phpstan:
extends: .php
stage: analyze
variables:
PHP_VERSION: "8.2"
script:
- composer ci:test:php:phpstan
cgl:
extends: .php
stage: analyze
variables:
PHP_VERSION: "8.2"
script:
- composer ci:test:php:cgl
unit:8.1:
extends: .php
stage: test
variables:
PHP_VERSION: "8.1"
script:
- composer ci:test:php:unit
unit:8.2:
extends: .php
stage: test
variables:
PHP_VERSION: "8.2"
script:
- composer ci:test:php:unit
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
functional:8.2:
extends: .php
stage: test
variables:
PHP_VERSION: "8.2"
typo3DatabaseDriver: "mysqli"
typo3DatabaseHost: "mysql"
typo3DatabaseName: "typo3_test"
typo3DatabaseUsername: "root"
typo3DatabasePassword: "root"
services:
- mysql:8.0
script:
- composer ci:test:php:functional
```
### Multi-Database Testing
```yaml
.functional:
extends: .php
stage: test
variables:
PHP_VERSION: "8.2"
script:
- composer ci:test:php:functional
functional:mysql:
extends: .functional
variables:
typo3DatabaseDriver: "mysqli"
typo3DatabaseHost: "mysql"
typo3DatabaseName: "typo3_test"
typo3DatabaseUsername: "root"
typo3DatabasePassword: "root"
services:
- mysql:8.0
functional:postgres:
extends: .functional
variables:
typo3DatabaseDriver: "pdo_pgsql"
typo3DatabaseHost: "postgres"
typo3DatabaseName: "typo3_test"
typo3DatabaseUsername: "postgres"
typo3DatabasePassword: "postgres"
services:
- postgres:15
before_script:
- apt-get update && apt-get install -y libpq-dev
- docker-php-ext-install pdo_pgsql
functional:sqlite:
extends: .functional
variables:
typo3DatabaseDriver: "pdo_sqlite"
```
## Best Practices
### 1. Fast Feedback Loop
Order jobs by execution time (fastest first):
```yaml
stages:
- lint # ~30 seconds
- analyze # ~1-2 minutes (PHPStan, CGL)
- unit # ~2-5 minutes
- functional # ~5-15 minutes
- acceptance # ~15-30 minutes
```
### 2. Fail Fast
```yaml
strategy:
fail-fast: true # Stop on first failure
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
```
### 3. Parallel Execution
```yaml
# GitHub Actions - parallel jobs
jobs:
lint: ...
phpstan: ...
unit: ...
# All run in parallel
# GitLab CI - parallel jobs
test:
parallel:
matrix:
- PHP_VERSION: ['8.1', '8.2', '8.3']
```
### 4. Cache Dependencies
GitHub Actions:
```yaml
- uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
```
GitLab CI:
```yaml
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .composer-cache/
```
### 5. Matrix Testing
Test critical combinations:
```yaml
strategy:
matrix:
include:
# Minimum supported versions
- php: '8.1'
typo3: '12.4'
# Current stable
- php: '8.2'
typo3: '12.4'
# Latest versions
- php: '8.3'
typo3: '13.0'
```
### 6. Artifacts and Reports
```yaml
- name: Archive test results
if: failure()
uses: actions/upload-artifact@v3
with:
name: test-results
path: |
var/log/
typo3temp/var/tests/
```
### 7. Notifications
GitHub Actions:
```yaml
- name: Slack Notification
if: failure()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
```
## Quality Gates
### Required Checks
Define which checks must pass:
GitHub:
```yaml
# .github/branch-protection.json
{
"required_status_checks": {
"strict": true,
"contexts": [
"lint",
"phpstan",
"unit (8.2)",
"functional (8.2, mysqli)"
]
}
}
```
GitLab:
```yaml
# .gitlab-ci.yml
unit:8.2:
only:
- merge_requests
allow_failure: false # Required check
```
### Coverage Requirements
```yaml
- name: Check code coverage
run: |
coverage=$(vendor/bin/phpunit --coverage-text | grep "Lines:" | awk '{print $2}' | sed 's/%//')
if (( $(echo "$coverage < 80" | bc -l) )); then
echo "Coverage $coverage% is below 80%"
exit 1
fi
```
## Environment-Specific Configuration
### Development Branch
```yaml
on:
push:
branches: [ develop ]
# Run all checks, allow failures
jobs:
experimental:
continue-on-error: true
strategy:
matrix:
php: ['8.4'] # Experimental PHP version
```
### Production Branch
```yaml
on:
push:
branches: [ main ]
# Strict checks only
jobs:
tests:
strategy:
fail-fast: true
matrix:
php: ['8.2'] # LTS version only
```
### Pull Requests
```yaml
on:
pull_request:
# Full test matrix
jobs:
tests:
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
database: ['mysqli', 'postgres']
```
## Resources
- [GitHub Actions Documentation](https://docs.github.com/actions)
- [GitLab CI Documentation](https://docs.gitlab.com/ee/ci/)
- [TYPO3 Tea Extension CI](https://github.com/TYPO3BestPractices/tea/tree/main/.github/workflows)
- [shivammathur/setup-php](https://github.com/shivammathur/setup-php)

View File

@@ -0,0 +1,720 @@
# Functional Testing in TYPO3
Functional tests verify components that interact with external systems like databases, using a full TYPO3 instance.
## When to Use Functional Tests
- Testing database operations (repositories, queries)
- Controller and plugin functionality
- Hook and event implementations
- DataHandler operations
- File and folder operations
- Extension configuration behavior
## Base Class
All functional tests extend `TYPO3\TestingFramework\Core\Functional\FunctionalTestCase`:
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Functional\Domain\Repository;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\Extension\Domain\Model\Product;
use Vendor\Extension\Domain\Repository\ProductRepository;
final class ProductRepositoryTest extends FunctionalTestCase
{
protected ProductRepository $subject;
protected array $testExtensionsToLoad = [
'typo3conf/ext/my_extension',
];
protected function setUp(): void
{
parent::setUp();
$this->subject = $this->get(ProductRepository::class);
}
/**
* @test
*/
public function findsProductsByCategory(): void
{
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
$products = $this->subject->findByCategory(1);
self::assertCount(3, $products);
}
}
```
## Test Database
Functional tests use an isolated test database:
- Created before test execution
- Populated with fixtures
- Destroyed after test completion
- Supports: MySQL, MariaDB, PostgreSQL, SQLite
### Database Configuration
Set via environment or `FunctionalTests.xml`:
```xml
<php>
<env name="typo3DatabaseDriver" value="mysqli"/>
<env name="typo3DatabaseHost" value="localhost"/>
<env name="typo3DatabasePort" value="3306"/>
<env name="typo3DatabaseUsername" value="root"/>
<env name="typo3DatabasePassword" value=""/>
<env name="typo3DatabaseName" value="typo3_test"/>
</php>
```
## Database Fixtures
### CSV Format
Create fixtures in `Tests/Functional/Fixtures/`:
```csv
# pages.csv
uid,pid,title,doktype
1,0,"Root",1
2,1,"Products",1
3,1,"Services",1
```
```csv
# tx_myext_domain_model_product.csv
uid,pid,title,price,category
1,2,"Product A",10.00,1
2,2,"Product B",20.00,1
3,2,"Product C",15.00,2
```
### Import Fixtures
```php
/**
* @test
*/
public function findsProducts(): void
{
// Import fixture
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
// Test repository
$products = $this->subject->findAll();
self::assertCount(3, $products);
}
```
### Multiple Fixtures
```php
protected function setUp(): void
{
parent::setUp();
// Import common fixtures
$this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv');
$this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
$this->subject = $this->get(ProductRepository::class);
}
```
## Dependency Injection
Use `$this->get()` to retrieve services:
```php
protected function setUp(): void
{
parent::setUp();
// Get service from container
$this->subject = $this->get(ProductRepository::class);
$this->dataMapper = $this->get(DataMapper::class);
}
```
## Testing Extensions
### Load Test Extensions
```php
protected array $testExtensionsToLoad = [
'typo3conf/ext/my_extension',
'typo3conf/ext/dependency_extension',
];
```
### Core Extensions
```php
protected array $coreExtensionsToLoad = [
'form',
'workspaces',
];
```
## Site Configuration
Create site configuration for frontend tests:
```php
protected function setUp(): void
{
parent::setUp();
$this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv');
$this->writeSiteConfiguration(
'test',
[
'rootPageId' => 1,
'base' => 'http://localhost/',
]
);
}
```
## Frontend Requests
Test frontend rendering:
```php
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
/**
* @test
*/
public function rendersProductList(): void
{
$this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv');
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
$this->writeSiteConfiguration('test', ['rootPageId' => 1]);
$response = $this->executeFrontendSubRequest(
new InternalRequest('http://localhost/products')
);
self::assertStringContainsString('Product A', (string)$response->getBody());
}
```
## Testing DataHandler Hooks (SC_OPTIONS)
Test DataHandler SC_OPTIONS hook integration with real framework:
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Functional\Database;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\Extension\Database\MyDataHandlerHook;
final class MyDataHandlerHookTest extends FunctionalTestCase
{
protected array $testExtensionsToLoad = [
'typo3conf/ext/my_extension',
];
protected array $coreExtensionsToLoad = [
'typo3/cms-rte-ckeditor', // If testing RTE-related hooks
];
protected function setUp(): void
{
parent::setUp();
$this->importCSVDataSet(__DIR__ . '/Fixtures/pages.csv');
$this->importCSVDataSet(__DIR__ . '/Fixtures/tt_content.csv');
}
private function createSubject(): MyDataHandlerHook
{
// Get services from container with proper DI
return new MyDataHandlerHook(
$this->get(ExtensionConfiguration::class),
$this->get(LogManager::class),
$this->get(ResourceFactory::class),
);
}
/**
* @test
*/
public function processDatamapPostProcessFieldArrayHandlesRteField(): void
{
$subject = $this->createSubject();
$status = 'update';
$table = 'tt_content';
$id = '1';
$fieldArray = [
'bodytext' => '<p>Test content with <img src="image.jpg" /></p>',
];
/** @var DataHandler $dataHandler */
$dataHandler = $this->get(DataHandler::class);
// Configure TCA for RTE field
/** @var array<string, mixed> $tcaConfig */
$tcaConfig = [
'type' => 'text',
'enableRichtext' => true,
];
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible
$GLOBALS['TCA']['tt_content']['columns']['bodytext']['config'] = $tcaConfig;
$subject->processDatamap_postProcessFieldArray(
$status,
$table,
$id,
$fieldArray,
$dataHandler,
);
// Field should be processed by hook
self::assertArrayHasKey('bodytext', $fieldArray);
self::assertIsString($fieldArray['bodytext']);
self::assertNotEmpty($fieldArray['bodytext']);
self::assertStringContainsString('Test content', $fieldArray['bodytext']);
}
/**
* @test
*/
public function hookIsRegisteredInGlobals(): void
{
// Verify hook is properly registered in TYPO3_CONF_VARS
self::assertIsArray($GLOBALS['TYPO3_CONF_VARS']);
self::assertArrayHasKey('SC_OPTIONS', $GLOBALS['TYPO3_CONF_VARS']);
$scOptions = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'];
self::assertIsArray($scOptions);
self::assertArrayHasKey('t3lib/class.t3lib_tcemain.php', $scOptions);
$tcemainOptions = $scOptions['t3lib/class.t3lib_tcemain.php'];
self::assertIsArray($tcemainOptions);
self::assertArrayHasKey('processDatamapClass', $tcemainOptions);
$registeredHooks = $tcemainOptions['processDatamapClass'];
self::assertIsArray($registeredHooks);
// Hook class should be registered
self::assertContains(MyDataHandlerHook::class, $registeredHooks);
}
}
```
### Key Patterns for DataHandler Hook Testing
1. **Use Factory Method Pattern**: Create `createSubject()` method to avoid uninitialized property PHPStan errors
2. **Test Real Framework Integration**: Don't mock DataHandler, test actual hook execution
3. **Configure TCA Dynamically**: Set up `$GLOBALS['TCA']` in tests for field configuration
4. **Verify Hook Registration**: Test that hooks are properly registered in `$GLOBALS['TYPO3_CONF_VARS']`
5. **Test Multiple Scenarios**: new vs update, single vs multiple fields, RTE vs non-RTE
## Testing File Abstraction Layer (FAL)
Test ResourceFactory and FAL storage integration:
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Functional\Controller;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\Extension\Controller\ImageRenderingController;
final class ImageRenderingControllerTest extends FunctionalTestCase
{
protected array $testExtensionsToLoad = [
'typo3conf/ext/my_extension',
];
protected function setUp(): void
{
parent::setUp();
$this->importCSVDataSet(__DIR__ . '/Fixtures/sys_file_storage.csv');
$this->importCSVDataSet(__DIR__ . '/Fixtures/sys_file.csv');
}
/**
* @test
*/
public function storageIsAccessible(): void
{
/** @var ResourceFactory $resourceFactory */
$resourceFactory = $this->get(ResourceFactory::class);
$storage = $resourceFactory->getStorageObject(1);
self::assertInstanceOf(ResourceStorage::class, $storage);
self::assertTrue($storage->isOnline());
}
/**
* @test
*/
public function canRetrieveFileFromStorage(): void
{
/** @var ResourceFactory $resourceFactory */
$resourceFactory = $this->get(ResourceFactory::class);
// Get file from test data
$file = $resourceFactory->getFileObject(1);
self::assertInstanceOf(File::class, $file);
self::assertSame('test-image.jpg', $file->getName());
}
/**
* @test
*/
public function canAccessStorageRootFolder(): void
{
/** @var ResourceFactory $resourceFactory */
$resourceFactory = $this->get(ResourceFactory::class);
$storage = $resourceFactory->getStorageObject(1);
$rootFolder = $storage->getRootLevelFolder();
self::assertInstanceOf(Folder::class, $rootFolder);
self::assertSame('/', $rootFolder->getIdentifier());
}
}
```
### FAL Test Fixtures
**sys_file_storage.csv:**
```csv
uid,pid,name,driver,configuration,is_default,is_browsable,is_public,is_writable,is_online
1,0,"fileadmin","Local","<?xml version=""1.0"" encoding=""utf-8"" standalone=""yes"" ?><T3FlexForms><data><sheet index=""sDEF""><language index=""lDEF""><field index=""basePath""><value index=""vDEF"">fileadmin/</value></field><field index=""pathType""><value index=""vDEF"">relative</value></field><field index=""caseSensitive""><value index=""vDEF"">1</value></field></language></sheet></data></T3FlexForms>",1,1,1,1,1
```
**sys_file.csv:**
```csv
uid,pid,storage,identifier,name,type,mime_type,size,sha1,extension
1,0,1,"/test-image.jpg","test-image.jpg",2,"image/jpeg",12345,"da39a3ee5e6b4b0d3255bfef95601890afd80709","jpg"
```
### Key Patterns for FAL Testing
1. **Test Storage Configuration**: Verify storage is properly configured and online
2. **Test File Retrieval**: Use `getFileObject()` to retrieve files from sys_file
3. **Test Folder Operations**: Verify folder access and structure
4. **Use CSV Fixtures**: Import sys_file_storage and sys_file test data
5. **Test Real Services**: Use container's ResourceFactory, don't mock
## PHPStan Type Safety in Functional Tests
### Handling $GLOBALS['TCA'] with PHPStan Level 9
PHPStan cannot infer types for runtime-configured `$GLOBALS` arrays. Use ignore annotations:
```php
// Configure TCA for RTE field
/** @var array<string, mixed> $tcaConfig */
$tcaConfig = [
'type' => 'text',
'enableRichtext' => true,
];
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible
$GLOBALS['TCA']['tt_content']['columns']['bodytext']['config'] = $tcaConfig;
```
### Type Assertions for Dynamic Arrays
When testing field arrays that are modified by reference:
```php
// ❌ PHPStan cannot verify this is still an array
self::assertStringContainsString('Test', $fieldArray['bodytext']);
// ✅ Add type assertions
self::assertArrayHasKey('bodytext', $fieldArray);
self::assertIsString($fieldArray['bodytext']);
self::assertStringContainsString('Test', $fieldArray['bodytext']);
```
### Avoiding Uninitialized Property Errors
Use factory methods instead of properties initialized in setUp():
```php
// ❌ PHPStan warns about uninitialized property
private MyService $subject;
protected function setUp(): void
{
$this->subject = $this->get(MyService::class);
}
// ✅ Use factory method
private function createSubject(): MyService
{
return $this->get(MyService::class);
}
public function testSomething(): void
{
$subject = $this->createSubject();
// Use $subject
}
```
### PHPStan Annotations for Functional Tests
Common patterns:
```php
// Ignore $GLOBALS access
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible
$GLOBALS['TCA']['table']['columns']['field']['config'] = $config;
// Type hint service retrieval
/** @var DataHandler $dataHandler */
$dataHandler = $this->get(DataHandler::class);
// Type hint config arrays
/** @var array<string, mixed> $tcaConfig */
$tcaConfig = ['type' => 'text'];
```
## Backend User Context
Test with backend user:
```php
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
/**
* @test
*/
public function editorCanEditRecord(): void
{
$this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
$this->setUpBackendUser(1); // uid from be_users.csv
$dataHandler = $this->get(DataHandler::class);
$dataHandler->start(
[
'tx_myext_domain_model_product' => [
1 => ['title' => 'Updated Product']
]
],
[]
);
$dataHandler->process_datamap();
self::assertEmpty($dataHandler->errorLog);
}
```
## File Operations
Test file handling:
```php
/**
* @test
*/
public function uploadsFile(): void
{
$fileStorage = $this->get(StorageRepository::class)->getDefaultStorage();
$file = $fileStorage->addFile(
__DIR__ . '/../Fixtures/Files/test.jpg',
$fileStorage->getDefaultFolder(),
'test.jpg'
);
self::assertFileExists($file->getForLocalProcessing(false));
}
```
## Configuration
### PHPUnit XML (Build/phpunit/FunctionalTests.xml)
```xml
<phpunit
bootstrap="FunctionalTestsBootstrap.php"
cacheResult="false"
beStrictAboutTestsThatDoNotTestAnything="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true">
<testsuites>
<testsuite name="Functional tests">
<directory>../../Tests/Functional/</directory>
</testsuite>
</testsuites>
<php>
<const name="TYPO3_TESTING_FUNCTIONAL_REMOVE_ERROR_HANDLER" value="true" />
<env name="TYPO3_CONTEXT" value="Testing"/>
<env name="typo3DatabaseDriver" value="mysqli"/>
</php>
</phpunit>
```
### Bootstrap (Build/phpunit/FunctionalTestsBootstrap.php)
```php
<?php
declare(strict_types=1);
call_user_func(static function () {
$testbase = new \TYPO3\TestingFramework\Core\Testbase();
$testbase->defineOriginalRootPath();
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests');
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient');
});
```
## Fixture Strategy
### Minimal Fixtures
Keep fixtures focused on test requirements:
```php
// ❌ Too much data
$this->importCSVDataSet(__DIR__ . '/../Fixtures/AllProducts.csv'); // 500 records
// ✅ Minimal test data
$this->importCSVDataSet(__DIR__ . '/../Fixtures/ProductsByCategory.csv'); // 3 records
```
### Reusable Fixtures
Create shared fixtures for common scenarios:
```
Tests/Functional/Fixtures/
├── pages.csv # Basic page tree
├── be_users.csv # Test backend users
├── Products/
│ ├── BasicProducts.csv # 3 simple products
│ ├── ProductsWithCategories.csv
│ └── ProductsWithImages.csv
```
### Fixture Documentation
Document fixture purpose in test or AGENTS.md:
```php
/**
* @test
*/
public function findsProductsByCategory(): void
{
// Fixture contains: 3 products in category 1, 2 products in category 2
$this->importCSVDataSet(__DIR__ . '/../Fixtures/ProductsByCategory.csv');
$products = $this->subject->findByCategory(1);
self::assertCount(3, $products);
}
```
## Best Practices
1. **Use setUp() for Common Setup**: Import shared fixtures in setUp()
2. **One Test Database**: Each test gets clean database instance
3. **Test Isolation**: Don't depend on other test execution
4. **Minimal Fixtures**: Only data required for specific test
5. **Clear Assertions**: Test specific behavior, not implementation
6. **Cleanup**: Testing framework handles cleanup automatically
## Common Pitfalls
**Large Fixtures**
```php
// Don't import unnecessary data
$this->importCSVDataSet('AllData.csv'); // 10,000 records
```
**No Fixtures**
```php
// Don't expect data to exist
$products = $this->subject->findAll();
self::assertCount(0, $products); // Always true without fixtures
```
**Missing Extensions**
```php
// Don't forget to load extension under test
// Missing: protected array $testExtensionsToLoad = ['typo3conf/ext/my_extension'];
```
**Focused, Well-Documented Tests**
```php
/**
* @test
*/
public function findsByCategory(): void
{
// Fixture: 3 products in category 1
$this->importCSVDataSet(__DIR__ . '/../Fixtures/CategoryProducts.csv');
$products = $this->subject->findByCategory(1);
self::assertCount(3, $products);
self::assertSame('Product A', $products[0]->getTitle());
}
```
## Running Functional Tests
```bash
# Via runTests.sh
Build/Scripts/runTests.sh -s functional
# Via PHPUnit directly
vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml
# Via Composer
composer ci:test:php:functional
# With specific database
typo3DatabaseDriver=pdo_mysql vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml
# Single test
vendor/bin/phpunit Tests/Functional/Domain/Repository/ProductRepositoryTest.php
```
## Resources
- [TYPO3 Functional Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/FunctionalTests.html)
- [Testing Framework](https://github.com/typo3/testing-framework)
- [CSV Fixture Format](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/FunctionalTests.html#importing-data)

View File

@@ -0,0 +1,499 @@
# JavaScript and CKEditor Testing
**Purpose:** Testing patterns for TYPO3 CKEditor plugins, JavaScript functionality, and frontend code
## Overview
While TYPO3 extensions are primarily PHP, many include JavaScript for:
- CKEditor custom plugins and features
- Backend module interactions
- Frontend enhancements
- RTE (Rich Text Editor) extensions
This guide covers testing patterns for JavaScript code in TYPO3 extensions.
## CKEditor Plugin Testing
### Testing Model Attributes
CKEditor plugins define model attributes that must be properly handled through upcast (view→model) and downcast (model→view) conversions.
**Example from t3x-rte_ckeditor_image:**
The plugin added a `noScale` attribute to prevent image processing. This requires testing:
1. **Attribute schema registration**
2. **Upcast conversion** (HTML → CKEditor model)
3. **Downcast conversion** (CKEditor model → HTML)
4. **UI interaction** (dialog checkbox)
### Test Structure Pattern
```javascript
// Resources/Public/JavaScript/Plugins/__tests__/typo3image.test.js
import { typo3image } from '../typo3image';
describe('TYPO3 Image Plugin', () => {
let editor;
beforeEach(async () => {
editor = await createTestEditor();
});
afterEach(() => {
return editor.destroy();
});
describe('Model Schema', () => {
it('should allow noScale attribute', () => {
const schema = editor.model.schema;
expect(schema.checkAttribute('typo3image', 'noScale')).toBe(true);
});
});
describe('Upcast Conversion', () => {
it('should read data-noscale from HTML', () => {
const html = '<img src="test.jpg" data-noscale="true" />';
editor.setData(html);
const imageElement = editor.model.document.getRoot()
.getChild(0);
expect(imageElement.getAttribute('noScale')).toBe(true);
});
it('should handle missing data-noscale attribute', () => {
const html = '<img src="test.jpg" />';
editor.setData(html);
const imageElement = editor.model.document.getRoot()
.getChild(0);
expect(imageElement.getAttribute('noScale')).toBe(false);
});
});
describe('Downcast Conversion', () => {
it('should write data-noscale to HTML when enabled', () => {
editor.model.change(writer => {
const imageElement = writer.createElement('typo3image', {
src: 'test.jpg',
noScale: true
});
writer.insert(imageElement, editor.model.document.getRoot(), 0);
});
const html = editor.getData();
expect(html).toContain('data-noscale="true"');
});
it('should omit data-noscale when disabled', () => {
editor.model.change(writer => {
const imageElement = writer.createElement('typo3image', {
src: 'test.jpg',
noScale: false
});
writer.insert(imageElement, editor.model.document.getRoot(), 0);
});
const html = editor.getData();
expect(html).not.toContain('data-noscale');
});
});
});
```
### Testing data-* Attributes
Many TYPO3 CKEditor plugins use `data-*` attributes to pass information from editor to server-side rendering.
**Common Patterns:**
```javascript
describe('data-* Attribute Handling', () => {
it('should preserve TYPO3-specific attributes', () => {
const testCases = [
{ attr: 'data-htmlarea-file-uid', value: '123' },
{ attr: 'data-htmlarea-file-table', value: 'sys_file' },
{ attr: 'data-htmlarea-zoom', value: 'true' },
{ attr: 'data-noscale', value: 'true' },
{ attr: 'data-alt-override', value: 'false' },
{ attr: 'data-title-override', value: 'true' }
];
testCases.forEach(({ attr, value }) => {
const html = `<img src="test.jpg" ${attr}="${value}" />`;
editor.setData(html);
// Verify upcast preserves attribute
const output = editor.getData();
expect(output).toContain(`${attr}="${value}"`);
});
});
it('should handle boolean data attributes', () => {
// Test true value
editor.setData('<img src="test.jpg" data-noscale="true" />');
let imageElement = editor.model.document.getRoot().getChild(0);
expect(imageElement.getAttribute('noScale')).toBe(true);
// Test false value
editor.setData('<img src="test.jpg" data-noscale="false" />');
imageElement = editor.model.document.getRoot().getChild(0);
expect(imageElement.getAttribute('noScale')).toBe(false);
// Test missing attribute
editor.setData('<img src="test.jpg" />');
imageElement = editor.model.document.getRoot().getChild(0);
expect(imageElement.getAttribute('noScale')).toBe(false);
});
});
```
### Testing Dialog UI
CKEditor dialogs require testing user interactions:
```javascript
describe('Image Dialog', () => {
let dialog, $checkbox;
beforeEach(() => {
dialog = createImageDialog(editor);
$checkbox = dialog.$el.find('#checkbox-noscale');
});
it('should display noScale checkbox', () => {
expect($checkbox.length).toBe(1);
expect($checkbox.parent('label').text())
.toContain('Use original file (noScale)');
});
it('should set noScale attribute when checkbox checked', () => {
$checkbox.prop('checked', true);
dialog.save();
const imageElement = getSelectedImage(editor);
expect(imageElement.getAttribute('noScale')).toBe(true);
});
it('should remove noScale attribute when checkbox unchecked', () => {
// Start with noScale enabled
const imageElement = getSelectedImage(editor);
editor.model.change(writer => {
writer.setAttribute('noScale', true, imageElement);
});
// Uncheck and save
$checkbox.prop('checked', false);
dialog.save();
expect(imageElement.getAttribute('noScale')).toBe(false);
});
it('should load checkbox state from existing attribute', () => {
const imageElement = getSelectedImage(editor);
editor.model.change(writer => {
writer.setAttribute('noScale', true, imageElement);
});
dialog = createImageDialog(editor);
$checkbox = dialog.$el.find('#checkbox-noscale');
expect($checkbox.prop('checked')).toBe(true);
});
});
```
## JavaScript Test Frameworks
### Jest (Recommended)
**Installation:**
```bash
npm install --save-dev jest @babel/preset-env
```
**Configuration (jest.config.js):**
```javascript
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.js$': 'babel-jest'
},
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy'
},
collectCoverageFrom: [
'Resources/Public/JavaScript/**/*.js',
'!Resources/Public/JavaScript/**/*.test.js',
'!Resources/Public/JavaScript/**/__tests__/**'
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
}
};
```
### Mocha + Chai
Alternative for projects already using Mocha:
```javascript
// test/javascript/typo3image.test.js
const { expect } = require('chai');
const { JSDOM } = require('jsdom');
describe('TYPO3 Image Plugin', function() {
let editor;
beforeEach(async function() {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
global.window = dom.window;
global.document = window.document;
editor = await createTestEditor();
});
it('should handle noScale attribute', function() {
// Test implementation
});
});
```
## Testing Best Practices
### 1. Isolate Editor Instance
Each test should use a fresh editor instance:
```javascript
async function createTestEditor() {
const div = document.createElement('div');
document.body.appendChild(div);
const editor = await ClassicEditor.create(div, {
plugins: [Typo3Image, /* other plugins */],
typo3image: {
/* plugin config */
}
});
return editor;
}
```
### 2. Clean Up After Tests
Prevent memory leaks and DOM pollution:
```javascript
afterEach(async () => {
if (editor) {
await editor.destroy();
editor = null;
}
// Clean up any test DOM elements
document.body.innerHTML = '';
});
```
### 3. Test Both Happy Path and Edge Cases
```javascript
describe('Attribute Validation', () => {
it('should handle valid boolean values', () => {
// Happy path
});
it('should handle invalid attribute values', () => {
const html = '<img src="test.jpg" data-noscale="invalid" />';
editor.setData(html);
// Should default to false
const imageElement = editor.model.document.getRoot().getChild(0);
expect(imageElement.getAttribute('noScale')).toBe(false);
});
it('should handle malformed HTML', () => {
const html = '<img src="test.jpg" data-noscale>'; // Missing value
// Test graceful handling
});
});
```
### 4. Mock Backend Interactions
For plugins that communicate with TYPO3 backend:
```javascript
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
});
afterEach(() => {
global.fetch.mockRestore();
});
it('should fetch image metadata from backend', async () => {
await plugin.fetchImageMetadata(123);
expect(fetch).toHaveBeenCalledWith(
'/typo3/ajax/image/metadata/123',
expect.any(Object)
);
});
```
## Integration with PHP Tests
JavaScript tests complement PHP unit tests:
**PHP Side (Backend):**
```php
// Tests/Unit/Controller/ImageRenderingControllerTest.php
public function testNoScaleAttribute(): void
{
$attributes = ['data-noscale' => 'true'];
$result = $this->controller->render($attributes);
// Verify noScale parameter passed to imgResource
$this->assertStringContainsString('noScale=1', $result);
}
```
**JavaScript Side (Frontend):**
```javascript
// Resources/Public/JavaScript/__tests__/typo3image.test.js
it('should generate data-noscale attribute', () => {
// Verify attribute is created in editor
editor.model.change(writer => {
const img = writer.createElement('typo3image', {
noScale: true
});
writer.insert(img, editor.model.document.getRoot(), 0);
});
expect(editor.getData()).toContain('data-noscale="true"');
});
```
**Together:** These tests ensure end-to-end functionality from editor UI → HTML attribute → PHP backend processing.
## CI/CD Integration
Add JavaScript tests to your CI pipeline:
**package.json:**
```json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch"
},
"devDependencies": {
"jest": "^29.0.0",
"@babel/preset-env": "^7.20.0"
}
}
```
**GitHub Actions:**
```yaml
- name: Run JavaScript tests
run: |
npm install
npm run test:coverage
- name: Upload JS coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: javascript
```
## Example: Complete Test Suite
See `t3x-rte_ckeditor_image` for a real-world example:
```
t3x-rte_ckeditor_image/
├── Resources/Public/JavaScript/
│ └── Plugins/
│ ├── typo3image.js # Main plugin
│ └── __tests__/
│ └── typo3image.test.js # JavaScript tests
└── Tests/Unit/
└── Controller/
└── ImageRenderingControllerTest.php # PHP tests
```
**Key Lessons:**
1. Test attribute schema registration
2. Test upcast/downcast conversions separately
3. Test UI interactions (checkboxes, inputs)
4. Test data-* attribute preservation
5. Clean up editor instances to prevent leaks
6. Mock backend API calls
7. Coordinate with PHP tests for full coverage
## Troubleshooting
### Tests Pass Locally but Fail in CI
**Cause:** DOM environment differences
**Solution:**
```javascript
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
testEnvironmentOptions: {
url: 'http://localhost'
}
};
```
### Memory Leaks in Test Suite
**Cause:** Editor instances not properly destroyed
**Solution:**
```javascript
afterEach(async () => {
if (editor && !editor.state === 'destroyed') {
await editor.destroy();
}
editor = null;
});
```
### Async Test Failures
**Cause:** Not waiting for editor initialization
**Solution:**
```javascript
beforeEach(async () => {
editor = await ClassicEditor.create(/* ... */);
// ☝️ await is critical
});
```
## References
- [CKEditor 5 Testing](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/testing-environment.html)
- [Jest Documentation](https://jestjs.io/docs/getting-started)
- [TYPO3 RTE CKEditor Image](https://github.com/netresearch/t3x-rte_ckeditor_image)

395
references/quality-tools.md Normal file
View File

@@ -0,0 +1,395 @@
# Quality Tools for TYPO3 Development
Automated code quality and static analysis tools for TYPO3 extensions.
## Overview
- **PHPStan**: Static analysis for type safety and bugs
- **Rector**: Automated code refactoring and modernization
- **php-cs-fixer**: Code style enforcement (PSR-12, TYPO3 CGL)
- **phplint**: PHP syntax validation
## PHPStan
### Installation
```bash
composer require --dev phpstan/phpstan phpstan/phpstan-strict-rules saschaegerer/phpstan-typo3
```
### Configuration
Create `Build/phpstan.neon`:
```neon
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/saschaegerer/phpstan-typo3/extension.neon
parameters:
level: max # Level 10 - maximum strictness
paths:
- Classes
- Tests
excludePaths:
- Tests/Acceptance/_output/*
reportUnmatchedIgnoredErrors: true
checkGenericClassInNonGenericObjectType: false
checkMissingIterableValueType: false
```
### Running PHPStan
```bash
# Via runTests.sh
Build/Scripts/runTests.sh -s phpstan
# Directly
vendor/bin/phpstan analyze --configuration Build/phpstan.neon
# With baseline (ignore existing errors)
vendor/bin/phpstan analyze --generate-baseline Build/phpstan-baseline.neon
# Clear cache
vendor/bin/phpstan clear-result-cache
```
### PHPStan Rule Levels
**Level 0-10** (use `max` for level 10): Increasing strictness
- **Level 0**: Basic checks (undefined variables, unknown functions)
- **Level 5**: Type checks, unknown properties, unknown methods
- **Level 9**: Strict mixed types, unused parameters
- **Level 10 (max)**: Maximum strictness - explicit mixed types, pure functions
**Recommendation**: Start with level 5, aim for level 10 (max) in modern TYPO3 13 projects.
**Why Level 10?**
- Enforces explicit type declarations (`mixed` must be declared, not implicit)
- Catches more potential bugs at development time
- Aligns with TYPO3 13 strict typing standards (`declare(strict_types=1)`)
- Required for PHPStan Level 10 compliant extensions
### Ignoring Errors
```php
/** @phpstan-ignore-next-line */
$value = $this->legacyMethod();
// Or in neon file
parameters:
ignoreErrors:
- '#Call to an undefined method.*::getRepository\(\)#'
```
### TYPO3-Specific Rules
```php
// PHPStan understands TYPO3 classes
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages');
// ✅ PHPStan knows this returns QueryBuilder
// Detects TYPO3 API misuse
TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(MyService::class);
// ✅ Checks if MyService is a valid class
```
## Rector
### Installation
```bash
composer require --dev rector/rector ssch/typo3-rector
```
### Configuration
Create `rector.php`:
```php
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Ssch\TYPO3Rector\Set\Typo3SetList;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/Classes',
__DIR__ . '/Tests',
])
->withSkip([
__DIR__ . '/Tests/Acceptance/_output',
])
->withPhpSets(php82: true)
->withSets([
LevelSetList::UP_TO_PHP_82,
SetList::CODE_QUALITY,
SetList::DEAD_CODE,
SetList::TYPE_DECLARATION,
Typo3SetList::TYPO3_13,
]);
```
### Running Rector
```bash
# Dry run (show changes)
vendor/bin/rector process --dry-run
# Apply changes
vendor/bin/rector process
# Via runTests.sh
Build/Scripts/runTests.sh -s rector
```
### Common Refactorings
**TYPO3 API Modernization**:
```php
// Before
$GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'pages', 'uid=1');
// After (Rector auto-refactors)
GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('pages')
->select(['*'], 'pages', ['uid' => 1])
->fetchAllAssociative();
```
**Type Declarations**:
```php
// Before
public function process($data)
{
return $data;
}
// After
public function process(array $data): array
{
return $data;
}
```
## php-cs-fixer
### Installation
```bash
composer require --dev friendsofphp/php-cs-fixer
```
### Configuration
Create `Build/php-cs-fixer.php`:
```php
<?php
declare(strict_types=1);
$finder = (new PhpCsFixer\Finder())
->in(__DIR__ . '/../Classes')
->in(__DIR__ . '/../Tests')
->exclude('_output');
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'@PhpCsFixer' => true,
'array_syntax' => ['syntax' => 'short'],
'concat_space' => ['spacing' => 'one'],
'declare_strict_types' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'single_line_throw' => false,
'phpdoc_align' => false,
'phpdoc_no_empty_return' => false,
'phpdoc_summary' => false,
])
->setRiskyAllowed(true)
->setFinder($finder);
```
### Running php-cs-fixer
```bash
# Check only (dry run)
vendor/bin/php-cs-fixer fix --config Build/php-cs-fixer.php --dry-run --diff
# Fix files
vendor/bin/php-cs-fixer fix --config Build/php-cs-fixer.php
# Via runTests.sh
Build/Scripts/runTests.sh -s cgl
```
### Common Rules
```php
// array_syntax: short
$array = [1, 2, 3]; // ✅
$array = array(1, 2, 3); // ❌
// concat_space: one
$message = 'Hello ' . $name; // ✅
$message = 'Hello '.$name; // ❌
// declare_strict_types
<?php
declare(strict_types=1); // ✅ Required at top of file
// ordered_imports
use Vendor\Extension\Domain\Model\Product; // ✅ Alphabetical
use Vendor\Extension\Domain\Repository\ProductRepository;
```
## phplint
### Installation
```bash
composer require --dev overtrue/phplint
```
### Configuration
Create `.phplint.yml`:
```yaml
path: ./
jobs: 10
cache: var/cache/phplint.cache
exclude:
- vendor
- var
- .Build
extensions:
- php
```
### Running phplint
```bash
# Lint all PHP files
vendor/bin/phplint
# Via runTests.sh
Build/Scripts/runTests.sh -s lint
# Specific directory
vendor/bin/phplint Classes/
```
## Composer Script Integration
```json
{
"scripts": {
"ci:test:php:lint": "phplint",
"ci:test:php:phpstan": "phpstan analyze --configuration Build/phpstan.neon --no-progress",
"ci:test:php:rector": "rector process --dry-run",
"ci:test:php:cgl": "php-cs-fixer fix --config Build/php-cs-fixer.php --dry-run --diff",
"ci:test:php:security": "composer audit",
"fix:cgl": "php-cs-fixer fix --config Build/php-cs-fixer.php",
"fix:rector": "rector process",
"ci:test": [
"@ci:test:php:lint",
"@ci:test:php:phpstan",
"@ci:test:php:rector",
"@ci:test:php:cgl",
"@ci:test:php:security"
]
}
}
```
> **Security Note**: `composer audit` checks for known security vulnerabilities in dependencies. Run this regularly and especially before releases.
## Pre-commit Hook
Create `.git/hooks/pre-commit`:
```bash
#!/bin/sh
echo "Running quality checks..."
# Lint
vendor/bin/phplint || exit 1
# PHPStan
vendor/bin/phpstan analyze --configuration Build/phpstan.neon --error-format=table --no-progress || exit 1
# Code style
vendor/bin/php-cs-fixer fix --config Build/php-cs-fixer.php --dry-run --diff || exit 1
echo "✓ All checks passed"
```
## CI/CD Integration
### GitHub Actions
```yaml
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4' # Use latest PHP for quality tools
- run: composer install
- run: composer ci:test:php:lint
- run: composer ci:test:php:phpstan
- run: composer ci:test:php:cgl
- run: composer ci:test:php:rector
- run: composer ci:test:php:security
```
## IDE Integration
### PHPStorm
1. **PHPStan**: Settings → PHP → Quality Tools → PHPStan
2. **php-cs-fixer**: Settings → PHP → Quality Tools → PHP CS Fixer
3. **File Watchers**: Auto-run on file save
### VS Code
```json
{
"php.validate.executablePath": "/usr/bin/php",
"phpstan.enabled": true,
"phpstan.configFile": "Build/phpstan.neon",
"php-cs-fixer.onsave": true,
"php-cs-fixer.config": "Build/php-cs-fixer.php"
}
```
## Best Practices
1. **PHPStan Level 10**: Aim for `level: max` in modern TYPO3 13 projects
2. **Baseline for Legacy**: Use baselines to track existing issues during migration
3. **Security Audits**: Run `composer audit` regularly and in CI
4. **Auto-fix in CI**: Run fixes automatically, fail on violations
5. **Consistent Rules**: Share config across team
6. **Pre-commit Checks**: Catch issues before commit (lint, PHPStan, CGL, security)
7. **Latest PHP**: Run quality tools with latest PHP version (8.4+)
8. **Regular Updates**: Keep tools and rules updated
## Resources
- [PHPStan Documentation](https://phpstan.org/user-guide/getting-started)
- [Rector Documentation](https://getrector.com/documentation)
- [PHP CS Fixer Documentation](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer)
- [TYPO3 Coding Guidelines](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/CodingGuidelines/)

691
references/test-runners.md Normal file
View File

@@ -0,0 +1,691 @@
# Test Runners and Orchestration
The `runTests.sh` script is the standard TYPO3 pattern for orchestrating all quality checks and test suites.
## Purpose
- Single entry point for all testing and quality checks
- Consistent environment across local and CI/CD
- Handles Docker, database setup, and test execution
- Based on [TYPO3 Best Practices tea extension](https://github.com/TYPO3BestPractices/tea)
## Script Location
```
Build/Scripts/runTests.sh
```
## Basic Usage
```bash
# Show help
./Build/Scripts/runTests.sh -h
# Run specific test suite
./Build/Scripts/runTests.sh -s unit
./Build/Scripts/runTests.sh -s functional
./Build/Scripts/runTests.sh -s acceptance
# Run quality tools
./Build/Scripts/runTests.sh -s lint
./Build/Scripts/runTests.sh -s phpstan
./Build/Scripts/runTests.sh -s cgl
./Build/Scripts/runTests.sh -s rector
```
## Script Options
```
-s <suite> Test suite to run (required)
unit, functional, acceptance, lint, phpstan, cgl, rector
-d <driver> Database driver for functional tests
mysqli (default), pdo_mysql, postgres, sqlite
-p <version> PHP version (7.4, 8.1, 8.2, 8.3)
-e <command> Execute specific command in container
-n Don't pull Docker images
-u Update composer dependencies
-v Enable verbose output
-x Stop on first error (PHPUnit --stop-on-error)
```
## Examples
### Run Unit Tests
```bash
# Default PHP version
./Build/Scripts/runTests.sh -s unit
# Specific PHP version
./Build/Scripts/runTests.sh -s unit -p 8.3
# Stop on first error
./Build/Scripts/runTests.sh -s unit -x
```
### Run Functional Tests
```bash
# Default database (mysqli)
./Build/Scripts/runTests.sh -s functional
# PostgreSQL
./Build/Scripts/runTests.sh -s functional -d postgres
# SQLite (fastest for local development)
./Build/Scripts/runTests.sh -s functional -d sqlite
```
### Run Quality Tools
```bash
# Lint all PHP files
./Build/Scripts/runTests.sh -s lint
# PHPStan static analysis
./Build/Scripts/runTests.sh -s phpstan
# Code style check
./Build/Scripts/runTests.sh -s cgl
# Rector automated refactoring
./Build/Scripts/runTests.sh -s rector
```
### Custom Commands
```bash
# Run specific test file
./Build/Scripts/runTests.sh -s unit -e "bin/phpunit Tests/Unit/Domain/Model/ProductTest.php"
# Run with coverage
./Build/Scripts/runTests.sh -s unit -e "bin/phpunit --coverage-html coverage/"
```
## Composer Integration
Integrate runTests.sh into composer.json:
```json
{
"scripts": {
"ci:test": [
"@ci:test:php:lint",
"@ci:test:php:phpstan",
"@ci:test:php:cgl",
"@ci:test:php:rector",
"@ci:test:php:unit",
"@ci:test:php:functional"
],
"ci:test:php:lint": "Build/Scripts/runTests.sh -s lint",
"ci:test:php:phpstan": "Build/Scripts/runTests.sh -s phpstan",
"ci:test:php:cgl": "Build/Scripts/runTests.sh -s cgl",
"ci:test:php:rector": "Build/Scripts/runTests.sh -s rector",
"ci:test:php:unit": "Build/Scripts/runTests.sh -s unit",
"ci:test:php:functional": "Build/Scripts/runTests.sh -s functional"
}
}
```
Then run via composer:
```bash
composer ci:test # All checks
composer ci:test:php:unit # Just unit tests
composer ci:test:php:phpstan # Just PHPStan
```
## Script Structure
### Basic Template
```bash
#!/usr/bin/env bash
# Script configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# Default values
TEST_SUITE=""
DATABASE_DRIVER="mysqli"
PHP_VERSION="8.4"
VERBOSE=""
# Parse arguments
while getopts ":s:d:p:e:nuvx" opt; do
case ${opt} in
s) TEST_SUITE=${OPTARG} ;;
d) DATABASE_DRIVER=${OPTARG} ;;
p) PHP_VERSION=${OPTARG} ;;
*) showHelp; exit 1 ;;
esac
done
# Validate required arguments
if [ -z "${TEST_SUITE}" ]; then
echo "Error: -s parameter (test suite) is required"
showHelp
exit 1
fi
# Execute test suite
case ${TEST_SUITE} in
unit)
runUnitTests
;;
functional)
runFunctionalTests
;;
lint)
runLint
;;
*)
echo "Error: Unknown test suite: ${TEST_SUITE}"
showHelp
exit 1
;;
esac
```
### Docker Integration
```bash
runUnitTests() {
CONTAINER_PATH="/app"
docker run \
--rm \
-v "${PROJECT_DIR}:${CONTAINER_PATH}" \
-w "${CONTAINER_PATH}" \
php:${PHP_VERSION}-cli \
bin/phpunit -c Build/phpunit/UnitTests.xml
}
runFunctionalTests() {
CONTAINER_PATH="/app"
docker run \
--rm \
-v "${PROJECT_DIR}:${CONTAINER_PATH}" \
-w "${CONTAINER_PATH}" \
-e typo3DatabaseDriver="${DATABASE_DRIVER}" \
-e typo3DatabaseHost="localhost" \
-e typo3DatabaseName="typo3_test" \
php:${PHP_VERSION}-cli \
bin/phpunit -c Build/phpunit/FunctionalTests.xml
}
```
### Quality Tool Functions
```bash
runLint() {
docker run \
--rm \
-v "${PROJECT_DIR}:/app" \
-w /app \
php:${PHP_VERSION}-cli \
vendor/bin/phplint
}
runPhpstan() {
docker run \
--rm \
-v "${PROJECT_DIR}:/app" \
-w /app \
php:${PHP_VERSION}-cli \
vendor/bin/phpstan analyze --configuration Build/phpstan.neon
}
runCgl() {
docker run \
--rm \
-v "${PROJECT_DIR}:/app" \
-w /app \
php:${PHP_VERSION}-cli \
vendor/bin/php-cs-fixer fix --config Build/php-cs-fixer.php --dry-run --diff
}
```
## Environment Variables
Configure via environment variables:
```bash
# Database configuration
export typo3DatabaseDriver=pdo_mysql
export typo3DatabaseHost=db
export typo3DatabasePort=3306
export typo3DatabaseName=typo3_test
export typo3DatabaseUsername=root
export typo3DatabasePassword=root
# TYPO3 context
export TYPO3_CONTEXT=Testing
# Run tests
./Build/Scripts/runTests.sh -s functional
```
## CI/CD Integration
### GitHub Actions
```yaml
name: Tests
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: '8.2', '8.2', '8.3']
suite: ['unit', 'functional', 'lint', 'phpstan']
steps:
- uses: actions/checkout@v4
- name: Run ${{ matrix.suite }} tests
run: Build/Scripts/runTests.sh -s ${{ matrix.suite }} -p ${{ matrix.php }}
```
### GitLab CI
```yaml
.test:
image: php:${PHP_VERSION}-cli
script:
- Build/Scripts/runTests.sh -s ${TEST_SUITE} -p ${PHP_VERSION}
unit:8.2:
extends: .test
variables:
PHP_VERSION: "8.2"
TEST_SUITE: "unit"
functional:8.2:
extends: .test
variables:
PHP_VERSION: "8.2"
TEST_SUITE: "functional"
```
## Performance Optimization
### Parallel Execution
```bash
# Run linting in parallel (fast)
find . -name '*.php' -print0 | xargs -0 -n1 -P8 php -l
# PHPUnit parallel execution
vendor/bin/paratest -c Build/phpunit/UnitTests.xml --processes=4
```
### Caching
```bash
# Cache Composer dependencies
if [ ! -d "${PROJECT_DIR}/.cache/composer" ]; then
mkdir -p "${PROJECT_DIR}/.cache/composer"
fi
docker run \
--rm \
-v "${PROJECT_DIR}:/app" \
-v "${PROJECT_DIR}/.cache/composer:/tmp/composer-cache" \
php:${PHP_VERSION}-cli \
composer install --no-progress --no-suggest
```
## Best Practices
1. **Single Source of Truth**: Use runTests.sh for all test execution
2. **CI/CD Alignment**: CI should use same script as local development
3. **Docker Isolation**: Run tests in containers for consistency
4. **Fast Feedback**: Run lint and unit tests first (fastest)
5. **Matrix Testing**: Test multiple PHP versions and databases
6. **Caching**: Cache dependencies to speed up execution
7. **Verbose Mode**: Use `-v` flag for debugging test failures
## Troubleshooting
### Docker Permission Issues
```bash
# Run with current user
docker run \
--rm \
--user $(id -u):$(id -g) \
-v "${PROJECT_DIR}:/app" \
php:${PHP_VERSION}-cli \
bin/phpunit
```
### Database Connection Errors
```bash
# Verify database is accessible
docker run --rm --network host mysql:8.0 \
mysql -h localhost -u root -p -e "SELECT 1"
# Use SQLite for simple tests
./Build/Scripts/runTests.sh -s functional -d sqlite
```
### Missing Dependencies
```bash
# Update dependencies
./Build/Scripts/runTests.sh -s unit -u
```
## Resources
- [TYPO3 Tea Extension runTests.sh](https://github.com/TYPO3BestPractices/tea/blob/main/Build/Scripts/runTests.sh)
- [TYPO3 Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/)
- [PHPUnit Documentation](https://phpunit.de/documentation.html)
## Multi-PHP Version Testing
TYPO3 extensions should support multiple PHP versions to maximize compatibility. This section covers testing strategies for all supported PHP versions.
### Standard Approach: Using Build/Scripts/runTests.sh
The `runTests.sh` script with Docker containers is the recommended TYPO3 testing approach. It provides isolated environments and consistent results.
#### Testing Across PHP Versions with Docker
```bash
# Test with PHP 8.2
./Build/Scripts/runTests.sh -s unit -p 8.2
./Build/Scripts/runTests.sh -s functional -p 8.2 -d sqlite
# Test with PHP 8.3
./Build/Scripts/runTests.sh -s unit -p 8.3
./Build/Scripts/runTests.sh -s functional -p 8.3 -d sqlite
# Test with PHP 8.4
./Build/Scripts/runTests.sh -s unit -p 8.4
./Build/Scripts/runTests.sh -s functional -p 8.4 -d sqlite
```
**Advantages:**
- Isolated Docker containers per PHP version
- Consistent environment across local and CI/CD
- No need to install multiple PHP versions locally
- Handles database setup automatically
- Based on official TYPO3 best practices
#### Complete Test Matrix Example
```bash
#!/bin/bash
# Test all supported PHP versions and databases
PHP_VERSIONS=("8.2" "8.3" "8.4")
DATABASES=("sqlite" "mysql" "postgres")
for PHP in "${PHP_VERSIONS[@]}"; do
echo "Testing PHP ${PHP}..."
# Unit tests
./Build/Scripts/runTests.sh -s unit -p "${PHP}"
# Functional tests with different databases
for DB in "${DATABASES[@]}"; do
echo " Functional tests with ${DB}..."
./Build/Scripts/runTests.sh -s functional -p "${PHP}" -d "${DB}"
done
# Quality tools (run once per PHP version)
./Build/Scripts/runTests.sh -s lint -p "${PHP}"
./Build/Scripts/runTests.sh -s phpstan -p "${PHP}"
done
```
### Alternative Approach: Native PHP Versions (Without Docker)
For CI/CD environments or when Docker is unavailable, use locally installed PHP versions.
#### Testing with Native PHP Installations
```bash
# Test with PHP 8.2
php8.2 /usr/local/bin/composer update --no-interaction
php8.2 .Build/bin/phpunit --configuration=Build/phpunit/UnitTests.xml
typo3DatabaseDriver=pdo_sqlite php8.2 .Build/bin/phpunit --configuration=Build/phpunit/FunctionalTests.xml
# Test with PHP 8.3
php8.3 /usr/local/bin/composer update --no-interaction
php8.3 .Build/bin/phpunit --configuration=Build/phpunit/UnitTests.xml
typo3DatabaseDriver=pdo_sqlite php8.3 .Build/bin/phpunit --configuration=Build/phpunit/FunctionalTests.xml
# Test with PHP 8.4
php8.4 /usr/local/bin/composer update --no-interaction
php8.4 .Build/bin/phpunit --configuration=Build/phpunit/UnitTests.xml
typo3DatabaseDriver=pdo_sqlite php8.4 .Build/bin/phpunit --configuration=Build/phpunit/FunctionalTests.xml
```
**CRITICAL**: Always run `composer update` with the target PHP version FIRST. This ensures:
- Correct PHPUnit version selection (PHPUnit 11 for PHP 8.2, PHPUnit 12 for PHP 8.3+)
- Proper dependency resolution for the PHP version
- Compatible autoloader generation
### PHPUnit Version Compatibility
The `typo3/testing-framework` supports both PHPUnit 11 and 12, allowing Composer to automatically select the compatible version:
| PHP Version | PHPUnit Version | Auto-Selected by Composer |
|-------------|-----------------|---------------------------|
| 8.2 | 11.x | ✅ Yes |
| 8.3 | 11.x or 12.x | ✅ Yes (12.x preferred) |
| 8.4 | 11.x or 12.x | ✅ Yes (12.x preferred) |
**Example: Automatic PHPUnit Selection**
```bash
# PHP 8.2 automatically gets PHPUnit 11
$ php8.2 /usr/local/bin/composer update
# Installing typo3/testing-framework (v8.0.14)
# Installing phpunit/phpunit (11.5.42)
# PHP 8.3 automatically gets PHPUnit 12
$ php8.3 /usr/local/bin/composer update
# Installing typo3/testing-framework (v8.0.14)
# Installing phpunit/phpunit (12.4.1)
```
### GitHub Actions CI/CD Integration
#### Using runTests.sh (Recommended)
```yaml
name: CI
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4']
suite: ['unit', 'functional']
database: ['sqlite', 'mysql', 'postgres']
exclude:
# Only test sqlite for unit tests
- suite: unit
database: mysql
- suite: unit
database: postgres
steps:
- uses: actions/checkout@v4
- name: Run ${{ matrix.suite }} tests on PHP ${{ matrix.php }}
run: |
Build/Scripts/runTests.sh \
-s ${{ matrix.suite }} \
-p ${{ matrix.php }} \
-d ${{ matrix.database }}
```
#### Using Native PHP (Alternative)
```yaml
name: CI
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4
- name: Set up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, libxml, sqlite3
coverage: none
- name: Install dependencies
run: composer update --no-interaction --no-progress
- name: Run unit tests
run: .Build/bin/phpunit -c Build/phpunit/UnitTests.xml
- name: Run functional tests
env:
typo3DatabaseDriver: pdo_sqlite
run: .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml
```
### Common Pitfalls
#### ❌ Wrong: Testing Without Updating Dependencies
```bash
# This will use wrong PHPUnit version
php8.2 .Build/bin/phpunit -c Build/phpunit/UnitTests.xml
```
**Problem:** Uses PHPUnit version from previous `composer update`, may be incompatible.
#### ✅ Right: Update Dependencies First
```bash
# This ensures correct PHPUnit version
php8.2 /usr/local/bin/composer update --no-interaction
php8.2 .Build/bin/phpunit -c Build/phpunit/UnitTests.xml
```
#### ❌ Wrong: Removing PHP Version Support Due to Test Failures
```bash
# Don't do this!
# composer.json: "php": "^8.3 || ^8.4" # Removed 8.2
```
**Problem:** Unnecessarily reduces compatibility. Fix the testing approach instead.
#### ✅ Right: Fix Testing Process
```bash
# Run composer update with the problematic PHP version
php8.2 /usr/local/bin/composer update
# Composer automatically selects compatible dependencies
# Now tests work correctly
```
### Best Practices
1. **Standard Approach First**: Use `Build/Scripts/runTests.sh` with Docker whenever possible
2. **Update Before Testing**: Always run `composer update` with target PHP version first (native approach)
3. **Trust Composer**: Let Composer select compatible PHPUnit versions automatically
4. **Test All Versions**: Run full test suite on all supported PHP versions in CI
5. **SQLite for Speed**: Use SQLite for local functional testing (fastest)
6. **Matrix Testing**: Use CI matrix to test all PHP versions in parallel
7. **Don't Remove Support**: Fix testing process, don't remove PHP version support
### Troubleshooting
#### Docker Issues
```bash
# Update Docker images
./Build/Scripts/runTests.sh -u
# Check Docker is running
docker ps
# Clean up old containers
docker system prune -a
```
#### Native PHP Issues
```bash
# Verify PHP version
php8.2 --version
# Check installed extensions
php8.2 -m | grep -E 'dom|libxml|sqlite'
# Clean and reinstall dependencies
rm -rf .Build vendor composer.lock
php8.2 /usr/local/bin/composer install
```
#### PHPUnit Version Conflicts
```bash
# Clear Composer cache
php8.2 /usr/local/bin/composer clear-cache
# Show why PHPUnit version was selected
php8.2 /usr/local/bin/composer why phpunit/phpunit
# Force dependency resolution
rm composer.lock
php8.2 /usr/local/bin/composer update --with-all-dependencies
```
### Summary
| Aspect | Docker/runTests.sh | Native PHP |
|--------|-------------------|------------|
| **Use Case** | Standard TYPO3 development | CI/CD, no Docker available |
| **Isolation** | ✅ Full container isolation | ⚠️ System-wide PHP |
| **Setup** | Docker required | Multiple PHP versions required |
| **Speed** | Slower (container overhead) | Faster (native execution) |
| **Consistency** | ✅ Guaranteed environment | ⚠️ Depends on system config |
| **Recommendation** | **Recommended** | Alternative when needed |
**Choose Docker/runTests.sh for:**
- Local development and testing
- Consistent environment across team
- Official TYPO3 best practices
**Choose Native PHP for:**
- GitHub Actions/GitLab CI environments
- Systems without Docker
- Performance-critical testing pipelines

929
references/unit-testing.md Normal file
View File

@@ -0,0 +1,929 @@
# Unit Testing in TYPO3
Unit tests are fast, isolated tests that verify individual components without external dependencies like databases or file systems.
## When to Use Unit Tests
**Ideal for:**
- Testing pure business logic
- Validators, calculators, transformers
- Value objects and DTOs
- Utilities and helper functions
- Domain models without persistence
- **Controllers with dependency injection** (new in TYPO3 13)
- **Services with injected dependencies**
**Not suitable for:**
- Database operations (use functional tests)
- File system operations
- Methods using `BackendUtility` or global state
- Complex TYPO3 framework integration
- Parent class behavior from framework classes
## Base Class
All unit tests extend `TYPO3\TestingFramework\Core\Unit\UnitTestCase`:
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Unit\Domain\Validator;
use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use Vendor\Extension\Domain\Validator\EmailValidator;
/**
* Unit tests for EmailValidator.
*
* @covers \Vendor\Extension\Domain\Validator\EmailValidator
*/
final class EmailValidatorTest extends UnitTestCase
{
private EmailValidator $subject;
protected function setUp(): void
{
parent::setUp();
$this->subject = new EmailValidator();
}
#[Test]
public function validEmailPassesValidation(): void
{
$result = $this->subject->validate('user@example.com');
self::assertFalse($result->hasErrors());
}
#[Test]
public function invalidEmailFailsValidation(): void
{
$result = $this->subject->validate('invalid-email');
self::assertTrue($result->hasErrors());
}
}
```
> **Note:** TYPO3 13+ with PHPUnit 11/12 uses PHP attributes (`#[Test]`) instead of `@test` annotations.
> Use `private` instead of `protected` for properties when possible (better encapsulation).
## Key Principles
### 1. No External Dependencies
Unit tests should NOT:
- Access the database
- Read/write files
- Make HTTP requests
- Use TYPO3 framework services
### 2. Fast Execution
Unit tests should run in milliseconds:
- No I/O operations
- Minimal object instantiation
- Use mocks for dependencies
### 3. Test Independence
Each test should:
- Be runnable standalone
- Not depend on execution order
- Clean up in tearDown()
## Test Structure
### Arrange-Act-Assert Pattern
```php
/**
* @test
*/
public function calculatesTotalPrice(): void
{
// Arrange: Set up test data
$cart = new ShoppingCart();
$cart->addItem(new Item('product1', 10.00, 2));
$cart->addItem(new Item('product2', 5.50, 1));
// Act: Execute the code under test
$total = $cart->calculateTotal();
// Assert: Verify the result
self::assertSame(25.50, $total);
}
```
### setUp() and tearDown()
```php
protected function setUp(): void
{
parent::setUp();
// Initialize test subject and dependencies
$this->subject = new Calculator();
}
protected function tearDown(): void
{
// Clean up resources
unset($this->subject);
parent::tearDown();
}
```
## Testing with Dependency Injection (TYPO3 13+)
Modern TYPO3 13 controllers and services use constructor injection. Here's how to test them:
### Basic Constructor Injection Test
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Unit\Controller;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use Vendor\Extension\Controller\ImageController;
final class ImageControllerTest extends UnitTestCase
{
private ImageController $subject;
/** @var ResourceFactory&MockObject */
private ResourceFactory $resourceFactoryMock;
protected function setUp(): void
{
parent::setUp();
/** @var ResourceFactory&MockObject $resourceFactoryMock */
$resourceFactoryMock = $this->createMock(ResourceFactory::class);
$this->resourceFactoryMock = $resourceFactoryMock;
$this->subject = new ImageController($this->resourceFactoryMock);
}
#[Test]
public function getFileRetrievesFileFromFactory(): void
{
$fileId = 123;
$fileMock = $this->createMock(\TYPO3\CMS\Core\Resource\File::class);
$this->resourceFactoryMock
->expects(self::once())
->method('getFileObject')
->with($fileId)
->willReturn($fileMock);
$result = $this->subject->getFile($fileId);
self::assertSame($fileMock, $result);
}
}
```
### Multiple Dependencies with Intersection Types
PHPUnit mocks require proper type hints using intersection types for PHPStan compliance:
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Unit\Controller;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use Vendor\Extension\Controller\ImageController;
use Vendor\Extension\Utils\ImageProcessor;
final class ImageControllerTest extends UnitTestCase
{
private ImageController $subject;
/** @var ResourceFactory&MockObject */
private ResourceFactory $resourceFactoryMock;
/** @var ImageProcessor&MockObject */
private ImageProcessor $imageProcessorMock;
/** @var LogManager&MockObject */
private LogManager $logManagerMock;
protected function setUp(): void
{
parent::setUp();
/** @var ResourceFactory&MockObject $resourceFactoryMock */
$resourceFactoryMock = $this->createMock(ResourceFactory::class);
/** @var ImageProcessor&MockObject $imageProcessorMock */
$imageProcessorMock = $this->createMock(ImageProcessor::class);
/** @var LogManager&MockObject $logManagerMock */
$logManagerMock = $this->createMock(LogManager::class);
$this->resourceFactoryMock = $resourceFactoryMock;
$this->imageProcessorMock = $imageProcessorMock;
$this->logManagerMock = $logManagerMock;
$this->subject = new ImageController(
$this->resourceFactoryMock,
$this->imageProcessorMock,
$this->logManagerMock,
);
}
#[Test]
public function processImageUsesInjectedProcessor(): void
{
$fileMock = $this->createMock(\TYPO3\CMS\Core\Resource\File::class);
$processedFileMock = $this->createMock(\TYPO3\CMS\Core\Resource\ProcessedFile::class);
$this->imageProcessorMock
->expects(self::once())
->method('process')
->with($fileMock, ['width' => 800])
->willReturn($processedFileMock);
$result = $this->subject->processImage($fileMock, ['width' => 800]);
self::assertSame($processedFileMock, $result);
}
}
```
**Key Points:**
- Use intersection types: `ResourceFactory&MockObject` for proper PHPStan type checking
- Assign mocks to properly typed variables before passing to constructor
- This pattern works with PHPUnit 11/12 and PHPStan Level 10
### Handling $GLOBALS and Singleton State
Some TYPO3 components still use global state. Handle this properly:
```php
final class BackendControllerTest extends UnitTestCase
{
protected bool $resetSingletonInstances = true;
#[Test]
public function checksBackendUserPermissions(): void
{
// Mock backend user
$backendUserMock = $this->createMock(BackendUserAuthentication::class);
$backendUserMock->method('isAdmin')->willReturn(true);
$GLOBALS['BE_USER'] = $backendUserMock;
$result = $this->subject->hasAccess();
self::assertTrue($result);
}
#[Test]
public function returnsFalseWhenNoBackendUser(): void
{
$GLOBALS['BE_USER'] = null;
$result = $this->subject->hasAccess();
self::assertFalse($result);
}
}
```
**Important:** Set `protected bool $resetSingletonInstances = true;` when tests interact with TYPO3 singletons to prevent test pollution.
## Mocking Dependencies
Use PHPUnit's built-in mocking (PHPUnit 11/12):
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Unit\Service;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use Vendor\Extension\Domain\Model\User;
use Vendor\Extension\Domain\Repository\UserRepository;
use Vendor\Extension\Service\UserService;
final class UserServiceTest extends UnitTestCase
{
private UserService $subject;
/** @var UserRepository&MockObject */
private UserRepository $repositoryMock;
protected function setUp(): void
{
parent::setUp();
/** @var UserRepository&MockObject $repositoryMock */
$repositoryMock = $this->createMock(UserRepository::class);
$this->repositoryMock = $repositoryMock;
$this->subject = new UserService($this->repositoryMock);
}
#[Test]
public function findsUserByEmail(): void
{
$email = 'test@example.com';
$user = new User('John');
$this->repositoryMock
->expects(self::once())
->method('findByEmail')
->with($email)
->willReturn($user);
$result = $this->subject->getUserByEmail($email);
self::assertSame('John', $result->getName());
}
#[Test]
public function throwsExceptionWhenUserNotFound(): void
{
$email = 'nonexistent@example.com';
$this->repositoryMock
->method('findByEmail')
->with($email)
->willReturn(null);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('User not found');
$this->subject->getUserByEmail($email);
}
}
```
> **Note:** TYPO3 13+ with PHPUnit 11/12 uses `createMock()` instead of Prophecy.
> Prophecy is deprecated and should not be used in new tests.
## Assertions
### Common Assertions
```php
// Equality
self::assertEquals($expected, $actual);
self::assertSame($expected, $actual); // Strict comparison
// Boolean
self::assertTrue($condition);
self::assertFalse($condition);
// Null checks
self::assertNull($value);
self::assertNotNull($value);
// Type checks
self::assertIsString($value);
self::assertIsInt($value);
self::assertIsArray($value);
self::assertInstanceOf(User::class, $object);
// Collections
self::assertCount(3, $array);
self::assertEmpty($array);
self::assertContains('item', $array);
// Exceptions
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid input');
$subject->methodThatThrows();
```
### Specific Over Generic
```php
// ❌ Too generic
self::assertTrue($result > 0);
self::assertEquals(true, $isValid);
// ✅ Specific and clear
self::assertGreaterThan(0, $result);
self::assertTrue($isValid);
```
## Data Providers
Test multiple scenarios with data providers:
```php
/**
* @test
* @dataProvider validEmailProvider
*/
public function validatesEmails(string $email, bool $expected): void
{
$result = $this->subject->isValid($email);
self::assertSame($expected, $result);
}
public static function validEmailProvider(): array
{
return [
'valid email' => ['user@example.com', true],
'email with subdomain' => ['user@mail.example.com', true],
'missing @' => ['userexample.com', false],
'missing domain' => ['user@', false],
'empty string' => ['', false],
];
}
```
## Testing Private/Protected Methods
**Preferred Approach**: Test through public API whenever possible:
```php
// ✅ Best approach - test through public interface
$result = $subject->publicMethodThatUsesPrivateMethod();
self::assertSame($expected, $result);
```
**When Reflection is Acceptable**: Sometimes protected methods contain complex logic that deserves dedicated testing (e.g., URL validation, attribute resolution). In these cases, use a helper method:
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Unit\Controller;
use PHPUnit\Framework\Attributes\Test;
use ReflectionMethod;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use Vendor\Extension\Controller\ImageController;
final class ImageControllerTest extends UnitTestCase
{
private ImageController $subject;
/**
* Helper method to access protected methods.
*
* @param array<int, mixed> $args
*/
private function callProtectedMethod(string $methodName, array $args): mixed
{
$reflection = new ReflectionMethod($this->subject, $methodName);
$reflection->setAccessible(true);
return $reflection->invokeArgs($this->subject, $args);
}
#[Test]
public function isExternalImageReturnsTrueForHttpsUrls(): void
{
$result = $this->callProtectedMethod('isExternalImage', ['https://example.com/image.jpg']);
self::assertTrue($result);
}
#[Test]
public function isExternalImageReturnsFalseForLocalPaths(): void
{
$result = $this->callProtectedMethod('isExternalImage', ['/fileadmin/images/test.jpg']);
self::assertFalse($result);
}
}
```
**Important Considerations**:
- Only use reflection when testing protected methods with complex logic worth testing independently
- Never test private methods - refactor to protected if testing is needed
- Prefer testing through public API when the logic is simple
- Document why reflection testing is used for a specific method
## Configuration
### PHPUnit XML (Build/phpunit/UnitTests.xml)
```xml
<phpunit
bootstrap="../../vendor/autoload.php"
cacheResult="false"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
failOnRisky="true">
<testsuites>
<testsuite name="Unit tests">
<directory>../../Tests/Unit/</directory>
</testsuite>
</testsuites>
</phpunit>
```
## Best Practices
1. **One Assert Per Test**: Focus tests on single behavior
2. **Clear Test Names**: Describe what is tested and expected result
3. **Arrange-Act-Assert**: Follow consistent structure
4. **No Logic in Tests**: Tests should be simple and readable
5. **Test Edge Cases**: Empty strings, null, zero, negative numbers
6. **Use Data Providers**: Test multiple scenarios efficiently
7. **Mock External Dependencies**: Keep tests isolated and fast
## Common Pitfalls
**Testing Framework Code**
```php
// Don't test TYPO3 core functionality
$this->assertTrue(is_array([])); // Useless test
```
**Slow Tests**
```php
// Don't access file system in unit tests
file_put_contents('/tmp/test.txt', 'data');
```
**Test Interdependence**
```php
// Don't depend on test execution order
/** @depends testCreate */
public function testUpdate(): void { }
```
**Focused, Fast, Isolated Tests**
```php
/**
* @test
*/
public function calculatesPriceWithDiscount(): void
{
$calculator = new PriceCalculator();
$price = $calculator->calculate(100.0, 0.2);
self::assertSame(80.0, $price);
}
```
## Running Unit Tests
```bash
# Via runTests.sh
Build/Scripts/runTests.sh -s unit
# Via PHPUnit directly
vendor/bin/phpunit -c Build/phpunit/UnitTests.xml
# Via Composer
composer ci:test:php:unit
# Single test file
vendor/bin/phpunit Tests/Unit/Domain/Validator/EmailValidatorTest.php
# Single test method
vendor/bin/phpunit --filter testValidEmail
```
## Troubleshooting Common Issues
### PHPStan Errors with Mocks
**Problem**: PHPStan complains about mock type mismatches.
```
Method expects ResourceFactory but got ResourceFactory&MockObject
```
**Solution**: Use intersection type annotations:
```php
/** @var ResourceFactory&MockObject */
private ResourceFactory $resourceFactoryMock;
protected function setUp(): void
{
parent::setUp();
/** @var ResourceFactory&MockObject $resourceFactoryMock */
$resourceFactoryMock = $this->createMock(ResourceFactory::class);
$this->resourceFactoryMock = $resourceFactoryMock;
$this->subject = new MyController($this->resourceFactoryMock);
}
```
### Undefined Array Key Warnings
**Problem**: Tests throw warnings about missing array keys.
```
Undefined array key "fileId"
```
**Solution**: Always provide all required keys in mock arrays:
```php
// ❌ Incomplete mock data
$requestMock->method('getQueryParams')->willReturn([
'fileId' => 123,
]);
// ✅ Complete mock data
$requestMock->method('getQueryParams')->willReturn([
'fileId' => 123,
'table' => 'tt_content',
'P' => [],
]);
```
### Tests Requiring Functional Setup
**Problem**: Unit tests fail with cache or framework errors.
```
NoSuchCacheException: A cache with identifier "runtime" does not exist.
```
**Solution**: Identify methods that require TYPO3 framework infrastructure and move them to functional tests:
- Methods using `BackendUtility::getPagesTSconfig()`
- Methods calling parent class framework behavior
- Methods requiring global state like `$GLOBALS['TYPO3_CONF_VARS']`
Add comments explaining the limitation:
```php
// Note: getMaxDimensions tests require functional test setup due to BackendUtility dependency
// These are better tested in functional tests
```
### Singleton State Pollution
**Problem**: Tests interfere with each other due to singleton state.
**Solution**: Enable singleton reset in your test class:
```php
final class MyControllerTest extends UnitTestCase
{
protected bool $resetSingletonInstances = true;
#[Test]
public function testWithGlobals(): void
{
$GLOBALS['BE_USER'] = $this->createMock(BackendUserAuthentication::class);
// Test will clean up automatically
}
}
```
### Exception Flow Issues
**Problem**: Catching and re-throwing exceptions masks the original error.
```php
// ❌ Inner exception caught by outer catch
try {
$file = $this->factory->getFile($id);
if ($file->isDeleted()) {
throw new RuntimeException('Deleted', 1234);
}
} catch (Exception $e) {
throw new RuntimeException('Not found', 5678);
}
```
**Solution**: Separate concerns - catch only what you need:
```php
// ✅ Proper exception flow
try {
$file = $this->factory->getFile($id);
} catch (Exception $e) {
throw new RuntimeException('Not found', 5678, $e);
}
if ($file->isDeleted()) {
throw new RuntimeException('Deleted', 1234);
}
```
## Testing DataHandler Hooks
DataHandler hooks (`processDatamap_*`, `processCmdmap_*`) require careful testing as they interact with TYPO3 globals.
### Example: Testing processDatamap_postProcessFieldArray
```php
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Unit\Database;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Core\Resource\DefaultUploadFolderResolver;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use Vendor\Extension\Database\MyDataHandlerHook;
/**
* Unit tests for MyDataHandlerHook.
*
* @covers \Vendor\Extension\Database\MyDataHandlerHook
*/
final class MyDataHandlerHookTest extends UnitTestCase
{
protected bool $resetSingletonInstances = true;
private MyDataHandlerHook $subject;
/** @var ExtensionConfiguration&MockObject */
private ExtensionConfiguration $extensionConfigurationMock;
/** @var LogManager&MockObject */
private LogManager $logManagerMock;
/** @var ResourceFactory&MockObject */
private ResourceFactory $resourceFactoryMock;
/** @var Context&MockObject */
private Context $contextMock;
/** @var RequestFactory&MockObject */
private RequestFactory $requestFactoryMock;
/** @var DefaultUploadFolderResolver&MockObject */
private DefaultUploadFolderResolver $uploadFolderResolverMock;
/** @var Logger&MockObject */
private Logger $loggerMock;
protected function setUp(): void
{
parent::setUp();
// Create all required mocks with intersection types for PHPStan compliance
/** @var ExtensionConfiguration&MockObject $extensionConfigurationMock */
$extensionConfigurationMock = $this->createMock(ExtensionConfiguration::class);
/** @var LogManager&MockObject $logManagerMock */
$logManagerMock = $this->createMock(LogManager::class);
/** @var ResourceFactory&MockObject $resourceFactoryMock */
$resourceFactoryMock = $this->createMock(ResourceFactory::class);
/** @var Context&MockObject $contextMock */
$contextMock = $this->createMock(Context::class);
/** @var RequestFactory&MockObject $requestFactoryMock */
$requestFactoryMock = $this->createMock(RequestFactory::class);
/** @var DefaultUploadFolderResolver&MockObject $uploadFolderResolverMock */
$uploadFolderResolverMock = $this->createMock(DefaultUploadFolderResolver::class);
/** @var Logger&MockObject $loggerMock */
$loggerMock = $this->createMock(Logger::class);
// Configure extension configuration mock with willReturnCallback
$extensionConfigurationMock
->method('get')
->willReturnCallback(function ($extension, $key) {
if ($extension === 'my_extension') {
return match ($key) {
'enableFeature' => true,
'timeout' => 30,
default => null,
};
}
return null;
});
// Configure log manager to return logger mock
$logManagerMock
->method('getLogger')
->with(MyDataHandlerHook::class)
->willReturn($loggerMock);
// Assign mocks to properties
$this->extensionConfigurationMock = $extensionConfigurationMock;
$this->logManagerMock = $logManagerMock;
$this->resourceFactoryMock = $resourceFactoryMock;
$this->contextMock = $contextMock;
$this->requestFactoryMock = $requestFactoryMock;
$this->uploadFolderResolverMock = $uploadFolderResolverMock;
$this->loggerMock = $loggerMock;
// Create subject with all dependencies
$this->subject = new MyDataHandlerHook(
$this->extensionConfigurationMock,
$this->logManagerMock,
$this->resourceFactoryMock,
$this->contextMock,
$this->requestFactoryMock,
$this->uploadFolderResolverMock,
);
}
#[Test]
public function constructorInitializesWithDependencyInjection(): void
{
// Verify subject was created successfully with all dependencies
self::assertInstanceOf(MyDataHandlerHook::class, $this->subject);
}
#[Test]
public function processDatamapPostProcessFieldArrayHandlesFieldCorrectly(): void
{
$status = 'update';
$table = 'tt_content';
$id = '123';
$fieldArray = ['bodytext' => '<p>Content with processing</p>'];
/** @var DataHandler&MockObject $dataHandlerMock */
$dataHandlerMock = $this->createMock(DataHandler::class);
// Mock TCA configuration for RTE field
$GLOBALS['TCA']['tt_content']['columns']['bodytext']['config'] = [
'type' => 'text',
'enableRichtext' => true,
];
// Test the hook processes the field
$this->subject->processDatamap_postProcessFieldArray(
$status,
$table,
$id,
$fieldArray,
$dataHandlerMock,
);
// Assert field was processed (actual assertion depends on implementation)
self::assertNotEmpty($fieldArray['bodytext']);
}
#[Test]
public function constructorLoadsExtensionConfiguration(): void
{
/** @var ExtensionConfiguration&MockObject $configMock */
$configMock = $this->createMock(ExtensionConfiguration::class);
$configMock
->expects(self::exactly(2))
->method('get')
->willReturnCallback(function ($extension, $key) {
self::assertSame('my_extension', $extension);
return match ($key) {
'enableFeature' => true,
'timeout' => 30,
default => null,
};
});
new MyDataHandlerHook(
$configMock,
$this->logManagerMock,
$this->resourceFactoryMock,
$this->contextMock,
$this->requestFactoryMock,
$this->uploadFolderResolverMock,
);
}
}
```
**Key Testing Patterns for DataHandler Hooks:**
1. **Intersection Types for PHPStan**: Use `ResourceFactory&MockObject` for strict type compliance
2. **TCA Globals**: Set `$GLOBALS['TCA']` in tests to simulate TYPO3 table configuration
3. **Extension Configuration**: Use `willReturnCallback` with `match` expressions for flexible config mocking
4. **DataHandler Mock**: Create mock for `$dataHandler` parameter (required in hook signature)
5. **Reset Singletons**: Always set `protected bool $resetSingletonInstances = true;`
6. **Constructor DI**: Inject all dependencies via constructor (TYPO3 13+ best practice)
## Resources
- [TYPO3 Unit Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/UnitTests.html)
- [PHPUnit Documentation](https://phpunit.de/documentation.html)
- [PHPUnit 11 Migration Guide](https://phpunit.de/announcements/phpunit-11.html)
- [TYPO3 DataHandler Hooks](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Hooks/DataHandler/Index.html)

224
scripts/generate-test.sh Executable file
View File

@@ -0,0 +1,224 @@
#!/usr/bin/env bash
#
# Generate TYPO3 test class
#
# Usage: ./generate-test.sh <type> <ClassName>
# Example: ./generate-test.sh unit EmailValidator
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Parse arguments
TEST_TYPE="$1"
CLASS_NAME="$2"
if [ -z "${TEST_TYPE}" ] || [ -z "${CLASS_NAME}" ]; then
echo "Usage: $0 <type> <ClassName>"
echo
echo "Types:"
echo " unit - Unit test (fast, no database)"
echo " functional - Functional test (with database)"
echo " acceptance - Acceptance test (browser-based)"
echo
echo "Example:"
echo " $0 unit EmailValidator"
echo " $0 functional ProductRepository"
echo " $0 acceptance LoginCest"
exit 1
fi
# Validate test type
case ${TEST_TYPE} in
unit|functional|acceptance)
;;
*)
echo -e "${RED}Error: Invalid test type '${TEST_TYPE}'${NC}"
echo "Valid types: unit, functional, acceptance"
exit 1
;;
esac
# Determine paths
PROJECT_DIR="$(pwd)"
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Check if Tests directory exists
if [ ! -d "${PROJECT_DIR}/Tests" ]; then
echo -e "${RED}Error: Tests directory not found${NC}"
echo "Run setup-testing.sh first"
exit 1
fi
# Set target directory based on test type
case ${TEST_TYPE} in
unit)
TEST_DIR="${PROJECT_DIR}/Tests/Unit"
TEST_SUFFIX="Test"
;;
functional)
TEST_DIR="${PROJECT_DIR}/Tests/Functional"
TEST_SUFFIX="Test"
;;
acceptance)
TEST_DIR="${PROJECT_DIR}/Tests/Acceptance"
TEST_SUFFIX="Cest"
;;
esac
# Extract namespace from composer.json
NAMESPACE=$(php -r '
$composer = json_decode(file_get_contents("composer.json"), true);
foreach ($composer["autoload"]["psr-4"] ?? [] as $ns => $path) {
if (strpos($path, "Classes") !== false) {
echo rtrim($ns, "\\");
break;
}
}
')
if [ -z "${NAMESPACE}" ]; then
echo -e "${RED}Error: Could not determine namespace from composer.json${NC}"
exit 1
fi
# Determine test file path
TEST_FILE="${TEST_DIR}/${CLASS_NAME}${TEST_SUFFIX}.php"
# Check if file already exists
if [ -f "${TEST_FILE}" ]; then
echo -e "${RED}Error: Test file already exists: ${TEST_FILE}${NC}"
exit 1
fi
# Create test file directory if needed
mkdir -p "$(dirname "${TEST_FILE}")"
echo -e "${GREEN}Generating ${TEST_TYPE} test for ${CLASS_NAME}...${NC}"
# Generate test class based on type
case ${TEST_TYPE} in
unit)
cat > "${TEST_FILE}" << EOF
<?php
declare(strict_types=1);
namespace ${NAMESPACE}\\Tests\\Unit;
use TYPO3\\TestingFramework\\Core\\Unit\\UnitTestCase;
use ${NAMESPACE}\\${CLASS_NAME};
/**
* Unit test for ${CLASS_NAME}
*/
final class ${CLASS_NAME}${TEST_SUFFIX} extends UnitTestCase
{
protected ${CLASS_NAME} \$subject;
protected function setUp(): void
{
parent::setUp();
\$this->subject = new ${CLASS_NAME}();
}
/**
* @test
*/
public function canBeInstantiated(): void
{
self::assertInstanceOf(${CLASS_NAME}::class, \$this->subject);
}
}
EOF
;;
functional)
cat > "${TEST_FILE}" << EOF
<?php
declare(strict_types=1);
namespace ${NAMESPACE}\\Tests\\Functional;
use TYPO3\\TestingFramework\\Core\\Functional\\FunctionalTestCase;
use ${NAMESPACE}\\${CLASS_NAME};
/**
* Functional test for ${CLASS_NAME}
*/
final class ${CLASS_NAME}${TEST_SUFFIX} extends FunctionalTestCase
{
protected ${CLASS_NAME} \$subject;
protected array \$testExtensionsToLoad = [
'typo3conf/ext/your_extension',
];
protected function setUp(): void
{
parent::setUp();
\$this->subject = \$this->get(${CLASS_NAME}::class);
}
/**
* @test
*/
public function canBeInstantiated(): void
{
self::assertInstanceOf(${CLASS_NAME}::class, \$this->subject);
}
}
EOF
# Create fixture file
FIXTURE_FILE="${PROJECT_DIR}/Tests/Functional/Fixtures/${CLASS_NAME}.csv"
if [ ! -f "${FIXTURE_FILE}" ]; then
echo "# Fixture for ${CLASS_NAME}${TEST_SUFFIX}" > "${FIXTURE_FILE}"
echo -e "${GREEN}✓ Created fixture: ${FIXTURE_FILE}${NC}"
fi
;;
acceptance)
cat > "${TEST_FILE}" << EOF
<?php
declare(strict_types=1);
namespace ${NAMESPACE}\\Tests\\Acceptance;
use ${NAMESPACE}\\Tests\\Acceptance\\AcceptanceTester;
/**
* Acceptance test for ${CLASS_NAME/Cest/} workflow
*/
final class ${CLASS_NAME}${TEST_SUFFIX}
{
public function _before(AcceptanceTester \$I): void
{
// Setup before each test
}
public function exampleTest(AcceptanceTester \$I): void
{
\$I->amOnPage('/');
\$I->see('Welcome');
}
}
EOF
;;
esac
echo -e "${GREEN}✓ Created: ${TEST_FILE}${NC}"
echo
echo "Run test:"
echo " vendor/bin/phpunit ${TEST_FILE}"
echo
echo "Or via composer:"
echo " composer ci:test:php:${TEST_TYPE}"

180
scripts/setup-testing.sh Executable file
View File

@@ -0,0 +1,180 @@
#!/usr/bin/env bash
#
# Setup TYPO3 testing infrastructure
#
# This script initializes testing infrastructure for TYPO3 extensions:
# - Composer dependencies
# - PHPUnit configurations
# - Directory structure
# - Optional: Docker Compose for acceptance tests
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Script configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
PROJECT_DIR="$(pwd)"
WITH_ACCEPTANCE=false
# Parse arguments
while getopts ":a" opt; do
case ${opt} in
a)
WITH_ACCEPTANCE=true
;;
\?)
echo "Usage: $0 [-a]"
echo " -a Include acceptance testing setup (Docker Compose, Codeception)"
exit 1
;;
esac
done
echo -e "${GREEN}=== TYPO3 Testing Infrastructure Setup ===${NC}"
echo
# Check if composer.json exists
if [ ! -f "${PROJECT_DIR}/composer.json" ]; then
echo -e "${RED}Error: composer.json not found in current directory${NC}"
echo "Please run this script from your TYPO3 extension root directory"
exit 1
fi
# 1. Install testing framework dependencies
echo -e "${YELLOW}[1/6] Installing testing framework dependencies...${NC}"
if ! grep -q "typo3/testing-framework" "${PROJECT_DIR}/composer.json"; then
composer require --dev "typo3/testing-framework:^8.0 || ^9.0" --no-update
echo -e "${GREEN}✓ Added typo3/testing-framework${NC}"
else
echo -e "${GREEN}✓ typo3/testing-framework already present${NC}"
fi
# Install PHPUnit if not present
if ! grep -q "phpunit/phpunit" "${PROJECT_DIR}/composer.json"; then
composer require --dev "phpunit/phpunit:^10.5 || ^11.0" --no-update
echo -e "${GREEN}✓ Added phpunit/phpunit${NC}"
fi
composer update --no-progress
# 2. Create directory structure
echo -e "${YELLOW}[2/6] Creating directory structure...${NC}"
mkdir -p "${PROJECT_DIR}/Tests/Unit"
mkdir -p "${PROJECT_DIR}/Tests/Functional/Fixtures"
mkdir -p "${PROJECT_DIR}/Build/phpunit"
mkdir -p "${PROJECT_DIR}/Build/Scripts"
echo -e "${GREEN}✓ Directories created${NC}"
# 3. Copy PHPUnit configurations
echo -e "${YELLOW}[3/6] Installing PHPUnit configurations...${NC}"
if [ ! -f "${PROJECT_DIR}/Build/phpunit/UnitTests.xml" ]; then
cp "${SKILL_DIR}/templates/UnitTests.xml" "${PROJECT_DIR}/Build/phpunit/"
echo -e "${GREEN}✓ Created UnitTests.xml${NC}"
else
echo -e "${YELLOW}⚠ UnitTests.xml already exists (skipped)${NC}"
fi
if [ ! -f "${PROJECT_DIR}/Build/phpunit/FunctionalTests.xml" ]; then
cp "${SKILL_DIR}/templates/FunctionalTests.xml" "${PROJECT_DIR}/Build/phpunit/"
echo -e "${GREEN}✓ Created FunctionalTests.xml${NC}"
else
echo -e "${YELLOW}⚠ FunctionalTests.xml already exists (skipped)${NC}"
fi
if [ ! -f "${PROJECT_DIR}/Build/phpunit/FunctionalTestsBootstrap.php" ]; then
cp "${SKILL_DIR}/templates/FunctionalTestsBootstrap.php" "${PROJECT_DIR}/Build/phpunit/"
echo -e "${GREEN}✓ Created FunctionalTestsBootstrap.php${NC}"
else
echo -e "${YELLOW}⚠ FunctionalTestsBootstrap.php already exists (skipped)${NC}"
fi
# 4. Create AGENTS.md templates
echo -e "${YELLOW}[4/6] Creating AGENTS.md templates...${NC}"
for dir in "${PROJECT_DIR}/Tests/Unit" "${PROJECT_DIR}/Tests/Functional"; do
if [ ! -f "${dir}/AGENTS.md" ]; then
cp "${SKILL_DIR}/templates/AGENTS.md" "${dir}/"
echo -e "${GREEN}✓ Created ${dir}/AGENTS.md${NC}"
else
echo -e "${YELLOW}${dir}/AGENTS.md already exists (skipped)${NC}"
fi
done
# 5. Setup composer scripts
echo -e "${YELLOW}[5/6] Adding composer test scripts...${NC}"
if ! grep -q "ci:test:php:unit" "${PROJECT_DIR}/composer.json"; then
echo -e "${GREEN} Add these scripts to your composer.json:${NC}"
cat << 'EOF'
"scripts": {
"ci:test": [
"@ci:test:php:lint",
"@ci:test:php:phpstan",
"@ci:test:php:unit",
"@ci:test:php:functional"
],
"ci:test:php:lint": "phplint",
"ci:test:php:phpstan": "phpstan analyze --configuration Build/phpstan.neon --no-progress",
"ci:test:php:unit": "phpunit -c Build/phpunit/UnitTests.xml",
"ci:test:php:functional": "phpunit -c Build/phpunit/FunctionalTests.xml"
}
EOF
else
echo -e "${GREEN}✓ Test scripts already configured${NC}"
fi
# 6. Setup acceptance testing if requested
if [ "${WITH_ACCEPTANCE}" = true ]; then
echo -e "${YELLOW}[6/6] Setting up acceptance testing...${NC}"
# Install Codeception
if ! grep -q "codeception/codeception" "${PROJECT_DIR}/composer.json"; then
composer require --dev codeception/codeception codeception/module-webdriver --no-update
composer update --no-progress
echo -e "${GREEN}✓ Installed Codeception${NC}"
fi
# Create acceptance test directory
mkdir -p "${PROJECT_DIR}/Tests/Acceptance"
# Copy Docker Compose and Codeception config
if [ ! -f "${PROJECT_DIR}/Build/docker-compose.yml" ]; then
cp "${SKILL_DIR}/templates/docker/docker-compose.yml" "${PROJECT_DIR}/Build/"
echo -e "${GREEN}✓ Created docker-compose.yml${NC}"
fi
if [ ! -f "${PROJECT_DIR}/codeception.yml" ]; then
cp "${SKILL_DIR}/templates/docker/codeception.yml" "${PROJECT_DIR}/"
echo -e "${GREEN}✓ Created codeception.yml${NC}"
fi
# Initialize Codeception
if [ ! -d "${PROJECT_DIR}/Tests/Acceptance/_support" ]; then
vendor/bin/codecept bootstrap
echo -e "${GREEN}✓ Initialized Codeception${NC}"
fi
else
echo -e "${YELLOW}[6/6] Skipping acceptance testing setup (use -a flag to include)${NC}"
fi
echo
echo -e "${GREEN}=== Setup Complete ===${NC}"
echo
echo "Next steps:"
echo "1. Generate your first test:"
echo " ${SKILL_DIR}/scripts/generate-test.sh unit MyClass"
echo
echo "2. Run tests:"
echo " composer ci:test:php:unit"
echo " composer ci:test:php:functional"
echo
echo "3. Add CI/CD workflow (optional):"
echo " cp ${SKILL_DIR}/templates/github-actions-tests.yml .github/workflows/tests.yml"

135
scripts/validate-setup.sh Executable file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env bash
#
# Validate TYPO3 testing infrastructure setup
#
# Checks:
# - Required dependencies
# - PHPUnit configurations
# - Directory structure
# - Docker (for acceptance tests)
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PROJECT_DIR="$(pwd)"
ERRORS=0
WARNINGS=0
echo -e "${GREEN}=== TYPO3 Testing Setup Validation ===${NC}"
echo
# Check composer.json
echo -e "${YELLOW}[1/5] Checking composer.json dependencies...${NC}"
if [ ! -f "${PROJECT_DIR}/composer.json" ]; then
echo -e "${RED}✗ composer.json not found${NC}"
((ERRORS++))
else
if grep -q "typo3/testing-framework" "${PROJECT_DIR}/composer.json"; then
echo -e "${GREEN}✓ typo3/testing-framework installed${NC}"
else
echo -e "${RED}✗ typo3/testing-framework missing${NC}"
((ERRORS++))
fi
if grep -q "phpunit/phpunit" "${PROJECT_DIR}/composer.json"; then
echo -e "${GREEN}✓ phpunit/phpunit installed${NC}"
else
echo -e "${RED}✗ phpunit/phpunit missing${NC}"
((ERRORS++))
fi
fi
# Check PHPUnit configurations
echo -e "${YELLOW}[2/5] Checking PHPUnit configurations...${NC}"
if [ -f "${PROJECT_DIR}/Build/phpunit/UnitTests.xml" ]; then
echo -e "${GREEN}✓ UnitTests.xml present${NC}"
else
echo -e "${RED}✗ UnitTests.xml missing${NC}"
((ERRORS++))
fi
if [ -f "${PROJECT_DIR}/Build/phpunit/FunctionalTests.xml" ]; then
echo -e "${GREEN}✓ FunctionalTests.xml present${NC}"
else
echo -e "${RED}✗ FunctionalTests.xml missing${NC}"
((ERRORS++))
fi
if [ -f "${PROJECT_DIR}/Build/phpunit/FunctionalTestsBootstrap.php" ]; then
echo -e "${GREEN}✓ FunctionalTestsBootstrap.php present${NC}"
else
echo -e "${RED}✗ FunctionalTestsBootstrap.php missing${NC}"
((ERRORS++))
fi
# Check directory structure
echo -e "${YELLOW}[3/5] Checking directory structure...${NC}"
for dir in "Tests/Unit" "Tests/Functional" "Tests/Functional/Fixtures"; do
if [ -d "${PROJECT_DIR}/${dir}" ]; then
echo -e "${GREEN}${dir}/ exists${NC}"
else
echo -e "${YELLOW}${dir}/ missing${NC}"
((WARNINGS++))
fi
done
# Check AGENTS.md files
echo -e "${YELLOW}[4/5] Checking AGENTS.md documentation...${NC}"
for dir in "Tests/Unit" "Tests/Functional"; do
if [ -f "${PROJECT_DIR}/${dir}/AGENTS.md" ]; then
echo -e "${GREEN}${dir}/AGENTS.md present${NC}"
else
echo -e "${YELLOW}${dir}/AGENTS.md missing${NC}"
((WARNINGS++))
fi
done
# Check Docker (optional, for acceptance tests)
echo -e "${YELLOW}[5/5] Checking Docker availability (for acceptance tests)...${NC}"
if command -v docker &> /dev/null; then
echo -e "${GREEN}✓ Docker installed${NC}"
if docker ps &> /dev/null; then
echo -e "${GREEN}✓ Docker daemon running${NC}"
else
echo -e "${YELLOW}⚠ Docker daemon not running${NC}"
((WARNINGS++))
fi
else
echo -e "${YELLOW}⚠ Docker not installed (required for acceptance tests)${NC}"
((WARNINGS++))
fi
# Summary
echo
echo -e "${GREEN}=== Validation Summary ===${NC}"
if [ ${ERRORS} -eq 0 ] && [ ${WARNINGS} -eq 0 ]; then
echo -e "${GREEN}✓ All checks passed!${NC}"
echo
echo "Your testing infrastructure is ready to use."
echo "Generate your first test:"
echo " ~/.claude/skills/typo3-testing/scripts/generate-test.sh unit MyClass"
exit 0
elif [ ${ERRORS} -eq 0 ]; then
echo -e "${YELLOW}${WARNINGS} warnings found${NC}"
echo
echo "Basic setup is complete, but some optional components are missing."
exit 0
else
echo -e "${RED}${ERRORS} errors found${NC}"
if [ ${WARNINGS} -gt 0 ]; then
echo -e "${YELLOW}${WARNINGS} warnings found${NC}"
fi
echo
echo "Run setup script to fix errors:"
echo " ~/.claude/skills/typo3-testing/scripts/setup-testing.sh"
exit 1
fi

154
templates/AGENTS.md Normal file
View File

@@ -0,0 +1,154 @@
# Testing Context for AI Assistants
This directory contains tests for the TYPO3 extension.
## Test Type
**[Unit|Functional|Acceptance]** tests
## Test Strategy
<!-- Describe what this directory tests and why -->
<!-- Example: "Unit tests for domain models - validates business logic without database" -->
<!-- Example: "Functional tests for repositories - verifies database queries and persistence" -->
<!-- Example: "Acceptance tests for checkout workflow - validates complete user journey from cart to payment" -->
**Scope:**
**Key Scenarios:**
**Not Covered:** <!-- What is intentionally not tested here -->
## Testing Framework
- **TYPO3 Testing Framework** (typo3/testing-framework)
- **PHPUnit** for assertions and test execution
- **[Additional tools for this test type]:**
- Unit: Prophecy for mocking
- Functional: CSV fixtures for database data
- Acceptance: Codeception + Selenium for browser automation
## Test Structure
### Base Class
Tests in this directory extend:
- **Unit**: `TYPO3\TestingFramework\Core\Unit\UnitTestCase`
- **Functional**: `TYPO3\TestingFramework\Core\Functional\FunctionalTestCase`
- **Acceptance**: Codeception Cest classes
### Naming Convention
- **Unit/Functional**: `*Test.php` (e.g., `ProductTest.php`, `ProductRepositoryTest.php`)
- **Acceptance**: `*Cest.php` (e.g., `LoginCest.php`, `CheckoutCest.php`)
## Key Patterns
### setUp() and tearDown()
```php
protected function setUp(): void
{
parent::setUp();
// Initialize test dependencies
}
protected function tearDown(): void
{
// Clean up resources
parent::tearDown();
}
```
### Assertions
Use specific assertions over generic ones:
- `self::assertTrue()`, `self::assertFalse()` for booleans
- `self::assertSame()` for strict equality
- `self::assertInstanceOf()` for type checks
- `self::assertCount()` for arrays/collections
### Fixtures (Functional Tests Only)
```php
$this->importCSVDataSet(__DIR__ . '/../Fixtures/MyFixture.csv');
```
**Fixture Files:** `Tests/Functional/Fixtures/`
**Strategy:**
- Keep fixtures minimal (only required data)
- One fixture per test scenario
- Document fixture contents in test or below
### Mocking (Unit Tests Only)
```php
use Prophecy\PhpUnit\ProphecyTrait;
$repository = $this->prophesize(UserRepository::class);
$repository->findByEmail('test@example.com')->willReturn($user);
```
## Running Tests
```bash
# All tests in this directory
composer ci:test:php:[unit|functional|acceptance]
# Via runTests.sh
Build/Scripts/runTests.sh -s [unit|functional|acceptance]
# Specific test file
vendor/bin/phpunit Tests/[Unit|Functional]/Path/To/TestFile.php
# Specific test method
vendor/bin/phpunit --filter testMethodName
```
## Fixtures Documentation (Functional Tests)
<!-- Document what each fixture contains -->
### `Fixtures/BasicProducts.csv`
- 3 products in category 1
- 2 products in category 2
- All products visible and published
### `Fixtures/PageTree.csv`
- Root page (uid: 1)
- Products page (uid: 2, pid: 1)
- Services page (uid: 3, pid: 1)
## Test Dependencies
<!-- List any special dependencies or requirements -->
- [ ] Database (functional tests only)
- [ ] Docker (acceptance tests only)
- [ ] Specific TYPO3 extensions: <!-- list if any -->
- [ ] External services: <!-- list if any -->
## Common Issues
<!-- Document common test failures and solutions -->
**Database connection errors:**
- Verify database driver configuration in `FunctionalTests.xml`
- Check Docker database service is running
**Fixture import errors:**
- Verify CSV format (proper escaping, matching table structure)
- Check file paths are correct relative to test class
**Flaky tests:**
- Use proper waits in acceptance tests (`waitForElement`)
- Avoid timing dependencies in unit/functional tests
- Ensure test independence (no shared state)
## Resources
- [Unit Testing Guide](~/.claude/skills/typo3-testing/references/unit-testing.md)
- [Functional Testing Guide](~/.claude/skills/typo3-testing/references/functional-testing.md)
- [Acceptance Testing Guide](~/.claude/skills/typo3-testing/references/acceptance-testing.md)
- [TYPO3 Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/)

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../vendor/phpunit/phpunit.xsd"
bootstrap="FunctionalTestsBootstrap.php"
cacheResult="false"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
failOnRisky="true"
colors="true">
<testsuites>
<testsuite name="Functional tests">
<directory>../../Tests/Functional/</directory>
</testsuite>
</testsuites>
<php>
<const name="TYPO3_TESTING_FUNCTIONAL_REMOVE_ERROR_HANDLER" value="true" />
<env name="TYPO3_CONTEXT" value="Testing"/>
<env name="typo3DatabaseDriver" value="mysqli" force="true"/>
<env name="typo3DatabaseHost" value="localhost" force="true"/>
<env name="typo3DatabasePort" value="3306" force="true"/>
<env name="typo3DatabaseName" value="typo3_test" force="true"/>
<env name="typo3DatabaseUsername" value="root" force="true"/>
<env name="typo3DatabasePassword" value="" force="true"/>
</php>
<coverage>
<report>
<clover outputFile="../../var/log/coverage/clover.xml"/>
<html outputDirectory="../../var/log/coverage/html"/>
<text outputFile="php://stdout" showOnlySummary="true"/>
</report>
</coverage>
</phpunit>

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* Bootstrap file for TYPO3 functional tests
*
* This file initializes the testing environment for functional tests.
* It sets up the necessary directory structure and prepares the TYPO3 instance.
*/
call_user_func(static function () {
$testbase = new \TYPO3\TestingFramework\Core\Testbase();
// Define original root path
$testbase->defineOriginalRootPath();
// Create necessary directories for test execution
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests');
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient');
});

26
templates/UnitTests.xml Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="../../vendor/autoload.php"
cacheResult="false"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
failOnRisky="true"
colors="true">
<testsuites>
<testsuite name="Unit tests">
<directory>../../Tests/Unit/</directory>
</testsuite>
</testsuites>
<coverage>
<report>
<clover outputFile="../../var/log/coverage/clover.xml"/>
<html outputDirectory="../../var/log/coverage/html"/>
<text outputFile="php://stdout" showOnlySummary="true"/>
</report>
</coverage>
</phpunit>

View File

@@ -0,0 +1,39 @@
paths:
tests: Tests/Acceptance
output: var/log/acceptance
data: Tests/Acceptance/_data
support: Tests/Acceptance/_support
envs: Tests/Acceptance/_envs
actor_suffix: Tester
extensions:
enabled:
- Codeception\Extension\RunFailed
suites:
acceptance:
actor: AcceptanceTester
path: .
modules:
enabled:
- WebDriver:
url: http://web:8000
browser: chrome
host: selenium
port: 4444
wait: 2
window_size: 1920x1080
capabilities:
chromeOptions:
args: ["--no-sandbox", "--disable-dev-shm-usage"]
- \\Helper\\Acceptance
config:
WebDriver:
browser: '%BROWSER%'
settings:
shuffle: false
lint: true
colors: true
memory_limit: 1024M

View File

@@ -0,0 +1,51 @@
services:
web:
image: php:8.4-apache
container_name: typo3-test-web
volumes:
- ../../../:/var/www/html
ports:
- "8000:80"
environment:
- TYPO3_CONTEXT=Testing
- typo3DatabaseDriver=mysqli
- typo3DatabaseHost=db
- typo3DatabaseName=typo3_test
- typo3DatabaseUsername=typo3
- typo3DatabasePassword=typo3
depends_on:
db:
condition: service_healthy
networks:
- typo3-test
db:
image: mysql:8.0
container_name: typo3-test-db
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: typo3_test
MYSQL_USER: typo3
MYSQL_PASSWORD: typo3
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- typo3-test
selenium:
image: selenium/standalone-chrome:latest
container_name: typo3-test-selenium
ports:
- "4444:4444"
shm_size: 2gb
networks:
- typo3-test
networks:
typo3-test:
driver: bridge

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Acceptance;
use Vendor\Extension\Tests\Acceptance\AcceptanceTester;
/**
* Example acceptance test demonstrating TYPO3 testing patterns
*
* Acceptance tests use a real browser to test complete user workflows.
* They verify frontend functionality and user interactions.
*/
final class LoginCest
{
public function _before(AcceptanceTester $I): void
{
// Runs before each test method
// Setup: Import fixtures, reset state, etc.
}
public function loginAsBackendUser(AcceptanceTester $I): void
{
// Navigate to login page
$I->amOnPage('/typo3');
// Fill login form
$I->fillField('username', 'admin');
$I->fillField('password', 'password');
// Submit form
$I->click('Login');
// Verify successful login
$I->see('Dashboard');
$I->seeInCurrentUrl('/typo3/module/dashboard');
}
public function loginFailsWithInvalidCredentials(AcceptanceTester $I): void
{
$I->amOnPage('/typo3');
$I->fillField('username', 'admin');
$I->fillField('password', 'wrong_password');
$I->click('Login');
// Verify login failed
$I->see('Login error');
$I->seeInCurrentUrl('/typo3');
}
public function searchesForProducts(AcceptanceTester $I): void
{
// Navigate to product listing
$I->amOnPage('/products');
// Wait for page to load
$I->waitForElement('.product-list', 5);
// Use search
$I->fillField('#search', 'laptop');
$I->click('Search');
// Wait for results
$I->waitForElement('.search-results', 5);
// Verify search results
$I->see('laptop', '.product-title');
$I->seeNumberOfElements('.product-item', [1, 10]);
}
public function addsProductToCart(AcceptanceTester $I): void
{
$I->amOnPage('/products/1');
// Click add to cart button
$I->click('#add-to-cart');
// Wait for AJAX response
$I->waitForElement('.cart-badge', 3);
// Verify cart updated
$I->see('1', '.cart-badge');
$I->see('Product added to cart');
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Functional\Domain\Repository;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\Extension\Domain\Model\Product;
use Vendor\Extension\Domain\Repository\ProductRepository;
/**
* Example functional test demonstrating TYPO3 testing patterns
*
* Functional tests use a real database and full TYPO3 instance.
* They test repositories, controllers, and integration scenarios.
*/
final class ProductRepositoryTest extends FunctionalTestCase
{
protected ProductRepository $subject;
/**
* Extensions to load for this test
*/
protected array $testExtensionsToLoad = [
'typo3conf/ext/my_extension',
];
protected function setUp(): void
{
parent::setUp();
// Get repository from dependency injection container
$this->subject = $this->get(ProductRepository::class);
}
/**
* @test
*/
public function findsProductsByCategory(): void
{
// Import test data from CSV fixture
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
// Execute repository method
$products = $this->subject->findByCategory(1);
// Assert results
self::assertCount(3, $products);
self::assertInstanceOf(Product::class, $products[0]);
}
/**
* @test
*/
public function findsVisibleProductsOnly(): void
{
$this->importCSVDataSet(__DIR__ . '/../Fixtures/ProductsWithHidden.csv');
$products = $this->subject->findAll();
// Only visible products should be returned
self::assertCount(2, $products);
foreach ($products as $product) {
self::assertFalse($product->isHidden());
}
}
/**
* @test
*/
public function persistsNewProduct(): void
{
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Pages.csv');
$product = new Product();
$product->setTitle('New Product');
$product->setPrice(19.99);
$product->setPid(1);
$this->subject->add($product);
// Persist to database
$this->persistenceManager->persistAll();
// Verify product was saved
$savedProducts = $this->subject->findAll();
self::assertCount(1, $savedProducts);
self::assertSame('New Product', $savedProducts[0]->getTitle());
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Unit\Domain\Validator;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use Vendor\Extension\Domain\Validator\EmailValidator;
/**
* Example unit test demonstrating TYPO3 testing patterns
*
* Unit tests are fast, isolated tests without external dependencies.
* They test individual components (validators, utilities, domain logic).
*/
final class EmailValidatorTest extends UnitTestCase
{
protected EmailValidator $subject;
protected function setUp(): void
{
parent::setUp();
$this->subject = new EmailValidator();
}
/**
* @test
*/
public function validEmailPassesValidation(): void
{
$result = $this->subject->validate('user@example.com');
self::assertFalse($result->hasErrors());
}
/**
* @test
*/
public function invalidEmailFailsValidation(): void
{
$result = $this->subject->validate('invalid-email');
self::assertTrue($result->hasErrors());
}
/**
* @test
* @dataProvider invalidEmailProvider
*/
public function rejectsInvalidEmails(string $email): void
{
$result = $this->subject->validate($email);
self::assertTrue($result->hasErrors(), "Email '$email' should be invalid");
}
public static function invalidEmailProvider(): array
{
return [
'missing @' => ['userexample.com'],
'missing domain' => ['user@'],
'empty string' => [''],
'spaces' => ['user @example.com'],
];
}
}

View File

@@ -0,0 +1,215 @@
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
name: PHP Lint
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist
- name: Run PHP linting
run: composer ci:test:php:lint
phpstan:
name: PHPStan (Latest PHP)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Run PHPStan
run: composer ci:test:php:phpstan
security:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Run Composer audit
run: composer audit
cgl:
name: Code Style
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Check code style
run: composer ci:test:php:cgl
unit:
name: Unit Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Run unit tests
run: composer ci:test:php:unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./var/log/coverage/clover.xml
flags: unittests
name: unit-tests-php-${{ matrix.php }}
functional:
name: Functional Tests (PHP ${{ matrix.php }}, ${{ matrix.database }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
database: ['mysqli', 'pdo_mysql', 'postgres', 'sqlite']
exclude:
# Reduce matrix for faster execution
- php: '8.2'
database: 'pdo_mysql'
- php: '8.3'
database: 'postgres'
- php: '8.4'
database: 'pdo_mysql'
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: typo3_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: typo3_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.database == 'postgres' && 'pdo_pgsql' || 'mysqli' }}
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Run functional tests
env:
typo3DatabaseDriver: ${{ matrix.database }}
typo3DatabaseHost: 127.0.0.1
typo3DatabaseName: typo3_test
typo3DatabaseUsername: ${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
typo3DatabasePassword: ${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
run: composer ci:test:php:functional