commit 624998dbc34e21af3ffdffbb404f515cd2a58333 Author: Zhongwei Li Date: Sun Nov 30 08:43:33 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..5e0b308 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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": [ + "./" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..727e015 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Temporary +*.tmp +*.bak diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a210932 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5b77fb --- /dev/null +++ b/README.md @@ -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. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..96edecd --- /dev/null +++ b/SKILL.md @@ -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 + +# 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 +``` +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 diff --git a/claudedocs/refactoring-summary.md b/claudedocs/refactoring-summary.md new file mode 100644 index 0000000..9147cad --- /dev/null +++ b/claudedocs/refactoring-summary.md @@ -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: ✓ diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..d62abd9 --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file diff --git a/references/acceptance-testing.md b/references/acceptance-testing.md new file mode 100644 index 0000000..a4b1b09 --- /dev/null +++ b/references/acceptance-testing.md @@ -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 +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 +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) diff --git a/references/ci-cd.md b/references/ci-cd.md new file mode 100644 index 0000000..8b8ad76 --- /dev/null +++ b/references/ci-cd.md @@ -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) diff --git a/references/functional-testing.md b/references/functional-testing.md new file mode 100644 index 0000000..f5b93e9 --- /dev/null +++ b/references/functional-testing.md @@ -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 +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 + + + + + + + + +``` + +## 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 +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' => '

Test content with

', + ]; + + /** @var DataHandler $dataHandler */ + $dataHandler = $this->get(DataHandler::class); + + // Configure TCA for RTE field + /** @var array $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 +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","fileadmin/relative1",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 $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 $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 + + + + ../../Tests/Functional/ + + + + + + + + +``` + +### Bootstrap (Build/phpunit/FunctionalTestsBootstrap.php) + +```php +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) diff --git a/references/javascript-testing.md b/references/javascript-testing.md new file mode 100644 index 0000000..e7d4ed5 --- /dev/null +++ b/references/javascript-testing.md @@ -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 = ''; + 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 = ''; + 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 = ``; + 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(''); + let imageElement = editor.model.document.getRoot().getChild(0); + expect(imageElement.getAttribute('noScale')).toBe(true); + + // Test false value + editor.setData(''); + imageElement = editor.model.document.getRoot().getChild(0); + expect(imageElement.getAttribute('noScale')).toBe(false); + + // Test missing attribute + editor.setData(''); + 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(''); + 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 = ''; + 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 = ''; // 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) diff --git a/references/quality-tools.md b/references/quality-tools.md new file mode 100644 index 0000000..8d18249 --- /dev/null +++ b/references/quality-tools.md @@ -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 +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 +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 + **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/) diff --git a/references/test-runners.md b/references/test-runners.md new file mode 100644 index 0000000..b1cb45d --- /dev/null +++ b/references/test-runners.md @@ -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 Test suite to run (required) + unit, functional, acceptance, lint, phpstan, cgl, rector + +-d Database driver for functional tests + mysqli (default), pdo_mysql, postgres, sqlite + +-p PHP version (7.4, 8.1, 8.2, 8.3) + +-e 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 diff --git a/references/unit-testing.md b/references/unit-testing.md new file mode 100644 index 0000000..cb5b64b --- /dev/null +++ b/references/unit-testing.md @@ -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 +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 +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 +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 +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 + $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 + + + + ../../Tests/Unit/ + + + +``` + +## 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 +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' => '

Content with processing

']; + + /** @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) diff --git a/scripts/generate-test.sh b/scripts/generate-test.sh new file mode 100755 index 0000000..c48e26d --- /dev/null +++ b/scripts/generate-test.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash + +# +# Generate TYPO3 test class +# +# Usage: ./generate-test.sh +# 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 " + 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 +subject = new ${CLASS_NAME}(); + } + + /** + * @test + */ + public function canBeInstantiated(): void + { + self::assertInstanceOf(${CLASS_NAME}::class, \$this->subject); + } +} +EOF + ;; + + functional) + cat > "${TEST_FILE}" << EOF +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 +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}" diff --git a/scripts/setup-testing.sh b/scripts/setup-testing.sh new file mode 100755 index 0000000..5825668 --- /dev/null +++ b/scripts/setup-testing.sh @@ -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" diff --git a/scripts/validate-setup.sh b/scripts/validate-setup.sh new file mode 100755 index 0000000..bc3e75a --- /dev/null +++ b/scripts/validate-setup.sh @@ -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 diff --git a/templates/AGENTS.md b/templates/AGENTS.md new file mode 100644 index 0000000..469680f --- /dev/null +++ b/templates/AGENTS.md @@ -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 + + + + + + +**Scope:** + +**Key Scenarios:** + +**Not Covered:** + +## 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) + + + +### `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 + + + +- [ ] Database (functional tests only) +- [ ] Docker (acceptance tests only) +- [ ] Specific TYPO3 extensions: +- [ ] External services: + +## Common Issues + + + +**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/) diff --git a/templates/FunctionalTests.xml b/templates/FunctionalTests.xml new file mode 100644 index 0000000..72ea179 --- /dev/null +++ b/templates/FunctionalTests.xml @@ -0,0 +1,36 @@ + + + + + ../../Tests/Functional/ + + + + + + + + + + + + + + + + + + + + diff --git a/templates/FunctionalTestsBootstrap.php b/templates/FunctionalTestsBootstrap.php new file mode 100644 index 0000000..9adbcc8 --- /dev/null +++ b/templates/FunctionalTestsBootstrap.php @@ -0,0 +1,21 @@ +defineOriginalRootPath(); + + // Create necessary directories for test execution + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); +}); diff --git a/templates/UnitTests.xml b/templates/UnitTests.xml new file mode 100644 index 0000000..2ca9781 --- /dev/null +++ b/templates/UnitTests.xml @@ -0,0 +1,26 @@ + + + + + ../../Tests/Unit/ + + + + + + + + + + diff --git a/templates/docker/codeception.yml b/templates/docker/codeception.yml new file mode 100644 index 0000000..cac3c12 --- /dev/null +++ b/templates/docker/codeception.yml @@ -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 diff --git a/templates/docker/docker-compose.yml b/templates/docker/docker-compose.yml new file mode 100644 index 0000000..8a9cccc --- /dev/null +++ b/templates/docker/docker-compose.yml @@ -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 diff --git a/templates/example-tests/ExampleAcceptanceCest.php b/templates/example-tests/ExampleAcceptanceCest.php new file mode 100644 index 0000000..bc7c3bc --- /dev/null +++ b/templates/example-tests/ExampleAcceptanceCest.php @@ -0,0 +1,87 @@ +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'); + } +} diff --git a/templates/example-tests/ExampleFunctionalTest.php b/templates/example-tests/ExampleFunctionalTest.php new file mode 100644 index 0000000..be0bb19 --- /dev/null +++ b/templates/example-tests/ExampleFunctionalTest.php @@ -0,0 +1,91 @@ +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()); + } +} diff --git a/templates/example-tests/ExampleUnitTest.php b/templates/example-tests/ExampleUnitTest.php new file mode 100644 index 0000000..d022676 --- /dev/null +++ b/templates/example-tests/ExampleUnitTest.php @@ -0,0 +1,66 @@ +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'], + ]; + } +} diff --git a/templates/github-actions-tests.yml b/templates/github-actions-tests.yml new file mode 100644 index 0000000..f8145e8 --- /dev/null +++ b/templates/github-actions-tests.yml @@ -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