Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "typo3-testing",
|
||||||
|
"description": "Create and manage TYPO3 extension tests with comprehensive testing infrastructure. Includes unit, functional, and acceptance testing support, PHPUnit configurations, Docker setup, CI/CD templates, and automated test generation following TYPO3 best practices.",
|
||||||
|
"version": "1.1.0-20251114",
|
||||||
|
"author": {
|
||||||
|
"name": "Netresearch DTT GmbH",
|
||||||
|
"email": "info@netresearch.de"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
20
LICENSE
Normal file
20
LICENSE
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
[Full GPL-2.0 license text - same as typo3-docs-skill]
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# typo3-testing
|
||||||
|
|
||||||
|
Create and manage TYPO3 extension tests with comprehensive testing infrastructure. Includes unit, functional, and acceptance testing support, PHPUnit configurations, Docker setup, CI/CD templates, and automated test generation following TYPO3 best practices.
|
||||||
256
SKILL.md
Normal file
256
SKILL.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
---
|
||||||
|
name: typo3-testing
|
||||||
|
version: 1.1.0
|
||||||
|
description: Create, configure, and manage TYPO3 extension tests (unit, functional, acceptance) following official TYPO3 testing framework patterns. Use when setting up tests, writing test cases, configuring PHPUnit, managing fixtures, or integrating CI/CD pipelines for TYPO3 extensions. Covers PHPUnit 11/12, TYPO3 v12/v13 LTS, modern dependency injection testing patterns, and comprehensive quality tooling (PHPStan level 10, Rector, php-cs-fixer).
|
||||||
|
license: Complete terms in LICENSE.txt
|
||||||
|
---
|
||||||
|
|
||||||
|
# TYPO3 Testing Skill
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This skill helps developers create, configure, and manage TYPO3 extension tests following official TYPO3 testing framework patterns and community best practices.
|
||||||
|
|
||||||
|
## Quick Start Decision Tree
|
||||||
|
|
||||||
|
### 1. What do you need?
|
||||||
|
|
||||||
|
```
|
||||||
|
├─ Create new test
|
||||||
|
│ ├─ Unit test (no database, fast)
|
||||||
|
│ ├─ Functional test (with database)
|
||||||
|
│ └─ Acceptance test (browser-based E2E)
|
||||||
|
│
|
||||||
|
├─ Setup testing infrastructure
|
||||||
|
│ ├─ Basic (unit + functional)
|
||||||
|
│ └─ Full (+ acceptance with Docker)
|
||||||
|
│
|
||||||
|
├─ Add CI/CD
|
||||||
|
│ ├─ GitHub Actions
|
||||||
|
│ └─ GitLab CI
|
||||||
|
│
|
||||||
|
├─ Manage fixtures
|
||||||
|
│ ├─ Create fixture
|
||||||
|
│ └─ Update fixture
|
||||||
|
│
|
||||||
|
└─ Understand testing patterns
|
||||||
|
└─ Check references/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Which test type?
|
||||||
|
|
||||||
|
**Unit Tests** → Use when:
|
||||||
|
- Testing pure logic without external dependencies
|
||||||
|
- No database access needed
|
||||||
|
- Fast execution required (milliseconds)
|
||||||
|
- Examples: Validators, Value Objects, Utilities
|
||||||
|
|
||||||
|
**Functional Tests** → Use when:
|
||||||
|
- Testing database interactions
|
||||||
|
- Full TYPO3 instance needed
|
||||||
|
- Testing repositories, controllers, hooks
|
||||||
|
- Slower execution acceptable (seconds)
|
||||||
|
|
||||||
|
**Acceptance Tests** → Use when:
|
||||||
|
- Testing complete user workflows
|
||||||
|
- Browser interaction required
|
||||||
|
- Frontend functionality validation
|
||||||
|
- Slowest execution (minutes)
|
||||||
|
|
||||||
|
### 3. Infrastructure exists?
|
||||||
|
|
||||||
|
**No** → Run setup script:
|
||||||
|
```bash
|
||||||
|
# Basic setup (unit + functional)
|
||||||
|
scripts/setup-testing.sh
|
||||||
|
|
||||||
|
# With acceptance testing
|
||||||
|
scripts/setup-testing.sh --with-acceptance
|
||||||
|
```
|
||||||
|
|
||||||
|
**Yes** → Generate test:
|
||||||
|
```bash
|
||||||
|
# Generate test class
|
||||||
|
scripts/generate-test.sh <type> <ClassName>
|
||||||
|
|
||||||
|
# Examples:
|
||||||
|
scripts/generate-test.sh unit UserValidator
|
||||||
|
scripts/generate-test.sh functional ProductRepository
|
||||||
|
scripts/generate-test.sh acceptance LoginCest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Setup Testing Infrastructure
|
||||||
|
```bash
|
||||||
|
scripts/setup-testing.sh [--with-acceptance]
|
||||||
|
```
|
||||||
|
Creates:
|
||||||
|
- composer.json dependencies
|
||||||
|
- Build/phpunit/ configs
|
||||||
|
- Build/Scripts/runTests.sh
|
||||||
|
- Tests/ directory structure
|
||||||
|
- .github/workflows/ CI configs (optional)
|
||||||
|
- docker-compose.yml (with --with-acceptance)
|
||||||
|
|
||||||
|
### Generate Test Class
|
||||||
|
```bash
|
||||||
|
scripts/generate-test.sh <type> <ClassName>
|
||||||
|
```
|
||||||
|
Creates:
|
||||||
|
- Test class file
|
||||||
|
- Fixture file (for functional tests)
|
||||||
|
- AGENTS.md in test directory (if missing)
|
||||||
|
|
||||||
|
### Validate Setup
|
||||||
|
```bash
|
||||||
|
scripts/validate-setup.sh
|
||||||
|
```
|
||||||
|
Checks:
|
||||||
|
- Required composer dependencies
|
||||||
|
- PHPUnit configuration files
|
||||||
|
- Test directory structure
|
||||||
|
- Docker availability (for acceptance tests)
|
||||||
|
|
||||||
|
## Test Execution
|
||||||
|
|
||||||
|
### via runTests.sh (Recommended)
|
||||||
|
```bash
|
||||||
|
# Unit tests
|
||||||
|
Build/Scripts/runTests.sh -s unit
|
||||||
|
|
||||||
|
# Functional tests
|
||||||
|
Build/Scripts/runTests.sh -s functional
|
||||||
|
|
||||||
|
# Acceptance tests
|
||||||
|
Build/Scripts/runTests.sh -s acceptance
|
||||||
|
|
||||||
|
# All quality checks
|
||||||
|
Build/Scripts/runTests.sh -s lint
|
||||||
|
Build/Scripts/runTests.sh -s phpstan
|
||||||
|
Build/Scripts/runTests.sh -s cgl
|
||||||
|
```
|
||||||
|
|
||||||
|
### via Composer
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
composer ci:test
|
||||||
|
|
||||||
|
# Specific suites
|
||||||
|
composer ci:test:php:unit
|
||||||
|
composer ci:test:php:functional
|
||||||
|
|
||||||
|
# Quality tools
|
||||||
|
composer ci:test:php:lint
|
||||||
|
composer ci:test:php:phpstan
|
||||||
|
composer ci:test:php:cgl
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
Detailed documentation for each testing aspect:
|
||||||
|
|
||||||
|
- [Unit Testing](references/unit-testing.md) - UnitTestCase patterns, mocking, assertions
|
||||||
|
- [Functional Testing](references/functional-testing.md) - FunctionalTestCase, fixtures, database
|
||||||
|
- [Acceptance Testing](references/acceptance-testing.md) - Codeception, Selenium, page objects
|
||||||
|
- [JavaScript Testing](references/javascript-testing.md) - CKEditor plugins, data-* attributes, frontend tests
|
||||||
|
- [Test Runners](references/test-runners.md) - runTests.sh orchestration patterns
|
||||||
|
- [CI/CD Integration](references/ci-cd.md) - GitHub Actions, GitLab CI workflows
|
||||||
|
- [Quality Tools](references/quality-tools.md) - PHPStan, Rector, php-cs-fixer
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
Ready-to-use configuration files and examples:
|
||||||
|
|
||||||
|
- `templates/AGENTS.md` - AI assistant context template for test directories
|
||||||
|
- `templates/UnitTests.xml` - PHPUnit config for unit tests
|
||||||
|
- `templates/FunctionalTests.xml` - PHPUnit config for functional tests
|
||||||
|
- `templates/FunctionalTestsBootstrap.php` - Bootstrap for functional tests
|
||||||
|
- `templates/runTests.sh` - Test orchestration script
|
||||||
|
- `templates/github-actions-tests.yml` - GitHub Actions workflow
|
||||||
|
- `templates/docker-compose.yml` - Docker services for acceptance tests
|
||||||
|
- `templates/codeception.yml` - Codeception configuration
|
||||||
|
- `templates/example-tests/` - Example test classes
|
||||||
|
|
||||||
|
## Fixture Management (Functional Tests)
|
||||||
|
|
||||||
|
### Create Fixture
|
||||||
|
1. Create CSV file in `Tests/Functional/Fixtures/`
|
||||||
|
2. Define database table structure
|
||||||
|
3. Add test data rows
|
||||||
|
4. Import in test via `$this->importCSVDataSet()`
|
||||||
|
|
||||||
|
### Fixture Strategy
|
||||||
|
- Keep fixtures minimal (only required data)
|
||||||
|
- One fixture per test scenario
|
||||||
|
- Use descriptive names (e.g., `ProductWithCategories.csv`)
|
||||||
|
- Document fixture purpose in AGENTS.md
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
```bash
|
||||||
|
# Add workflow
|
||||||
|
cp templates/github-actions-tests.yml .github/workflows/tests.yml
|
||||||
|
|
||||||
|
# Customize matrix (PHP versions, TYPO3 versions, databases)
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitLab CI
|
||||||
|
See `references/ci-cd.md` for GitLab CI example configuration.
|
||||||
|
|
||||||
|
## Test Organization Standards
|
||||||
|
|
||||||
|
**When organizing tests**, apply these patterns:
|
||||||
|
1. Group tests by feature or domain, not by test type
|
||||||
|
2. Name unit and functional tests with `*Test.php` suffix
|
||||||
|
3. Name acceptance tests with `*Cest.php` suffix
|
||||||
|
4. Keep fixtures minimal, reusable, and well-documented
|
||||||
|
5. Use specific assertions (assertSame, assertInstanceOf) over generic assertEquals
|
||||||
|
6. Ensure each test can run independently without side effects
|
||||||
|
7. Apply setUp() and tearDown() methods consistently across test classes
|
||||||
|
8. Document test strategy in AGENTS.md to explain what each directory tests
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Tests not found:**
|
||||||
|
- Check PHPUnit XML testsuites configuration
|
||||||
|
- Verify test class extends correct base class
|
||||||
|
- Check file naming convention (*Test.php)
|
||||||
|
|
||||||
|
**Database errors in functional tests:**
|
||||||
|
- Verify database driver in FunctionalTests.xml
|
||||||
|
- Check fixture CSV format (proper escaping)
|
||||||
|
- Ensure bootstrap file is configured
|
||||||
|
|
||||||
|
**Acceptance tests fail:**
|
||||||
|
- Verify Docker and Docker Compose installed
|
||||||
|
- Check Selenium service is running
|
||||||
|
- Review Codeception configuration
|
||||||
|
|
||||||
|
## Reference Material Usage
|
||||||
|
|
||||||
|
**When understanding TYPO3 testing patterns**, read [TYPO3 Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/) for:
|
||||||
|
- Official testing framework usage
|
||||||
|
- Best practices and patterns
|
||||||
|
- Version-specific requirements
|
||||||
|
|
||||||
|
**When working with test framework internals**, check [TYPO3 Testing Framework](https://github.com/typo3/testing-framework) for:
|
||||||
|
- Framework API reference
|
||||||
|
- Base test case implementations
|
||||||
|
- Fixture handling utilities
|
||||||
|
|
||||||
|
**When seeking reference implementations**, study [Tea Extension](https://github.com/TYPO3BestPractices/tea) for:
|
||||||
|
- Production-quality test examples
|
||||||
|
- Complete testing infrastructure setup
|
||||||
|
- Best practice patterns in action
|
||||||
|
|
||||||
|
**When writing PHPUnit tests**, consult [PHPUnit Documentation](https://phpunit.de/documentation.html) for:
|
||||||
|
- Assertion methods
|
||||||
|
- Test doubles and mocking
|
||||||
|
- Configuration options
|
||||||
|
|
||||||
|
**When implementing acceptance tests**, reference [Codeception Documentation](https://codeception.com/docs/) for:
|
||||||
|
- Page object patterns
|
||||||
|
- Browser automation
|
||||||
|
- E2E test scenarios
|
||||||
44
claudedocs/refactoring-summary.md
Normal file
44
claudedocs/refactoring-summary.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# SKILL.md Refactoring Summary
|
||||||
|
|
||||||
|
**Date:** 2025-11-14
|
||||||
|
**Version Change:** 1.0.0 → 1.1.0
|
||||||
|
**Skill:** typo3-testing
|
||||||
|
|
||||||
|
## Changes Applied
|
||||||
|
|
||||||
|
### Pattern 2: Converted "## Best Practices" to Imperative Form
|
||||||
|
- **Before:** "## Best Practices" with numbered list
|
||||||
|
- **After:** "## Test Organization Standards" with imperative "When organizing tests" format
|
||||||
|
- **Changes:**
|
||||||
|
- Added "When organizing tests, apply these patterns:" trigger
|
||||||
|
- Converted each practice to action-oriented instruction
|
||||||
|
- Maintained all 8 best practices with clearer directives
|
||||||
|
|
||||||
|
### Pattern 4: Converted "## Resources" to Imperative Usage
|
||||||
|
- **Before:** Simple bullet list with URLs and descriptions
|
||||||
|
- **After:** "## Reference Material Usage" with trigger-based usage instructions
|
||||||
|
- **Changes:**
|
||||||
|
- "When understanding TYPO3 testing patterns" → TYPO3 Testing Documentation
|
||||||
|
- "When working with test framework internals" → TYPO3 Testing Framework
|
||||||
|
- "When seeking reference implementations" → Tea Extension
|
||||||
|
- "When writing PHPUnit tests" → PHPUnit Documentation
|
||||||
|
- "When implementing acceptance tests" → Codeception Documentation
|
||||||
|
- Each entry specifies what information to extract
|
||||||
|
|
||||||
|
## Impact Analysis
|
||||||
|
|
||||||
|
**Readability:** Improved - clearer when to apply each practice
|
||||||
|
**Consistency:** Aligned with typo3-ddev-skill and typo3-docs-skill patterns
|
||||||
|
**Usability:** Enhanced - readers know when to consult each resource
|
||||||
|
**Structure:** Maintained complete information while improving clarity
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `/SKILL.md` (lines 1-257)
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Version number updated in YAML frontmatter: ✓
|
||||||
|
- Best Practices converted to imperative form: ✓
|
||||||
|
- Resources converted to usage-based format: ✓
|
||||||
|
- No broken links: ✓
|
||||||
137
plugin.lock.json
Normal file
137
plugin.lock.json
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:netresearch/claude-code-marketplace:skills/typo3-testing",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "6173b9432bed4017bbcaa1c1eb4d26708b84538a",
|
||||||
|
"treeHash": "9a26ffc3c5fb57676595accbcbe5a79d217ac8fad7233ec6a0fb4754b0f4e455",
|
||||||
|
"generatedAt": "2025-11-28T10:27:19.129054Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "typo3-testing",
|
||||||
|
"description": "Create and manage TYPO3 extension tests with comprehensive testing infrastructure. Includes unit, functional, and acceptance testing support, PHPUnit configurations, Docker setup, CI/CD templates, and automated test generation following TYPO3 best practices.",
|
||||||
|
"version": "1.1.0-20251114"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "LICENSE",
|
||||||
|
"sha256": "62e5db1ab91125b874e6408a65301f10802d8204c06b3047ec07f84d7db3f471"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "f28a4c09a65697ddbcd61ba2f1fb25300f9962a060a2b24f218ef973c04eba65"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".gitignore",
|
||||||
|
"sha256": "db6cc676b541ea3d6987d9e5d662e998f36a146eba3abd14b5e65cab025fc389"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "SKILL.md",
|
||||||
|
"sha256": "8d3779f65441ab80e7c3c5f3b6f4cdee483e9d1eced56a7820330cccb79ad488"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/quality-tools.md",
|
||||||
|
"sha256": "d186766c200a72755ea9456677a8a26632b6fa5093aad847f9c4463ea56aac15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/test-runners.md",
|
||||||
|
"sha256": "ea26c2ddcc6abc23a5206665aa39c053250d09cc21c3b172f9c08f78727f4017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/acceptance-testing.md",
|
||||||
|
"sha256": "2eb3373a49f4bd584d6af026406fe89b77bf53eaf6250a5e739036fe681152b1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/functional-testing.md",
|
||||||
|
"sha256": "7a5f94e262f76b533328e6522cfce2730e03c5d4718b20dc82bf9427e59bca4f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/unit-testing.md",
|
||||||
|
"sha256": "d284a11aea0f9f74c65de21028ad1e0312a23d8f2b4a6f02d71383908a8f7a24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/javascript-testing.md",
|
||||||
|
"sha256": "d912bf3a55c3fde7d5b35dd6b8d217a2c92dc56cbce2aa833b97cbf4f930c090"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/ci-cd.md",
|
||||||
|
"sha256": "cc63f64bb0b3cc59e08c145a27e5f4a22a74ffb23c7d07d11b43276e60872fb5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "claudedocs/refactoring-summary.md",
|
||||||
|
"sha256": "23e8c66839a7c3dc5f277a82eccb2d74dcea57ebf992ca3e01a519270613e7f5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/validate-setup.sh",
|
||||||
|
"sha256": "c44fa4a6b8b0e88e6d2bbbdd4415279635a6478dd9f1e599cb27f890fef8ef92"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/setup-testing.sh",
|
||||||
|
"sha256": "e19c01d4c34abd3baa6afd6674e15095d1666909bac602273ac666f34a50e1db"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/generate-test.sh",
|
||||||
|
"sha256": "e72909650ac0cc12e9483e0ec7a84da863dce99882fa12410035460fc1c93cd3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "db557a6fe92994c05bf347cc7f7685c5130da1c4e8e246443037a12b3808c426"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/FunctionalTestsBootstrap.php",
|
||||||
|
"sha256": "60caec69cc3792c3f22184c627eb6f597e57a387c1646e2277148e877f66aa0c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/UnitTests.xml",
|
||||||
|
"sha256": "61cd16d938ba8fbb7d49c21a444fb86766ce65449f66e3d9a12135451a174786"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/github-actions-tests.yml",
|
||||||
|
"sha256": "8da189186487e10e5d5ddf0c51302138c169220a2c11306c4eab345230853d41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/AGENTS.md",
|
||||||
|
"sha256": "9a0c9055ce3404dca8f3913515de25698030855e5583367450aae2eb22c56e17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/FunctionalTests.xml",
|
||||||
|
"sha256": "8b73dedafa787328d263b46661bb4df91850e60ed12d2a33c5f493b828e79f9a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/docker/codeception.yml",
|
||||||
|
"sha256": "85c24f53e5a1f7e959312d141fd62261f0440be3ca70a45896082e0b5353dd2b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/docker/docker-compose.yml",
|
||||||
|
"sha256": "2693efec58eb5ddebd218d2a452581f21c02397fa214fea3ae46e677516881dd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/example-tests/ExampleAcceptanceCest.php",
|
||||||
|
"sha256": "30e158149508c15ecb75de850d8b6f9b09baad0014d6ab0183aba249dbd03d10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/example-tests/ExampleFunctionalTest.php",
|
||||||
|
"sha256": "81de444c741d8f05ef9abba8b005d32474da9b77a0a0aefe559350acd5f2b428"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/example-tests/ExampleUnitTest.php",
|
||||||
|
"sha256": "2c68c617a1c70f8a896c252c4aa0ed5578af5bcc7c0e7cc49257866da05fb11c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "9a26ffc3c5fb57676595accbcbe5a79d217ac8fad7233ec6a0fb4754b0f4e455"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
468
references/acceptance-testing.md
Normal file
468
references/acceptance-testing.md
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
# Acceptance Testing in TYPO3
|
||||||
|
|
||||||
|
Acceptance tests verify complete user workflows through browser automation using Codeception and Selenium.
|
||||||
|
|
||||||
|
## When to Use Acceptance Tests
|
||||||
|
|
||||||
|
- Testing complete user journeys (login → browse → checkout)
|
||||||
|
- Frontend functionality validation
|
||||||
|
- Cross-browser compatibility
|
||||||
|
- JavaScript-heavy interactions
|
||||||
|
- Visual regression testing
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker and Docker Compose
|
||||||
|
- Codeception
|
||||||
|
- Selenium (ChromeDriver or Firefox)
|
||||||
|
- Web server (Nginx/Apache)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Install Codeception
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require --dev codeception/codeception codeception/module-webdriver codeception/module-asserts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Initialize Codeception
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vendor/bin/codecept bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Docker Compose
|
||||||
|
|
||||||
|
Create `Build/docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: php:8.2-apache
|
||||||
|
volumes:
|
||||||
|
- ../../:/var/www/html
|
||||||
|
ports:
|
||||||
|
- "8000:80"
|
||||||
|
environment:
|
||||||
|
- TYPO3_CONTEXT=Testing
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mysql:8.0
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: typo3_test
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
|
||||||
|
selenium:
|
||||||
|
image: selenium/standalone-chrome:latest
|
||||||
|
ports:
|
||||||
|
- "4444:4444"
|
||||||
|
shm_size: 2gb
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Codeception Configuration
|
||||||
|
|
||||||
|
Create `codeception.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
paths:
|
||||||
|
tests: Tests/Acceptance
|
||||||
|
output: var/log/acceptance
|
||||||
|
data: Tests/Acceptance/_data
|
||||||
|
support: Tests/Acceptance/_support
|
||||||
|
envs: Tests/Acceptance/_envs
|
||||||
|
|
||||||
|
actor_suffix: Tester
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
enabled:
|
||||||
|
- Codeception\Extension\RunFailed
|
||||||
|
|
||||||
|
params:
|
||||||
|
- .env.testing
|
||||||
|
|
||||||
|
suites:
|
||||||
|
acceptance:
|
||||||
|
actor: AcceptanceTester
|
||||||
|
modules:
|
||||||
|
enabled:
|
||||||
|
- WebDriver:
|
||||||
|
url: http://web:8000
|
||||||
|
browser: chrome
|
||||||
|
host: selenium
|
||||||
|
port: 4444
|
||||||
|
- \\Helper\\Acceptance
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
### Basic Test (Cest)
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Acceptance;
|
||||||
|
|
||||||
|
use Vendor\Extension\Tests\Acceptance\AcceptanceTester;
|
||||||
|
|
||||||
|
final class LoginCest
|
||||||
|
{
|
||||||
|
public function _before(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
// Runs before each test
|
||||||
|
$I->amOnPage('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loginAsBackendUser(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$I->amOnPage('/typo3');
|
||||||
|
$I->fillField('username', 'admin');
|
||||||
|
$I->fillField('password', 'password');
|
||||||
|
$I->click('Login');
|
||||||
|
|
||||||
|
$I->see('Dashboard');
|
||||||
|
$I->seeInCurrentUrl('/typo3/module/dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loginFailsWithWrongPassword(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$I->amOnPage('/typo3');
|
||||||
|
$I->fillField('username', 'admin');
|
||||||
|
$I->fillField('password', 'wrong_password');
|
||||||
|
$I->click('Login');
|
||||||
|
|
||||||
|
$I->see('Login error');
|
||||||
|
$I->seeInCurrentUrl('/typo3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Page Objects Pattern
|
||||||
|
|
||||||
|
Create reusable page objects:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Acceptance\PageObject;
|
||||||
|
|
||||||
|
use Vendor\Extension\Tests\Acceptance\AcceptanceTester;
|
||||||
|
|
||||||
|
final class LoginPage
|
||||||
|
{
|
||||||
|
public static string $URL = '/typo3';
|
||||||
|
|
||||||
|
public static string $usernameField = '#username';
|
||||||
|
public static string $passwordField = '#password';
|
||||||
|
public static string $loginButton = 'button[type="submit"]';
|
||||||
|
|
||||||
|
private AcceptanceTester $tester;
|
||||||
|
|
||||||
|
public function __construct(AcceptanceTester $I)
|
||||||
|
{
|
||||||
|
$this->tester = $I;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login(string $username, string $password): void
|
||||||
|
{
|
||||||
|
$I = $this->tester;
|
||||||
|
|
||||||
|
$I->amOnPage(self::$URL);
|
||||||
|
$I->fillField(self::$usernameField, $username);
|
||||||
|
$I->fillField(self::$passwordField, $password);
|
||||||
|
$I->click(self::$loginButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use page object in test:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function loginWithPageObject(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$loginPage = new LoginPage($I);
|
||||||
|
$loginPage->login('admin', 'password');
|
||||||
|
|
||||||
|
$I->see('Dashboard');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Actions
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Navigate to URL
|
||||||
|
$I->amOnPage('/products');
|
||||||
|
$I->amOnUrl('https://example.com/page');
|
||||||
|
|
||||||
|
// Click links
|
||||||
|
$I->click('Products');
|
||||||
|
$I->click('#menu-products');
|
||||||
|
$I->click(['link' => 'View Details']);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Interaction
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Fill fields
|
||||||
|
$I->fillField('email', 'user@example.com');
|
||||||
|
$I->fillField('#password', 'secret');
|
||||||
|
|
||||||
|
// Select options
|
||||||
|
$I->selectOption('country', 'Germany');
|
||||||
|
$I->selectOption('category', ['Books', 'Electronics']);
|
||||||
|
|
||||||
|
// Checkboxes and radio buttons
|
||||||
|
$I->checkOption('terms');
|
||||||
|
$I->uncheckOption('newsletter');
|
||||||
|
|
||||||
|
// Submit forms
|
||||||
|
$I->submitForm('#contact-form', [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
|
||||||
|
```php
|
||||||
|
// See text
|
||||||
|
$I->see('Welcome');
|
||||||
|
$I->see('Product added', '.success-message');
|
||||||
|
$I->dontSee('Error');
|
||||||
|
|
||||||
|
// See elements
|
||||||
|
$I->seeElement('.product-list');
|
||||||
|
$I->seeElement('#add-to-cart');
|
||||||
|
$I->dontSeeElement('.error-message');
|
||||||
|
|
||||||
|
// URL checks
|
||||||
|
$I->seeInCurrentUrl('/checkout');
|
||||||
|
$I->seeCurrentUrlEquals('/thank-you');
|
||||||
|
|
||||||
|
// Field values
|
||||||
|
$I->seeInField('email', 'user@example.com');
|
||||||
|
|
||||||
|
// Number of elements
|
||||||
|
$I->seeNumberOfElements('.product-item', 10);
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Execute JavaScript
|
||||||
|
$I->executeJS('window.scrollTo(0, document.body.scrollHeight);');
|
||||||
|
|
||||||
|
// Wait for JavaScript
|
||||||
|
$I->waitForJS('return document.readyState === "complete"', 5);
|
||||||
|
|
||||||
|
// Wait for element
|
||||||
|
$I->waitForElement('.product-list', 10);
|
||||||
|
$I->waitForElementVisible('#modal', 5);
|
||||||
|
|
||||||
|
// AJAX requests
|
||||||
|
$I->waitForAjaxLoad();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Take screenshot
|
||||||
|
$I->makeScreenshot('product_page');
|
||||||
|
|
||||||
|
// Screenshot on failure (automatic in codeception.yml)
|
||||||
|
$I->makeScreenshot('FAILED_' . $test->getName());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Management
|
||||||
|
|
||||||
|
### Using Fixtures
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function _before(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
// Reset database
|
||||||
|
$I->resetDatabase();
|
||||||
|
|
||||||
|
// Import fixtures
|
||||||
|
$I->importFixture('products.sql');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Data
|
||||||
|
|
||||||
|
Create data providers:
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function productData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['name' => 'Product A', 'price' => 10.00],
|
||||||
|
['name' => 'Product B', 'price' => 20.00],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider productData
|
||||||
|
*/
|
||||||
|
public function createsProduct(AcceptanceTester $I, \Codeception\Example $example): void
|
||||||
|
{
|
||||||
|
$I->amOnPage('/admin/products/new');
|
||||||
|
$I->fillField('name', $example['name']);
|
||||||
|
$I->fillField('price', $example['price']);
|
||||||
|
$I->click('Save');
|
||||||
|
|
||||||
|
$I->see($example['name']);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Configuration
|
||||||
|
|
||||||
|
### Multiple Browsers
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# codeception.yml
|
||||||
|
suites:
|
||||||
|
acceptance:
|
||||||
|
modules:
|
||||||
|
config:
|
||||||
|
WebDriver:
|
||||||
|
browser: '%BROWSER%'
|
||||||
|
|
||||||
|
# Run with different browsers
|
||||||
|
BROWSER=chrome vendor/bin/codecept run acceptance
|
||||||
|
BROWSER=firefox vendor/bin/codecept run acceptance
|
||||||
|
```
|
||||||
|
|
||||||
|
### Headless Mode
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
WebDriver:
|
||||||
|
capabilities:
|
||||||
|
chromeOptions:
|
||||||
|
args: ['--headless', '--no-sandbox', '--disable-gpu']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### Basic Execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All acceptance tests
|
||||||
|
vendor/bin/codecept run acceptance
|
||||||
|
|
||||||
|
# Specific test
|
||||||
|
vendor/bin/codecept run acceptance LoginCest
|
||||||
|
|
||||||
|
# Specific method
|
||||||
|
vendor/bin/codecept run acceptance LoginCest:loginAsBackendUser
|
||||||
|
|
||||||
|
# With HTML report
|
||||||
|
vendor/bin/codecept run acceptance --html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via runTests.sh
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Build/Scripts/runTests.sh -s acceptance
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start services
|
||||||
|
docker-compose -f Build/docker-compose.yml up -d
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
vendor/bin/codecept run acceptance
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
docker-compose -f Build/docker-compose.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use Page Objects**: Reusable page representations
|
||||||
|
2. **Wait Strategically**: Use `waitFor*` methods for dynamic content
|
||||||
|
3. **Independent Tests**: Each test can run standalone
|
||||||
|
4. **Descriptive Names**: Clear test method names
|
||||||
|
5. **Screenshot on Failure**: Automatic debugging aid
|
||||||
|
6. **Minimal Setup**: Only necessary fixtures and data
|
||||||
|
7. **Stable Selectors**: Use IDs or data attributes, not fragile CSS
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
❌ **No Waits for Dynamic Content**
|
||||||
|
```php
|
||||||
|
$I->click('Load More');
|
||||||
|
$I->see('Product 11'); // May fail if AJAX is slow
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Proper Waits**
|
||||||
|
```php
|
||||||
|
$I->click('Load More');
|
||||||
|
$I->waitForElement('.product-item:nth-child(11)', 5);
|
||||||
|
$I->see('Product 11');
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Brittle Selectors**
|
||||||
|
```php
|
||||||
|
$I->click('div.container > div:nth-child(3) > button'); // Fragile
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Stable Selectors**
|
||||||
|
```php
|
||||||
|
$I->click('[data-test="add-to-cart"]'); // Stable
|
||||||
|
$I->click('#product-add-button'); // Better
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Large Test Scenarios**
|
||||||
|
```php
|
||||||
|
// Don't test entire user journey in one test
|
||||||
|
public function completeUserJourney() { /* 50 steps */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Focused Tests**
|
||||||
|
```php
|
||||||
|
public function addsProductToCart() { /* 5 steps */ }
|
||||||
|
public function proceedsToCheckout() { /* 7 steps */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### Interactive Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vendor/bin/codecept run acceptance --debug
|
||||||
|
vendor/bin/codecept run acceptance --steps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pause Execution
|
||||||
|
|
||||||
|
```php
|
||||||
|
$I->pauseExecution(); // Opens interactive shell
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML Reports
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vendor/bin/codecept run acceptance --html
|
||||||
|
# View report at Tests/Acceptance/_output/report.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Codeception Documentation](https://codeception.com/docs/)
|
||||||
|
- [WebDriver Module](https://codeception.com/docs/modules/WebDriver)
|
||||||
|
- [Page Objects](https://codeception.com/docs/06-ReusingTestCode#pageobjects)
|
||||||
|
- [TYPO3 Acceptance Testing](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/AcceptanceTests.html)
|
||||||
523
references/ci-cd.md
Normal file
523
references/ci-cd.md
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
# CI/CD Integration for TYPO3 Testing
|
||||||
|
|
||||||
|
Continuous Integration and Continuous Deployment workflows for automated TYPO3 extension testing.
|
||||||
|
|
||||||
|
## GitHub Actions
|
||||||
|
|
||||||
|
### Basic Workflow
|
||||||
|
|
||||||
|
Create `.github/workflows/tests.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint PHP
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress
|
||||||
|
|
||||||
|
- name: Run linting
|
||||||
|
run: composer ci:test:php:lint
|
||||||
|
|
||||||
|
phpstan:
|
||||||
|
name: PHPStan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress
|
||||||
|
|
||||||
|
- name: Run PHPStan
|
||||||
|
run: composer ci:test:php:phpstan
|
||||||
|
|
||||||
|
unit:
|
||||||
|
name: Unit Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php: ['8.1', '8.2', '8.3', '8.4']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP ${{ matrix.php }}
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
coverage: xdebug
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: composer ci:test:php:unit
|
||||||
|
|
||||||
|
- name: Upload coverage
|
||||||
|
# Upload coverage for all PHP versions
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
|
||||||
|
functional:
|
||||||
|
name: Functional Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php: ['8.1', '8.2', '8.3', '8.4']
|
||||||
|
database: ['mysqli', 'pdo_mysql', 'postgres', 'sqlite']
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
env:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: typo3_test
|
||||||
|
ports:
|
||||||
|
- 3306:3306
|
||||||
|
options: >-
|
||||||
|
--health-cmd="mysqladmin ping"
|
||||||
|
--health-interval=10s
|
||||||
|
--health-timeout=5s
|
||||||
|
--health-retries=3
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: typo3_test
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP ${{ matrix.php }}
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
extensions: ${{ matrix.database == 'postgres' && 'pdo_pgsql' || 'mysqli' }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress
|
||||||
|
|
||||||
|
- name: Run functional tests
|
||||||
|
run: |
|
||||||
|
export typo3DatabaseDriver=${{ matrix.database }}
|
||||||
|
export typo3DatabaseHost=127.0.0.1
|
||||||
|
export typo3DatabaseName=typo3_test
|
||||||
|
export typo3DatabaseUsername=${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
|
||||||
|
export typo3DatabasePassword=${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
|
||||||
|
composer ci:test:php:functional
|
||||||
|
```
|
||||||
|
|
||||||
|
### Matrix Strategy
|
||||||
|
|
||||||
|
Test multiple PHP and TYPO3 versions:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php: ['8.1', '8.2', '8.3', '8.4']
|
||||||
|
typo3: ['12.4', '13.0']
|
||||||
|
exclude:
|
||||||
|
- php: '8.1'
|
||||||
|
typo3: '13.0' # TYPO3 v13 requires PHP 8.2+
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Install TYPO3 v${{ matrix.typo3 }}
|
||||||
|
run: |
|
||||||
|
composer require "typo3/cms-core:^${{ matrix.typo3 }}" --no-update
|
||||||
|
composer update --no-progress
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching Dependencies
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache
|
||||||
|
key: composer-${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
composer-${{ runner.os }}-${{ matrix.php }}-
|
||||||
|
composer-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress --prefer-dist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Coverage
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Run tests with coverage
|
||||||
|
run: vendor/bin/phpunit -c Build/phpunit/UnitTests.xml --coverage-clover coverage.xml
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
|
flags: unittests
|
||||||
|
name: codecov-umbrella
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitLab CI
|
||||||
|
|
||||||
|
### Basic Pipeline
|
||||||
|
|
||||||
|
Create `.gitlab-ci.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
variables:
|
||||||
|
COMPOSER_CACHE_DIR: ".composer-cache"
|
||||||
|
MYSQL_ROOT_PASSWORD: "root"
|
||||||
|
MYSQL_DATABASE: "typo3_test"
|
||||||
|
|
||||||
|
cache:
|
||||||
|
key: "$CI_COMMIT_REF_SLUG"
|
||||||
|
paths:
|
||||||
|
- .composer-cache/
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- lint
|
||||||
|
- analyze
|
||||||
|
- test
|
||||||
|
|
||||||
|
.php:
|
||||||
|
image: php:${PHP_VERSION}-cli
|
||||||
|
before_script:
|
||||||
|
- apt-get update && apt-get install -y git zip unzip
|
||||||
|
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
|
- composer install --no-progress
|
||||||
|
|
||||||
|
lint:
|
||||||
|
extends: .php
|
||||||
|
stage: lint
|
||||||
|
variables:
|
||||||
|
PHP_VERSION: "8.2"
|
||||||
|
script:
|
||||||
|
- composer ci:test:php:lint
|
||||||
|
|
||||||
|
phpstan:
|
||||||
|
extends: .php
|
||||||
|
stage: analyze
|
||||||
|
variables:
|
||||||
|
PHP_VERSION: "8.2"
|
||||||
|
script:
|
||||||
|
- composer ci:test:php:phpstan
|
||||||
|
|
||||||
|
cgl:
|
||||||
|
extends: .php
|
||||||
|
stage: analyze
|
||||||
|
variables:
|
||||||
|
PHP_VERSION: "8.2"
|
||||||
|
script:
|
||||||
|
- composer ci:test:php:cgl
|
||||||
|
|
||||||
|
unit:8.1:
|
||||||
|
extends: .php
|
||||||
|
stage: test
|
||||||
|
variables:
|
||||||
|
PHP_VERSION: "8.1"
|
||||||
|
script:
|
||||||
|
- composer ci:test:php:unit
|
||||||
|
|
||||||
|
unit:8.2:
|
||||||
|
extends: .php
|
||||||
|
stage: test
|
||||||
|
variables:
|
||||||
|
PHP_VERSION: "8.2"
|
||||||
|
script:
|
||||||
|
- composer ci:test:php:unit
|
||||||
|
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
||||||
|
artifacts:
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
functional:8.2:
|
||||||
|
extends: .php
|
||||||
|
stage: test
|
||||||
|
variables:
|
||||||
|
PHP_VERSION: "8.2"
|
||||||
|
typo3DatabaseDriver: "mysqli"
|
||||||
|
typo3DatabaseHost: "mysql"
|
||||||
|
typo3DatabaseName: "typo3_test"
|
||||||
|
typo3DatabaseUsername: "root"
|
||||||
|
typo3DatabasePassword: "root"
|
||||||
|
services:
|
||||||
|
- mysql:8.0
|
||||||
|
script:
|
||||||
|
- composer ci:test:php:functional
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Database Testing
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
.functional:
|
||||||
|
extends: .php
|
||||||
|
stage: test
|
||||||
|
variables:
|
||||||
|
PHP_VERSION: "8.2"
|
||||||
|
script:
|
||||||
|
- composer ci:test:php:functional
|
||||||
|
|
||||||
|
functional:mysql:
|
||||||
|
extends: .functional
|
||||||
|
variables:
|
||||||
|
typo3DatabaseDriver: "mysqli"
|
||||||
|
typo3DatabaseHost: "mysql"
|
||||||
|
typo3DatabaseName: "typo3_test"
|
||||||
|
typo3DatabaseUsername: "root"
|
||||||
|
typo3DatabasePassword: "root"
|
||||||
|
services:
|
||||||
|
- mysql:8.0
|
||||||
|
|
||||||
|
functional:postgres:
|
||||||
|
extends: .functional
|
||||||
|
variables:
|
||||||
|
typo3DatabaseDriver: "pdo_pgsql"
|
||||||
|
typo3DatabaseHost: "postgres"
|
||||||
|
typo3DatabaseName: "typo3_test"
|
||||||
|
typo3DatabaseUsername: "postgres"
|
||||||
|
typo3DatabasePassword: "postgres"
|
||||||
|
services:
|
||||||
|
- postgres:15
|
||||||
|
before_script:
|
||||||
|
- apt-get update && apt-get install -y libpq-dev
|
||||||
|
- docker-php-ext-install pdo_pgsql
|
||||||
|
|
||||||
|
functional:sqlite:
|
||||||
|
extends: .functional
|
||||||
|
variables:
|
||||||
|
typo3DatabaseDriver: "pdo_sqlite"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Fast Feedback Loop
|
||||||
|
|
||||||
|
Order jobs by execution time (fastest first):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
stages:
|
||||||
|
- lint # ~30 seconds
|
||||||
|
- analyze # ~1-2 minutes (PHPStan, CGL)
|
||||||
|
- unit # ~2-5 minutes
|
||||||
|
- functional # ~5-15 minutes
|
||||||
|
- acceptance # ~15-30 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Fail Fast
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
strategy:
|
||||||
|
fail-fast: true # Stop on first failure
|
||||||
|
matrix:
|
||||||
|
php: ['8.1', '8.2', '8.3', '8.4']
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Parallel Execution
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions - parallel jobs
|
||||||
|
jobs:
|
||||||
|
lint: ...
|
||||||
|
phpstan: ...
|
||||||
|
unit: ...
|
||||||
|
# All run in parallel
|
||||||
|
|
||||||
|
# GitLab CI - parallel jobs
|
||||||
|
test:
|
||||||
|
parallel:
|
||||||
|
matrix:
|
||||||
|
- PHP_VERSION: ['8.1', '8.2', '8.3']
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Cache Dependencies
|
||||||
|
|
||||||
|
GitHub Actions:
|
||||||
|
```yaml
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
GitLab CI:
|
||||||
|
```yaml
|
||||||
|
cache:
|
||||||
|
key: ${CI_COMMIT_REF_SLUG}
|
||||||
|
paths:
|
||||||
|
- .composer-cache/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Matrix Testing
|
||||||
|
|
||||||
|
Test critical combinations:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Minimum supported versions
|
||||||
|
- php: '8.1'
|
||||||
|
typo3: '12.4'
|
||||||
|
|
||||||
|
# Current stable
|
||||||
|
- php: '8.2'
|
||||||
|
typo3: '12.4'
|
||||||
|
|
||||||
|
# Latest versions
|
||||||
|
- php: '8.3'
|
||||||
|
typo3: '13.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Artifacts and Reports
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Archive test results
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: |
|
||||||
|
var/log/
|
||||||
|
typo3temp/var/tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Notifications
|
||||||
|
|
||||||
|
GitHub Actions:
|
||||||
|
```yaml
|
||||||
|
- name: Slack Notification
|
||||||
|
if: failure()
|
||||||
|
uses: rtCamp/action-slack-notify@v2
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality Gates
|
||||||
|
|
||||||
|
### Required Checks
|
||||||
|
|
||||||
|
Define which checks must pass:
|
||||||
|
|
||||||
|
GitHub:
|
||||||
|
```yaml
|
||||||
|
# .github/branch-protection.json
|
||||||
|
{
|
||||||
|
"required_status_checks": {
|
||||||
|
"strict": true,
|
||||||
|
"contexts": [
|
||||||
|
"lint",
|
||||||
|
"phpstan",
|
||||||
|
"unit (8.2)",
|
||||||
|
"functional (8.2, mysqli)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
GitLab:
|
||||||
|
```yaml
|
||||||
|
# .gitlab-ci.yml
|
||||||
|
unit:8.2:
|
||||||
|
only:
|
||||||
|
- merge_requests
|
||||||
|
allow_failure: false # Required check
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage Requirements
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Check code coverage
|
||||||
|
run: |
|
||||||
|
coverage=$(vendor/bin/phpunit --coverage-text | grep "Lines:" | awk '{print $2}' | sed 's/%//')
|
||||||
|
if (( $(echo "$coverage < 80" | bc -l) )); then
|
||||||
|
echo "Coverage $coverage% is below 80%"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment-Specific Configuration
|
||||||
|
|
||||||
|
### Development Branch
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ develop ]
|
||||||
|
|
||||||
|
# Run all checks, allow failures
|
||||||
|
jobs:
|
||||||
|
experimental:
|
||||||
|
continue-on-error: true
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php: ['8.4'] # Experimental PHP version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Branch
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
# Strict checks only
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
php: ['8.2'] # LTS version only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
# Full test matrix
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php: ['8.1', '8.2', '8.3', '8.4']
|
||||||
|
database: ['mysqli', 'postgres']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [GitHub Actions Documentation](https://docs.github.com/actions)
|
||||||
|
- [GitLab CI Documentation](https://docs.gitlab.com/ee/ci/)
|
||||||
|
- [TYPO3 Tea Extension CI](https://github.com/TYPO3BestPractices/tea/tree/main/.github/workflows)
|
||||||
|
- [shivammathur/setup-php](https://github.com/shivammathur/setup-php)
|
||||||
720
references/functional-testing.md
Normal file
720
references/functional-testing.md
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
# Functional Testing in TYPO3
|
||||||
|
|
||||||
|
Functional tests verify components that interact with external systems like databases, using a full TYPO3 instance.
|
||||||
|
|
||||||
|
## When to Use Functional Tests
|
||||||
|
|
||||||
|
- Testing database operations (repositories, queries)
|
||||||
|
- Controller and plugin functionality
|
||||||
|
- Hook and event implementations
|
||||||
|
- DataHandler operations
|
||||||
|
- File and folder operations
|
||||||
|
- Extension configuration behavior
|
||||||
|
|
||||||
|
## Base Class
|
||||||
|
|
||||||
|
All functional tests extend `TYPO3\TestingFramework\Core\Functional\FunctionalTestCase`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Functional\Domain\Repository;
|
||||||
|
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
|
||||||
|
use Vendor\Extension\Domain\Model\Product;
|
||||||
|
use Vendor\Extension\Domain\Repository\ProductRepository;
|
||||||
|
|
||||||
|
final class ProductRepositoryTest extends FunctionalTestCase
|
||||||
|
{
|
||||||
|
protected ProductRepository $subject;
|
||||||
|
|
||||||
|
protected array $testExtensionsToLoad = [
|
||||||
|
'typo3conf/ext/my_extension',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->subject = $this->get(ProductRepository::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function findsProductsByCategory(): void
|
||||||
|
{
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
|
||||||
|
|
||||||
|
$products = $this->subject->findByCategory(1);
|
||||||
|
|
||||||
|
self::assertCount(3, $products);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Database
|
||||||
|
|
||||||
|
Functional tests use an isolated test database:
|
||||||
|
|
||||||
|
- Created before test execution
|
||||||
|
- Populated with fixtures
|
||||||
|
- Destroyed after test completion
|
||||||
|
- Supports: MySQL, MariaDB, PostgreSQL, SQLite
|
||||||
|
|
||||||
|
### Database Configuration
|
||||||
|
|
||||||
|
Set via environment or `FunctionalTests.xml`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<php>
|
||||||
|
<env name="typo3DatabaseDriver" value="mysqli"/>
|
||||||
|
<env name="typo3DatabaseHost" value="localhost"/>
|
||||||
|
<env name="typo3DatabasePort" value="3306"/>
|
||||||
|
<env name="typo3DatabaseUsername" value="root"/>
|
||||||
|
<env name="typo3DatabasePassword" value=""/>
|
||||||
|
<env name="typo3DatabaseName" value="typo3_test"/>
|
||||||
|
</php>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Fixtures
|
||||||
|
|
||||||
|
### CSV Format
|
||||||
|
|
||||||
|
Create fixtures in `Tests/Functional/Fixtures/`:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
# pages.csv
|
||||||
|
uid,pid,title,doktype
|
||||||
|
1,0,"Root",1
|
||||||
|
2,1,"Products",1
|
||||||
|
3,1,"Services",1
|
||||||
|
```
|
||||||
|
|
||||||
|
```csv
|
||||||
|
# tx_myext_domain_model_product.csv
|
||||||
|
uid,pid,title,price,category
|
||||||
|
1,2,"Product A",10.00,1
|
||||||
|
2,2,"Product B",20.00,1
|
||||||
|
3,2,"Product C",15.00,2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Fixtures
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function findsProducts(): void
|
||||||
|
{
|
||||||
|
// Import fixture
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
|
||||||
|
|
||||||
|
// Test repository
|
||||||
|
$products = $this->subject->findAll();
|
||||||
|
|
||||||
|
self::assertCount(3, $products);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Fixtures
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Import common fixtures
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv');
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
|
||||||
|
|
||||||
|
$this->subject = $this->get(ProductRepository::class);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
Use `$this->get()` to retrieve services:
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Get service from container
|
||||||
|
$this->subject = $this->get(ProductRepository::class);
|
||||||
|
$this->dataMapper = $this->get(DataMapper::class);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Extensions
|
||||||
|
|
||||||
|
### Load Test Extensions
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected array $testExtensionsToLoad = [
|
||||||
|
'typo3conf/ext/my_extension',
|
||||||
|
'typo3conf/ext/dependency_extension',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Extensions
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected array $coreExtensionsToLoad = [
|
||||||
|
'form',
|
||||||
|
'workspaces',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Site Configuration
|
||||||
|
|
||||||
|
Create site configuration for frontend tests:
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv');
|
||||||
|
|
||||||
|
$this->writeSiteConfiguration(
|
||||||
|
'test',
|
||||||
|
[
|
||||||
|
'rootPageId' => 1,
|
||||||
|
'base' => 'http://localhost/',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Requests
|
||||||
|
|
||||||
|
Test frontend rendering:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function rendersProductList(): void
|
||||||
|
{
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv');
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
|
||||||
|
|
||||||
|
$this->writeSiteConfiguration('test', ['rootPageId' => 1]);
|
||||||
|
|
||||||
|
$response = $this->executeFrontendSubRequest(
|
||||||
|
new InternalRequest('http://localhost/products')
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertStringContainsString('Product A', (string)$response->getBody());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing DataHandler Hooks (SC_OPTIONS)
|
||||||
|
|
||||||
|
Test DataHandler SC_OPTIONS hook integration with real framework:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Functional\Database;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
|
||||||
|
use Vendor\Extension\Database\MyDataHandlerHook;
|
||||||
|
|
||||||
|
final class MyDataHandlerHookTest extends FunctionalTestCase
|
||||||
|
{
|
||||||
|
protected array $testExtensionsToLoad = [
|
||||||
|
'typo3conf/ext/my_extension',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $coreExtensionsToLoad = [
|
||||||
|
'typo3/cms-rte-ckeditor', // If testing RTE-related hooks
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/Fixtures/pages.csv');
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/Fixtures/tt_content.csv');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createSubject(): MyDataHandlerHook
|
||||||
|
{
|
||||||
|
// Get services from container with proper DI
|
||||||
|
return new MyDataHandlerHook(
|
||||||
|
$this->get(ExtensionConfiguration::class),
|
||||||
|
$this->get(LogManager::class),
|
||||||
|
$this->get(ResourceFactory::class),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function processDatamapPostProcessFieldArrayHandlesRteField(): void
|
||||||
|
{
|
||||||
|
$subject = $this->createSubject();
|
||||||
|
|
||||||
|
$status = 'update';
|
||||||
|
$table = 'tt_content';
|
||||||
|
$id = '1';
|
||||||
|
$fieldArray = [
|
||||||
|
'bodytext' => '<p>Test content with <img src="image.jpg" /></p>',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var DataHandler $dataHandler */
|
||||||
|
$dataHandler = $this->get(DataHandler::class);
|
||||||
|
|
||||||
|
// Configure TCA for RTE field
|
||||||
|
/** @var array<string, mixed> $tcaConfig */
|
||||||
|
$tcaConfig = [
|
||||||
|
'type' => 'text',
|
||||||
|
'enableRichtext' => true,
|
||||||
|
];
|
||||||
|
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible
|
||||||
|
$GLOBALS['TCA']['tt_content']['columns']['bodytext']['config'] = $tcaConfig;
|
||||||
|
|
||||||
|
$subject->processDatamap_postProcessFieldArray(
|
||||||
|
$status,
|
||||||
|
$table,
|
||||||
|
$id,
|
||||||
|
$fieldArray,
|
||||||
|
$dataHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Field should be processed by hook
|
||||||
|
self::assertArrayHasKey('bodytext', $fieldArray);
|
||||||
|
self::assertIsString($fieldArray['bodytext']);
|
||||||
|
self::assertNotEmpty($fieldArray['bodytext']);
|
||||||
|
self::assertStringContainsString('Test content', $fieldArray['bodytext']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function hookIsRegisteredInGlobals(): void
|
||||||
|
{
|
||||||
|
// Verify hook is properly registered in TYPO3_CONF_VARS
|
||||||
|
self::assertIsArray($GLOBALS['TYPO3_CONF_VARS']);
|
||||||
|
self::assertArrayHasKey('SC_OPTIONS', $GLOBALS['TYPO3_CONF_VARS']);
|
||||||
|
|
||||||
|
$scOptions = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'];
|
||||||
|
self::assertIsArray($scOptions);
|
||||||
|
self::assertArrayHasKey('t3lib/class.t3lib_tcemain.php', $scOptions);
|
||||||
|
|
||||||
|
$tcemainOptions = $scOptions['t3lib/class.t3lib_tcemain.php'];
|
||||||
|
self::assertIsArray($tcemainOptions);
|
||||||
|
self::assertArrayHasKey('processDatamapClass', $tcemainOptions);
|
||||||
|
|
||||||
|
$registeredHooks = $tcemainOptions['processDatamapClass'];
|
||||||
|
self::assertIsArray($registeredHooks);
|
||||||
|
|
||||||
|
// Hook class should be registered
|
||||||
|
self::assertContains(MyDataHandlerHook::class, $registeredHooks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Patterns for DataHandler Hook Testing
|
||||||
|
|
||||||
|
1. **Use Factory Method Pattern**: Create `createSubject()` method to avoid uninitialized property PHPStan errors
|
||||||
|
2. **Test Real Framework Integration**: Don't mock DataHandler, test actual hook execution
|
||||||
|
3. **Configure TCA Dynamically**: Set up `$GLOBALS['TCA']` in tests for field configuration
|
||||||
|
4. **Verify Hook Registration**: Test that hooks are properly registered in `$GLOBALS['TYPO3_CONF_VARS']`
|
||||||
|
5. **Test Multiple Scenarios**: new vs update, single vs multiple fields, RTE vs non-RTE
|
||||||
|
|
||||||
|
## Testing File Abstraction Layer (FAL)
|
||||||
|
|
||||||
|
Test ResourceFactory and FAL storage integration:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Functional\Controller;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Resource\File;
|
||||||
|
use TYPO3\CMS\Core\Resource\Folder;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceStorage;
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
|
||||||
|
use Vendor\Extension\Controller\ImageRenderingController;
|
||||||
|
|
||||||
|
final class ImageRenderingControllerTest extends FunctionalTestCase
|
||||||
|
{
|
||||||
|
protected array $testExtensionsToLoad = [
|
||||||
|
'typo3conf/ext/my_extension',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/Fixtures/sys_file_storage.csv');
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/Fixtures/sys_file.csv');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function storageIsAccessible(): void
|
||||||
|
{
|
||||||
|
/** @var ResourceFactory $resourceFactory */
|
||||||
|
$resourceFactory = $this->get(ResourceFactory::class);
|
||||||
|
$storage = $resourceFactory->getStorageObject(1);
|
||||||
|
|
||||||
|
self::assertInstanceOf(ResourceStorage::class, $storage);
|
||||||
|
self::assertTrue($storage->isOnline());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function canRetrieveFileFromStorage(): void
|
||||||
|
{
|
||||||
|
/** @var ResourceFactory $resourceFactory */
|
||||||
|
$resourceFactory = $this->get(ResourceFactory::class);
|
||||||
|
|
||||||
|
// Get file from test data
|
||||||
|
$file = $resourceFactory->getFileObject(1);
|
||||||
|
|
||||||
|
self::assertInstanceOf(File::class, $file);
|
||||||
|
self::assertSame('test-image.jpg', $file->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function canAccessStorageRootFolder(): void
|
||||||
|
{
|
||||||
|
/** @var ResourceFactory $resourceFactory */
|
||||||
|
$resourceFactory = $this->get(ResourceFactory::class);
|
||||||
|
$storage = $resourceFactory->getStorageObject(1);
|
||||||
|
|
||||||
|
$rootFolder = $storage->getRootLevelFolder();
|
||||||
|
|
||||||
|
self::assertInstanceOf(Folder::class, $rootFolder);
|
||||||
|
self::assertSame('/', $rootFolder->getIdentifier());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FAL Test Fixtures
|
||||||
|
|
||||||
|
**sys_file_storage.csv:**
|
||||||
|
```csv
|
||||||
|
uid,pid,name,driver,configuration,is_default,is_browsable,is_public,is_writable,is_online
|
||||||
|
1,0,"fileadmin","Local","<?xml version=""1.0"" encoding=""utf-8"" standalone=""yes"" ?><T3FlexForms><data><sheet index=""sDEF""><language index=""lDEF""><field index=""basePath""><value index=""vDEF"">fileadmin/</value></field><field index=""pathType""><value index=""vDEF"">relative</value></field><field index=""caseSensitive""><value index=""vDEF"">1</value></field></language></sheet></data></T3FlexForms>",1,1,1,1,1
|
||||||
|
```
|
||||||
|
|
||||||
|
**sys_file.csv:**
|
||||||
|
```csv
|
||||||
|
uid,pid,storage,identifier,name,type,mime_type,size,sha1,extension
|
||||||
|
1,0,1,"/test-image.jpg","test-image.jpg",2,"image/jpeg",12345,"da39a3ee5e6b4b0d3255bfef95601890afd80709","jpg"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Patterns for FAL Testing
|
||||||
|
|
||||||
|
1. **Test Storage Configuration**: Verify storage is properly configured and online
|
||||||
|
2. **Test File Retrieval**: Use `getFileObject()` to retrieve files from sys_file
|
||||||
|
3. **Test Folder Operations**: Verify folder access and structure
|
||||||
|
4. **Use CSV Fixtures**: Import sys_file_storage and sys_file test data
|
||||||
|
5. **Test Real Services**: Use container's ResourceFactory, don't mock
|
||||||
|
|
||||||
|
## PHPStan Type Safety in Functional Tests
|
||||||
|
|
||||||
|
### Handling $GLOBALS['TCA'] with PHPStan Level 9
|
||||||
|
|
||||||
|
PHPStan cannot infer types for runtime-configured `$GLOBALS` arrays. Use ignore annotations:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Configure TCA for RTE field
|
||||||
|
/** @var array<string, mixed> $tcaConfig */
|
||||||
|
$tcaConfig = [
|
||||||
|
'type' => 'text',
|
||||||
|
'enableRichtext' => true,
|
||||||
|
];
|
||||||
|
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible
|
||||||
|
$GLOBALS['TCA']['tt_content']['columns']['bodytext']['config'] = $tcaConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Assertions for Dynamic Arrays
|
||||||
|
|
||||||
|
When testing field arrays that are modified by reference:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ PHPStan cannot verify this is still an array
|
||||||
|
self::assertStringContainsString('Test', $fieldArray['bodytext']);
|
||||||
|
|
||||||
|
// ✅ Add type assertions
|
||||||
|
self::assertArrayHasKey('bodytext', $fieldArray);
|
||||||
|
self::assertIsString($fieldArray['bodytext']);
|
||||||
|
self::assertStringContainsString('Test', $fieldArray['bodytext']);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avoiding Uninitialized Property Errors
|
||||||
|
|
||||||
|
Use factory methods instead of properties initialized in setUp():
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ PHPStan warns about uninitialized property
|
||||||
|
private MyService $subject;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->subject = $this->get(MyService::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Use factory method
|
||||||
|
private function createSubject(): MyService
|
||||||
|
{
|
||||||
|
return $this->get(MyService::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSomething(): void
|
||||||
|
{
|
||||||
|
$subject = $this->createSubject();
|
||||||
|
// Use $subject
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHPStan Annotations for Functional Tests
|
||||||
|
|
||||||
|
Common patterns:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Ignore $GLOBALS access
|
||||||
|
// @phpstan-ignore-next-line offsetAccess.nonOffsetAccessible
|
||||||
|
$GLOBALS['TCA']['table']['columns']['field']['config'] = $config;
|
||||||
|
|
||||||
|
// Type hint service retrieval
|
||||||
|
/** @var DataHandler $dataHandler */
|
||||||
|
$dataHandler = $this->get(DataHandler::class);
|
||||||
|
|
||||||
|
// Type hint config arrays
|
||||||
|
/** @var array<string, mixed> $tcaConfig */
|
||||||
|
$tcaConfig = ['type' => 'text'];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend User Context
|
||||||
|
|
||||||
|
Test with backend user:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function editorCanEditRecord(): void
|
||||||
|
{
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
|
||||||
|
|
||||||
|
$this->setUpBackendUser(1); // uid from be_users.csv
|
||||||
|
|
||||||
|
$dataHandler = $this->get(DataHandler::class);
|
||||||
|
$dataHandler->start(
|
||||||
|
[
|
||||||
|
'tx_myext_domain_model_product' => [
|
||||||
|
1 => ['title' => 'Updated Product']
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
$dataHandler->process_datamap();
|
||||||
|
|
||||||
|
self::assertEmpty($dataHandler->errorLog);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Operations
|
||||||
|
|
||||||
|
Test file handling:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function uploadsFile(): void
|
||||||
|
{
|
||||||
|
$fileStorage = $this->get(StorageRepository::class)->getDefaultStorage();
|
||||||
|
|
||||||
|
$file = $fileStorage->addFile(
|
||||||
|
__DIR__ . '/../Fixtures/Files/test.jpg',
|
||||||
|
$fileStorage->getDefaultFolder(),
|
||||||
|
'test.jpg'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertFileExists($file->getForLocalProcessing(false));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### PHPUnit XML (Build/phpunit/FunctionalTests.xml)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<phpunit
|
||||||
|
bootstrap="FunctionalTestsBootstrap.php"
|
||||||
|
cacheResult="false"
|
||||||
|
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||||
|
failOnDeprecation="true"
|
||||||
|
failOnNotice="true"
|
||||||
|
failOnWarning="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Functional tests">
|
||||||
|
<directory>../../Tests/Functional/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<php>
|
||||||
|
<const name="TYPO3_TESTING_FUNCTIONAL_REMOVE_ERROR_HANDLER" value="true" />
|
||||||
|
<env name="TYPO3_CONTEXT" value="Testing"/>
|
||||||
|
<env name="typo3DatabaseDriver" value="mysqli"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bootstrap (Build/phpunit/FunctionalTestsBootstrap.php)
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
call_user_func(static function () {
|
||||||
|
$testbase = new \TYPO3\TestingFramework\Core\Testbase();
|
||||||
|
$testbase->defineOriginalRootPath();
|
||||||
|
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests');
|
||||||
|
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fixture Strategy
|
||||||
|
|
||||||
|
### Minimal Fixtures
|
||||||
|
|
||||||
|
Keep fixtures focused on test requirements:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ Too much data
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/AllProducts.csv'); // 500 records
|
||||||
|
|
||||||
|
// ✅ Minimal test data
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/ProductsByCategory.csv'); // 3 records
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reusable Fixtures
|
||||||
|
|
||||||
|
Create shared fixtures for common scenarios:
|
||||||
|
|
||||||
|
```
|
||||||
|
Tests/Functional/Fixtures/
|
||||||
|
├── pages.csv # Basic page tree
|
||||||
|
├── be_users.csv # Test backend users
|
||||||
|
├── Products/
|
||||||
|
│ ├── BasicProducts.csv # 3 simple products
|
||||||
|
│ ├── ProductsWithCategories.csv
|
||||||
|
│ └── ProductsWithImages.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fixture Documentation
|
||||||
|
|
||||||
|
Document fixture purpose in test or AGENTS.md:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function findsProductsByCategory(): void
|
||||||
|
{
|
||||||
|
// Fixture contains: 3 products in category 1, 2 products in category 2
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/ProductsByCategory.csv');
|
||||||
|
|
||||||
|
$products = $this->subject->findByCategory(1);
|
||||||
|
|
||||||
|
self::assertCount(3, $products);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use setUp() for Common Setup**: Import shared fixtures in setUp()
|
||||||
|
2. **One Test Database**: Each test gets clean database instance
|
||||||
|
3. **Test Isolation**: Don't depend on other test execution
|
||||||
|
4. **Minimal Fixtures**: Only data required for specific test
|
||||||
|
5. **Clear Assertions**: Test specific behavior, not implementation
|
||||||
|
6. **Cleanup**: Testing framework handles cleanup automatically
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
❌ **Large Fixtures**
|
||||||
|
```php
|
||||||
|
// Don't import unnecessary data
|
||||||
|
$this->importCSVDataSet('AllData.csv'); // 10,000 records
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **No Fixtures**
|
||||||
|
```php
|
||||||
|
// Don't expect data to exist
|
||||||
|
$products = $this->subject->findAll();
|
||||||
|
self::assertCount(0, $products); // Always true without fixtures
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Missing Extensions**
|
||||||
|
```php
|
||||||
|
// Don't forget to load extension under test
|
||||||
|
// Missing: protected array $testExtensionsToLoad = ['typo3conf/ext/my_extension'];
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Focused, Well-Documented Tests**
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function findsByCategory(): void
|
||||||
|
{
|
||||||
|
// Fixture: 3 products in category 1
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/CategoryProducts.csv');
|
||||||
|
|
||||||
|
$products = $this->subject->findByCategory(1);
|
||||||
|
|
||||||
|
self::assertCount(3, $products);
|
||||||
|
self::assertSame('Product A', $products[0]->getTitle());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Functional Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via runTests.sh
|
||||||
|
Build/Scripts/runTests.sh -s functional
|
||||||
|
|
||||||
|
# Via PHPUnit directly
|
||||||
|
vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml
|
||||||
|
|
||||||
|
# Via Composer
|
||||||
|
composer ci:test:php:functional
|
||||||
|
|
||||||
|
# With specific database
|
||||||
|
typo3DatabaseDriver=pdo_mysql vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml
|
||||||
|
|
||||||
|
# Single test
|
||||||
|
vendor/bin/phpunit Tests/Functional/Domain/Repository/ProductRepositoryTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [TYPO3 Functional Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/FunctionalTests.html)
|
||||||
|
- [Testing Framework](https://github.com/typo3/testing-framework)
|
||||||
|
- [CSV Fixture Format](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/FunctionalTests.html#importing-data)
|
||||||
499
references/javascript-testing.md
Normal file
499
references/javascript-testing.md
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
# JavaScript and CKEditor Testing
|
||||||
|
|
||||||
|
**Purpose:** Testing patterns for TYPO3 CKEditor plugins, JavaScript functionality, and frontend code
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
While TYPO3 extensions are primarily PHP, many include JavaScript for:
|
||||||
|
- CKEditor custom plugins and features
|
||||||
|
- Backend module interactions
|
||||||
|
- Frontend enhancements
|
||||||
|
- RTE (Rich Text Editor) extensions
|
||||||
|
|
||||||
|
This guide covers testing patterns for JavaScript code in TYPO3 extensions.
|
||||||
|
|
||||||
|
## CKEditor Plugin Testing
|
||||||
|
|
||||||
|
### Testing Model Attributes
|
||||||
|
|
||||||
|
CKEditor plugins define model attributes that must be properly handled through upcast (view→model) and downcast (model→view) conversions.
|
||||||
|
|
||||||
|
**Example from t3x-rte_ckeditor_image:**
|
||||||
|
|
||||||
|
The plugin added a `noScale` attribute to prevent image processing. This requires testing:
|
||||||
|
|
||||||
|
1. **Attribute schema registration**
|
||||||
|
2. **Upcast conversion** (HTML → CKEditor model)
|
||||||
|
3. **Downcast conversion** (CKEditor model → HTML)
|
||||||
|
4. **UI interaction** (dialog checkbox)
|
||||||
|
|
||||||
|
### Test Structure Pattern
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Resources/Public/JavaScript/Plugins/__tests__/typo3image.test.js
|
||||||
|
|
||||||
|
import { typo3image } from '../typo3image';
|
||||||
|
|
||||||
|
describe('TYPO3 Image Plugin', () => {
|
||||||
|
let editor;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
editor = await createTestEditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return editor.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model Schema', () => {
|
||||||
|
it('should allow noScale attribute', () => {
|
||||||
|
const schema = editor.model.schema;
|
||||||
|
expect(schema.checkAttribute('typo3image', 'noScale')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Upcast Conversion', () => {
|
||||||
|
it('should read data-noscale from HTML', () => {
|
||||||
|
const html = '<img src="test.jpg" data-noscale="true" />';
|
||||||
|
editor.setData(html);
|
||||||
|
|
||||||
|
const imageElement = editor.model.document.getRoot()
|
||||||
|
.getChild(0);
|
||||||
|
|
||||||
|
expect(imageElement.getAttribute('noScale')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing data-noscale attribute', () => {
|
||||||
|
const html = '<img src="test.jpg" />';
|
||||||
|
editor.setData(html);
|
||||||
|
|
||||||
|
const imageElement = editor.model.document.getRoot()
|
||||||
|
.getChild(0);
|
||||||
|
|
||||||
|
expect(imageElement.getAttribute('noScale')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Downcast Conversion', () => {
|
||||||
|
it('should write data-noscale to HTML when enabled', () => {
|
||||||
|
editor.model.change(writer => {
|
||||||
|
const imageElement = writer.createElement('typo3image', {
|
||||||
|
src: 'test.jpg',
|
||||||
|
noScale: true
|
||||||
|
});
|
||||||
|
writer.insert(imageElement, editor.model.document.getRoot(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = editor.getData();
|
||||||
|
expect(html).toContain('data-noscale="true"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should omit data-noscale when disabled', () => {
|
||||||
|
editor.model.change(writer => {
|
||||||
|
const imageElement = writer.createElement('typo3image', {
|
||||||
|
src: 'test.jpg',
|
||||||
|
noScale: false
|
||||||
|
});
|
||||||
|
writer.insert(imageElement, editor.model.document.getRoot(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = editor.getData();
|
||||||
|
expect(html).not.toContain('data-noscale');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing data-* Attributes
|
||||||
|
|
||||||
|
Many TYPO3 CKEditor plugins use `data-*` attributes to pass information from editor to server-side rendering.
|
||||||
|
|
||||||
|
**Common Patterns:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
describe('data-* Attribute Handling', () => {
|
||||||
|
it('should preserve TYPO3-specific attributes', () => {
|
||||||
|
const testCases = [
|
||||||
|
{ attr: 'data-htmlarea-file-uid', value: '123' },
|
||||||
|
{ attr: 'data-htmlarea-file-table', value: 'sys_file' },
|
||||||
|
{ attr: 'data-htmlarea-zoom', value: 'true' },
|
||||||
|
{ attr: 'data-noscale', value: 'true' },
|
||||||
|
{ attr: 'data-alt-override', value: 'false' },
|
||||||
|
{ attr: 'data-title-override', value: 'true' }
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ attr, value }) => {
|
||||||
|
const html = `<img src="test.jpg" ${attr}="${value}" />`;
|
||||||
|
editor.setData(html);
|
||||||
|
|
||||||
|
// Verify upcast preserves attribute
|
||||||
|
const output = editor.getData();
|
||||||
|
expect(output).toContain(`${attr}="${value}"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle boolean data attributes', () => {
|
||||||
|
// Test true value
|
||||||
|
editor.setData('<img src="test.jpg" data-noscale="true" />');
|
||||||
|
let imageElement = editor.model.document.getRoot().getChild(0);
|
||||||
|
expect(imageElement.getAttribute('noScale')).toBe(true);
|
||||||
|
|
||||||
|
// Test false value
|
||||||
|
editor.setData('<img src="test.jpg" data-noscale="false" />');
|
||||||
|
imageElement = editor.model.document.getRoot().getChild(0);
|
||||||
|
expect(imageElement.getAttribute('noScale')).toBe(false);
|
||||||
|
|
||||||
|
// Test missing attribute
|
||||||
|
editor.setData('<img src="test.jpg" />');
|
||||||
|
imageElement = editor.model.document.getRoot().getChild(0);
|
||||||
|
expect(imageElement.getAttribute('noScale')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Dialog UI
|
||||||
|
|
||||||
|
CKEditor dialogs require testing user interactions:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
describe('Image Dialog', () => {
|
||||||
|
let dialog, $checkbox;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dialog = createImageDialog(editor);
|
||||||
|
$checkbox = dialog.$el.find('#checkbox-noscale');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display noScale checkbox', () => {
|
||||||
|
expect($checkbox.length).toBe(1);
|
||||||
|
expect($checkbox.parent('label').text())
|
||||||
|
.toContain('Use original file (noScale)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set noScale attribute when checkbox checked', () => {
|
||||||
|
$checkbox.prop('checked', true);
|
||||||
|
dialog.save();
|
||||||
|
|
||||||
|
const imageElement = getSelectedImage(editor);
|
||||||
|
expect(imageElement.getAttribute('noScale')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove noScale attribute when checkbox unchecked', () => {
|
||||||
|
// Start with noScale enabled
|
||||||
|
const imageElement = getSelectedImage(editor);
|
||||||
|
editor.model.change(writer => {
|
||||||
|
writer.setAttribute('noScale', true, imageElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Uncheck and save
|
||||||
|
$checkbox.prop('checked', false);
|
||||||
|
dialog.save();
|
||||||
|
|
||||||
|
expect(imageElement.getAttribute('noScale')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load checkbox state from existing attribute', () => {
|
||||||
|
const imageElement = getSelectedImage(editor);
|
||||||
|
editor.model.change(writer => {
|
||||||
|
writer.setAttribute('noScale', true, imageElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog = createImageDialog(editor);
|
||||||
|
$checkbox = dialog.$el.find('#checkbox-noscale');
|
||||||
|
|
||||||
|
expect($checkbox.prop('checked')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript Test Frameworks
|
||||||
|
|
||||||
|
### Jest (Recommended)
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
```bash
|
||||||
|
npm install --save-dev jest @babel/preset-env
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration (jest.config.js):**
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.js$': 'babel-jest'
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
'\\.(css|less|scss)$': 'identity-obj-proxy'
|
||||||
|
},
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'Resources/Public/JavaScript/**/*.js',
|
||||||
|
'!Resources/Public/JavaScript/**/*.test.js',
|
||||||
|
'!Resources/Public/JavaScript/**/__tests__/**'
|
||||||
|
],
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 70,
|
||||||
|
functions: 70,
|
||||||
|
lines: 70,
|
||||||
|
statements: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mocha + Chai
|
||||||
|
|
||||||
|
Alternative for projects already using Mocha:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// test/javascript/typo3image.test.js
|
||||||
|
const { expect } = require('chai');
|
||||||
|
const { JSDOM } = require('jsdom');
|
||||||
|
|
||||||
|
describe('TYPO3 Image Plugin', function() {
|
||||||
|
let editor;
|
||||||
|
|
||||||
|
beforeEach(async function() {
|
||||||
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
||||||
|
global.window = dom.window;
|
||||||
|
global.document = window.document;
|
||||||
|
|
||||||
|
editor = await createTestEditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle noScale attribute', function() {
|
||||||
|
// Test implementation
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Best Practices
|
||||||
|
|
||||||
|
### 1. Isolate Editor Instance
|
||||||
|
|
||||||
|
Each test should use a fresh editor instance:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function createTestEditor() {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
const editor = await ClassicEditor.create(div, {
|
||||||
|
plugins: [Typo3Image, /* other plugins */],
|
||||||
|
typo3image: {
|
||||||
|
/* plugin config */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Clean Up After Tests
|
||||||
|
|
||||||
|
Prevent memory leaks and DOM pollution:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
afterEach(async () => {
|
||||||
|
if (editor) {
|
||||||
|
await editor.destroy();
|
||||||
|
editor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any test DOM elements
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Both Happy Path and Edge Cases
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
describe('Attribute Validation', () => {
|
||||||
|
it('should handle valid boolean values', () => {
|
||||||
|
// Happy path
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid attribute values', () => {
|
||||||
|
const html = '<img src="test.jpg" data-noscale="invalid" />';
|
||||||
|
editor.setData(html);
|
||||||
|
|
||||||
|
// Should default to false
|
||||||
|
const imageElement = editor.model.document.getRoot().getChild(0);
|
||||||
|
expect(imageElement.getAttribute('noScale')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle malformed HTML', () => {
|
||||||
|
const html = '<img src="test.jpg" data-noscale>'; // Missing value
|
||||||
|
// Test graceful handling
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Mock Backend Interactions
|
||||||
|
|
||||||
|
For plugins that communicate with TYPO3 backend:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
beforeEach(() => {
|
||||||
|
global.fetch = jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
json: () => Promise.resolve({ success: true })
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch image metadata from backend', async () => {
|
||||||
|
await plugin.fetchImageMetadata(123);
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledWith(
|
||||||
|
'/typo3/ajax/image/metadata/123',
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with PHP Tests
|
||||||
|
|
||||||
|
JavaScript tests complement PHP unit tests:
|
||||||
|
|
||||||
|
**PHP Side (Backend):**
|
||||||
|
```php
|
||||||
|
// Tests/Unit/Controller/ImageRenderingControllerTest.php
|
||||||
|
public function testNoScaleAttribute(): void
|
||||||
|
{
|
||||||
|
$attributes = ['data-noscale' => 'true'];
|
||||||
|
$result = $this->controller->render($attributes);
|
||||||
|
|
||||||
|
// Verify noScale parameter passed to imgResource
|
||||||
|
$this->assertStringContainsString('noScale=1', $result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript Side (Frontend):**
|
||||||
|
```javascript
|
||||||
|
// Resources/Public/JavaScript/__tests__/typo3image.test.js
|
||||||
|
it('should generate data-noscale attribute', () => {
|
||||||
|
// Verify attribute is created in editor
|
||||||
|
editor.model.change(writer => {
|
||||||
|
const img = writer.createElement('typo3image', {
|
||||||
|
noScale: true
|
||||||
|
});
|
||||||
|
writer.insert(img, editor.model.document.getRoot(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editor.getData()).toContain('data-noscale="true"');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Together:** These tests ensure end-to-end functionality from editor UI → HTML attribute → PHP backend processing.
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
Add JavaScript tests to your CI pipeline:
|
||||||
|
|
||||||
|
**package.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
|
"test:coverage": "jest --coverage",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^29.0.0",
|
||||||
|
"@babel/preset-env": "^7.20.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**GitHub Actions:**
|
||||||
|
```yaml
|
||||||
|
- name: Run JavaScript tests
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
- name: Upload JS coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: ./coverage/lcov.info
|
||||||
|
flags: javascript
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Complete Test Suite
|
||||||
|
|
||||||
|
See `t3x-rte_ckeditor_image` for a real-world example:
|
||||||
|
|
||||||
|
```
|
||||||
|
t3x-rte_ckeditor_image/
|
||||||
|
├── Resources/Public/JavaScript/
|
||||||
|
│ └── Plugins/
|
||||||
|
│ ├── typo3image.js # Main plugin
|
||||||
|
│ └── __tests__/
|
||||||
|
│ └── typo3image.test.js # JavaScript tests
|
||||||
|
└── Tests/Unit/
|
||||||
|
└── Controller/
|
||||||
|
└── ImageRenderingControllerTest.php # PHP tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Lessons:**
|
||||||
|
1. Test attribute schema registration
|
||||||
|
2. Test upcast/downcast conversions separately
|
||||||
|
3. Test UI interactions (checkboxes, inputs)
|
||||||
|
4. Test data-* attribute preservation
|
||||||
|
5. Clean up editor instances to prevent leaks
|
||||||
|
6. Mock backend API calls
|
||||||
|
7. Coordinate with PHP tests for full coverage
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Tests Pass Locally but Fail in CI
|
||||||
|
|
||||||
|
**Cause:** DOM environment differences
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```javascript
|
||||||
|
// jest.config.js
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
testEnvironmentOptions: {
|
||||||
|
url: 'http://localhost'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Leaks in Test Suite
|
||||||
|
|
||||||
|
**Cause:** Editor instances not properly destroyed
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```javascript
|
||||||
|
afterEach(async () => {
|
||||||
|
if (editor && !editor.state === 'destroyed') {
|
||||||
|
await editor.destroy();
|
||||||
|
}
|
||||||
|
editor = null;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Test Failures
|
||||||
|
|
||||||
|
**Cause:** Not waiting for editor initialization
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```javascript
|
||||||
|
beforeEach(async () => {
|
||||||
|
editor = await ClassicEditor.create(/* ... */);
|
||||||
|
// ☝️ await is critical
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [CKEditor 5 Testing](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/testing-environment.html)
|
||||||
|
- [Jest Documentation](https://jestjs.io/docs/getting-started)
|
||||||
|
- [TYPO3 RTE CKEditor Image](https://github.com/netresearch/t3x-rte_ckeditor_image)
|
||||||
395
references/quality-tools.md
Normal file
395
references/quality-tools.md
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
# Quality Tools for TYPO3 Development
|
||||||
|
|
||||||
|
Automated code quality and static analysis tools for TYPO3 extensions.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
- **PHPStan**: Static analysis for type safety and bugs
|
||||||
|
- **Rector**: Automated code refactoring and modernization
|
||||||
|
- **php-cs-fixer**: Code style enforcement (PSR-12, TYPO3 CGL)
|
||||||
|
- **phplint**: PHP syntax validation
|
||||||
|
|
||||||
|
## PHPStan
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require --dev phpstan/phpstan phpstan/phpstan-strict-rules saschaegerer/phpstan-typo3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Create `Build/phpstan.neon`:
|
||||||
|
|
||||||
|
```neon
|
||||||
|
includes:
|
||||||
|
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||||
|
- vendor/saschaegerer/phpstan-typo3/extension.neon
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
level: max # Level 10 - maximum strictness
|
||||||
|
paths:
|
||||||
|
- Classes
|
||||||
|
- Tests
|
||||||
|
excludePaths:
|
||||||
|
- Tests/Acceptance/_output/*
|
||||||
|
reportUnmatchedIgnoredErrors: true
|
||||||
|
checkGenericClassInNonGenericObjectType: false
|
||||||
|
checkMissingIterableValueType: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running PHPStan
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via runTests.sh
|
||||||
|
Build/Scripts/runTests.sh -s phpstan
|
||||||
|
|
||||||
|
# Directly
|
||||||
|
vendor/bin/phpstan analyze --configuration Build/phpstan.neon
|
||||||
|
|
||||||
|
# With baseline (ignore existing errors)
|
||||||
|
vendor/bin/phpstan analyze --generate-baseline Build/phpstan-baseline.neon
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
vendor/bin/phpstan clear-result-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHPStan Rule Levels
|
||||||
|
|
||||||
|
**Level 0-10** (use `max` for level 10): Increasing strictness
|
||||||
|
- **Level 0**: Basic checks (undefined variables, unknown functions)
|
||||||
|
- **Level 5**: Type checks, unknown properties, unknown methods
|
||||||
|
- **Level 9**: Strict mixed types, unused parameters
|
||||||
|
- **Level 10 (max)**: Maximum strictness - explicit mixed types, pure functions
|
||||||
|
|
||||||
|
**Recommendation**: Start with level 5, aim for level 10 (max) in modern TYPO3 13 projects.
|
||||||
|
|
||||||
|
**Why Level 10?**
|
||||||
|
- Enforces explicit type declarations (`mixed` must be declared, not implicit)
|
||||||
|
- Catches more potential bugs at development time
|
||||||
|
- Aligns with TYPO3 13 strict typing standards (`declare(strict_types=1)`)
|
||||||
|
- Required for PHPStan Level 10 compliant extensions
|
||||||
|
|
||||||
|
### Ignoring Errors
|
||||||
|
|
||||||
|
```php
|
||||||
|
/** @phpstan-ignore-next-line */
|
||||||
|
$value = $this->legacyMethod();
|
||||||
|
|
||||||
|
// Or in neon file
|
||||||
|
parameters:
|
||||||
|
ignoreErrors:
|
||||||
|
- '#Call to an undefined method.*::getRepository\(\)#'
|
||||||
|
```
|
||||||
|
|
||||||
|
### TYPO3-Specific Rules
|
||||||
|
|
||||||
|
```php
|
||||||
|
// PHPStan understands TYPO3 classes
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getQueryBuilderForTable('pages');
|
||||||
|
// ✅ PHPStan knows this returns QueryBuilder
|
||||||
|
|
||||||
|
// Detects TYPO3 API misuse
|
||||||
|
TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(MyService::class);
|
||||||
|
// ✅ Checks if MyService is a valid class
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rector
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require --dev rector/rector ssch/typo3-rector
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Create `rector.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Rector\Config\RectorConfig;
|
||||||
|
use Rector\Set\ValueObject\LevelSetList;
|
||||||
|
use Rector\Set\ValueObject\SetList;
|
||||||
|
use Ssch\TYPO3Rector\Set\Typo3SetList;
|
||||||
|
|
||||||
|
return RectorConfig::configure()
|
||||||
|
->withPaths([
|
||||||
|
__DIR__ . '/Classes',
|
||||||
|
__DIR__ . '/Tests',
|
||||||
|
])
|
||||||
|
->withSkip([
|
||||||
|
__DIR__ . '/Tests/Acceptance/_output',
|
||||||
|
])
|
||||||
|
->withPhpSets(php82: true)
|
||||||
|
->withSets([
|
||||||
|
LevelSetList::UP_TO_PHP_82,
|
||||||
|
SetList::CODE_QUALITY,
|
||||||
|
SetList::DEAD_CODE,
|
||||||
|
SetList::TYPE_DECLARATION,
|
||||||
|
Typo3SetList::TYPO3_13,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Rector
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry run (show changes)
|
||||||
|
vendor/bin/rector process --dry-run
|
||||||
|
|
||||||
|
# Apply changes
|
||||||
|
vendor/bin/rector process
|
||||||
|
|
||||||
|
# Via runTests.sh
|
||||||
|
Build/Scripts/runTests.sh -s rector
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Refactorings
|
||||||
|
|
||||||
|
**TYPO3 API Modernization**:
|
||||||
|
```php
|
||||||
|
// Before
|
||||||
|
$GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'pages', 'uid=1');
|
||||||
|
|
||||||
|
// After (Rector auto-refactors)
|
||||||
|
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getConnectionForTable('pages')
|
||||||
|
->select(['*'], 'pages', ['uid' => 1])
|
||||||
|
->fetchAllAssociative();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Type Declarations**:
|
||||||
|
```php
|
||||||
|
// Before
|
||||||
|
public function process($data)
|
||||||
|
{
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
public function process(array $data): array
|
||||||
|
{
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## php-cs-fixer
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require --dev friendsofphp/php-cs-fixer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Create `Build/php-cs-fixer.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$finder = (new PhpCsFixer\Finder())
|
||||||
|
->in(__DIR__ . '/../Classes')
|
||||||
|
->in(__DIR__ . '/../Tests')
|
||||||
|
->exclude('_output');
|
||||||
|
|
||||||
|
return (new PhpCsFixer\Config())
|
||||||
|
->setRules([
|
||||||
|
'@PSR12' => true,
|
||||||
|
'@PhpCsFixer' => true,
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
'concat_space' => ['spacing' => 'one'],
|
||||||
|
'declare_strict_types' => true,
|
||||||
|
'ordered_imports' => ['sort_algorithm' => 'alpha'],
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
'single_line_throw' => false,
|
||||||
|
'phpdoc_align' => false,
|
||||||
|
'phpdoc_no_empty_return' => false,
|
||||||
|
'phpdoc_summary' => false,
|
||||||
|
])
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setFinder($finder);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running php-cs-fixer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check only (dry run)
|
||||||
|
vendor/bin/php-cs-fixer fix --config Build/php-cs-fixer.php --dry-run --diff
|
||||||
|
|
||||||
|
# Fix files
|
||||||
|
vendor/bin/php-cs-fixer fix --config Build/php-cs-fixer.php
|
||||||
|
|
||||||
|
# Via runTests.sh
|
||||||
|
Build/Scripts/runTests.sh -s cgl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Rules
|
||||||
|
|
||||||
|
```php
|
||||||
|
// array_syntax: short
|
||||||
|
$array = [1, 2, 3]; // ✅
|
||||||
|
$array = array(1, 2, 3); // ❌
|
||||||
|
|
||||||
|
// concat_space: one
|
||||||
|
$message = 'Hello ' . $name; // ✅
|
||||||
|
$message = 'Hello '.$name; // ❌
|
||||||
|
|
||||||
|
// declare_strict_types
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1); // ✅ Required at top of file
|
||||||
|
|
||||||
|
// ordered_imports
|
||||||
|
use Vendor\Extension\Domain\Model\Product; // ✅ Alphabetical
|
||||||
|
use Vendor\Extension\Domain\Repository\ProductRepository;
|
||||||
|
```
|
||||||
|
|
||||||
|
## phplint
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require --dev overtrue/phplint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Create `.phplint.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
path: ./
|
||||||
|
jobs: 10
|
||||||
|
cache: var/cache/phplint.cache
|
||||||
|
exclude:
|
||||||
|
- vendor
|
||||||
|
- var
|
||||||
|
- .Build
|
||||||
|
extensions:
|
||||||
|
- php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running phplint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lint all PHP files
|
||||||
|
vendor/bin/phplint
|
||||||
|
|
||||||
|
# Via runTests.sh
|
||||||
|
Build/Scripts/runTests.sh -s lint
|
||||||
|
|
||||||
|
# Specific directory
|
||||||
|
vendor/bin/phplint Classes/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composer Script Integration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"ci:test:php:lint": "phplint",
|
||||||
|
"ci:test:php:phpstan": "phpstan analyze --configuration Build/phpstan.neon --no-progress",
|
||||||
|
"ci:test:php:rector": "rector process --dry-run",
|
||||||
|
"ci:test:php:cgl": "php-cs-fixer fix --config Build/php-cs-fixer.php --dry-run --diff",
|
||||||
|
"ci:test:php:security": "composer audit",
|
||||||
|
|
||||||
|
"fix:cgl": "php-cs-fixer fix --config Build/php-cs-fixer.php",
|
||||||
|
"fix:rector": "rector process",
|
||||||
|
|
||||||
|
"ci:test": [
|
||||||
|
"@ci:test:php:lint",
|
||||||
|
"@ci:test:php:phpstan",
|
||||||
|
"@ci:test:php:rector",
|
||||||
|
"@ci:test:php:cgl",
|
||||||
|
"@ci:test:php:security"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Security Note**: `composer audit` checks for known security vulnerabilities in dependencies. Run this regularly and especially before releases.
|
||||||
|
|
||||||
|
## Pre-commit Hook
|
||||||
|
|
||||||
|
Create `.git/hooks/pre-commit`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "Running quality checks..."
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
vendor/bin/phplint || exit 1
|
||||||
|
|
||||||
|
# PHPStan
|
||||||
|
vendor/bin/phpstan analyze --configuration Build/phpstan.neon --error-format=table --no-progress || exit 1
|
||||||
|
|
||||||
|
# Code style
|
||||||
|
vendor/bin/php-cs-fixer fix --config Build/php-cs-fixer.php --dry-run --diff || exit 1
|
||||||
|
|
||||||
|
echo "✓ All checks passed"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4' # Use latest PHP for quality tools
|
||||||
|
- run: composer install
|
||||||
|
- run: composer ci:test:php:lint
|
||||||
|
- run: composer ci:test:php:phpstan
|
||||||
|
- run: composer ci:test:php:cgl
|
||||||
|
- run: composer ci:test:php:rector
|
||||||
|
- run: composer ci:test:php:security
|
||||||
|
```
|
||||||
|
|
||||||
|
## IDE Integration
|
||||||
|
|
||||||
|
### PHPStorm
|
||||||
|
|
||||||
|
1. **PHPStan**: Settings → PHP → Quality Tools → PHPStan
|
||||||
|
2. **php-cs-fixer**: Settings → PHP → Quality Tools → PHP CS Fixer
|
||||||
|
3. **File Watchers**: Auto-run on file save
|
||||||
|
|
||||||
|
### VS Code
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"php.validate.executablePath": "/usr/bin/php",
|
||||||
|
"phpstan.enabled": true,
|
||||||
|
"phpstan.configFile": "Build/phpstan.neon",
|
||||||
|
"php-cs-fixer.onsave": true,
|
||||||
|
"php-cs-fixer.config": "Build/php-cs-fixer.php"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **PHPStan Level 10**: Aim for `level: max` in modern TYPO3 13 projects
|
||||||
|
2. **Baseline for Legacy**: Use baselines to track existing issues during migration
|
||||||
|
3. **Security Audits**: Run `composer audit` regularly and in CI
|
||||||
|
4. **Auto-fix in CI**: Run fixes automatically, fail on violations
|
||||||
|
5. **Consistent Rules**: Share config across team
|
||||||
|
6. **Pre-commit Checks**: Catch issues before commit (lint, PHPStan, CGL, security)
|
||||||
|
7. **Latest PHP**: Run quality tools with latest PHP version (8.4+)
|
||||||
|
8. **Regular Updates**: Keep tools and rules updated
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [PHPStan Documentation](https://phpstan.org/user-guide/getting-started)
|
||||||
|
- [Rector Documentation](https://getrector.com/documentation)
|
||||||
|
- [PHP CS Fixer Documentation](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer)
|
||||||
|
- [TYPO3 Coding Guidelines](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/CodingGuidelines/)
|
||||||
691
references/test-runners.md
Normal file
691
references/test-runners.md
Normal file
@@ -0,0 +1,691 @@
|
|||||||
|
# Test Runners and Orchestration
|
||||||
|
|
||||||
|
The `runTests.sh` script is the standard TYPO3 pattern for orchestrating all quality checks and test suites.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
- Single entry point for all testing and quality checks
|
||||||
|
- Consistent environment across local and CI/CD
|
||||||
|
- Handles Docker, database setup, and test execution
|
||||||
|
- Based on [TYPO3 Best Practices tea extension](https://github.com/TYPO3BestPractices/tea)
|
||||||
|
|
||||||
|
## Script Location
|
||||||
|
|
||||||
|
```
|
||||||
|
Build/Scripts/runTests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show help
|
||||||
|
./Build/Scripts/runTests.sh -h
|
||||||
|
|
||||||
|
# Run specific test suite
|
||||||
|
./Build/Scripts/runTests.sh -s unit
|
||||||
|
./Build/Scripts/runTests.sh -s functional
|
||||||
|
./Build/Scripts/runTests.sh -s acceptance
|
||||||
|
|
||||||
|
# Run quality tools
|
||||||
|
./Build/Scripts/runTests.sh -s lint
|
||||||
|
./Build/Scripts/runTests.sh -s phpstan
|
||||||
|
./Build/Scripts/runTests.sh -s cgl
|
||||||
|
./Build/Scripts/runTests.sh -s rector
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-s <suite> Test suite to run (required)
|
||||||
|
unit, functional, acceptance, lint, phpstan, cgl, rector
|
||||||
|
|
||||||
|
-d <driver> Database driver for functional tests
|
||||||
|
mysqli (default), pdo_mysql, postgres, sqlite
|
||||||
|
|
||||||
|
-p <version> PHP version (7.4, 8.1, 8.2, 8.3)
|
||||||
|
|
||||||
|
-e <command> Execute specific command in container
|
||||||
|
|
||||||
|
-n Don't pull Docker images
|
||||||
|
|
||||||
|
-u Update composer dependencies
|
||||||
|
|
||||||
|
-v Enable verbose output
|
||||||
|
|
||||||
|
-x Stop on first error (PHPUnit --stop-on-error)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Run Unit Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default PHP version
|
||||||
|
./Build/Scripts/runTests.sh -s unit
|
||||||
|
|
||||||
|
# Specific PHP version
|
||||||
|
./Build/Scripts/runTests.sh -s unit -p 8.3
|
||||||
|
|
||||||
|
# Stop on first error
|
||||||
|
./Build/Scripts/runTests.sh -s unit -x
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Functional Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default database (mysqli)
|
||||||
|
./Build/Scripts/runTests.sh -s functional
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
./Build/Scripts/runTests.sh -s functional -d postgres
|
||||||
|
|
||||||
|
# SQLite (fastest for local development)
|
||||||
|
./Build/Scripts/runTests.sh -s functional -d sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Quality Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lint all PHP files
|
||||||
|
./Build/Scripts/runTests.sh -s lint
|
||||||
|
|
||||||
|
# PHPStan static analysis
|
||||||
|
./Build/Scripts/runTests.sh -s phpstan
|
||||||
|
|
||||||
|
# Code style check
|
||||||
|
./Build/Scripts/runTests.sh -s cgl
|
||||||
|
|
||||||
|
# Rector automated refactoring
|
||||||
|
./Build/Scripts/runTests.sh -s rector
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run specific test file
|
||||||
|
./Build/Scripts/runTests.sh -s unit -e "bin/phpunit Tests/Unit/Domain/Model/ProductTest.php"
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
./Build/Scripts/runTests.sh -s unit -e "bin/phpunit --coverage-html coverage/"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composer Integration
|
||||||
|
|
||||||
|
Integrate runTests.sh into composer.json:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"ci:test": [
|
||||||
|
"@ci:test:php:lint",
|
||||||
|
"@ci:test:php:phpstan",
|
||||||
|
"@ci:test:php:cgl",
|
||||||
|
"@ci:test:php:rector",
|
||||||
|
"@ci:test:php:unit",
|
||||||
|
"@ci:test:php:functional"
|
||||||
|
],
|
||||||
|
"ci:test:php:lint": "Build/Scripts/runTests.sh -s lint",
|
||||||
|
"ci:test:php:phpstan": "Build/Scripts/runTests.sh -s phpstan",
|
||||||
|
"ci:test:php:cgl": "Build/Scripts/runTests.sh -s cgl",
|
||||||
|
"ci:test:php:rector": "Build/Scripts/runTests.sh -s rector",
|
||||||
|
"ci:test:php:unit": "Build/Scripts/runTests.sh -s unit",
|
||||||
|
"ci:test:php:functional": "Build/Scripts/runTests.sh -s functional"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run via composer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer ci:test # All checks
|
||||||
|
composer ci:test:php:unit # Just unit tests
|
||||||
|
composer ci:test:php:phpstan # Just PHPStan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script Structure
|
||||||
|
|
||||||
|
### Basic Template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Script configuration
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
TEST_SUITE=""
|
||||||
|
DATABASE_DRIVER="mysqli"
|
||||||
|
PHP_VERSION="8.4"
|
||||||
|
VERBOSE=""
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while getopts ":s:d:p:e:nuvx" opt; do
|
||||||
|
case ${opt} in
|
||||||
|
s) TEST_SUITE=${OPTARG} ;;
|
||||||
|
d) DATABASE_DRIVER=${OPTARG} ;;
|
||||||
|
p) PHP_VERSION=${OPTARG} ;;
|
||||||
|
*) showHelp; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate required arguments
|
||||||
|
if [ -z "${TEST_SUITE}" ]; then
|
||||||
|
echo "Error: -s parameter (test suite) is required"
|
||||||
|
showHelp
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Execute test suite
|
||||||
|
case ${TEST_SUITE} in
|
||||||
|
unit)
|
||||||
|
runUnitTests
|
||||||
|
;;
|
||||||
|
functional)
|
||||||
|
runFunctionalTests
|
||||||
|
;;
|
||||||
|
lint)
|
||||||
|
runLint
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown test suite: ${TEST_SUITE}"
|
||||||
|
showHelp
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
runUnitTests() {
|
||||||
|
CONTAINER_PATH="/app"
|
||||||
|
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v "${PROJECT_DIR}:${CONTAINER_PATH}" \
|
||||||
|
-w "${CONTAINER_PATH}" \
|
||||||
|
php:${PHP_VERSION}-cli \
|
||||||
|
bin/phpunit -c Build/phpunit/UnitTests.xml
|
||||||
|
}
|
||||||
|
|
||||||
|
runFunctionalTests() {
|
||||||
|
CONTAINER_PATH="/app"
|
||||||
|
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v "${PROJECT_DIR}:${CONTAINER_PATH}" \
|
||||||
|
-w "${CONTAINER_PATH}" \
|
||||||
|
-e typo3DatabaseDriver="${DATABASE_DRIVER}" \
|
||||||
|
-e typo3DatabaseHost="localhost" \
|
||||||
|
-e typo3DatabaseName="typo3_test" \
|
||||||
|
php:${PHP_VERSION}-cli \
|
||||||
|
bin/phpunit -c Build/phpunit/FunctionalTests.xml
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quality Tool Functions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
runLint() {
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v "${PROJECT_DIR}:/app" \
|
||||||
|
-w /app \
|
||||||
|
php:${PHP_VERSION}-cli \
|
||||||
|
vendor/bin/phplint
|
||||||
|
}
|
||||||
|
|
||||||
|
runPhpstan() {
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v "${PROJECT_DIR}:/app" \
|
||||||
|
-w /app \
|
||||||
|
php:${PHP_VERSION}-cli \
|
||||||
|
vendor/bin/phpstan analyze --configuration Build/phpstan.neon
|
||||||
|
}
|
||||||
|
|
||||||
|
runCgl() {
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v "${PROJECT_DIR}:/app" \
|
||||||
|
-w /app \
|
||||||
|
php:${PHP_VERSION}-cli \
|
||||||
|
vendor/bin/php-cs-fixer fix --config Build/php-cs-fixer.php --dry-run --diff
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Configure via environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database configuration
|
||||||
|
export typo3DatabaseDriver=pdo_mysql
|
||||||
|
export typo3DatabaseHost=db
|
||||||
|
export typo3DatabasePort=3306
|
||||||
|
export typo3DatabaseName=typo3_test
|
||||||
|
export typo3DatabaseUsername=root
|
||||||
|
export typo3DatabasePassword=root
|
||||||
|
|
||||||
|
# TYPO3 context
|
||||||
|
export TYPO3_CONTEXT=Testing
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
./Build/Scripts/runTests.sh -s functional
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php: '8.2', '8.2', '8.3']
|
||||||
|
suite: ['unit', 'functional', 'lint', 'phpstan']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run ${{ matrix.suite }} tests
|
||||||
|
run: Build/Scripts/runTests.sh -s ${{ matrix.suite }} -p ${{ matrix.php }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitLab CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
.test:
|
||||||
|
image: php:${PHP_VERSION}-cli
|
||||||
|
script:
|
||||||
|
- Build/Scripts/runTests.sh -s ${TEST_SUITE} -p ${PHP_VERSION}
|
||||||
|
|
||||||
|
unit:8.2:
|
||||||
|
extends: .test
|
||||||
|
variables:
|
||||||
|
PHP_VERSION: "8.2"
|
||||||
|
TEST_SUITE: "unit"
|
||||||
|
|
||||||
|
functional:8.2:
|
||||||
|
extends: .test
|
||||||
|
variables:
|
||||||
|
PHP_VERSION: "8.2"
|
||||||
|
TEST_SUITE: "functional"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Parallel Execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run linting in parallel (fast)
|
||||||
|
find . -name '*.php' -print0 | xargs -0 -n1 -P8 php -l
|
||||||
|
|
||||||
|
# PHPUnit parallel execution
|
||||||
|
vendor/bin/paratest -c Build/phpunit/UnitTests.xml --processes=4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cache Composer dependencies
|
||||||
|
if [ ! -d "${PROJECT_DIR}/.cache/composer" ]; then
|
||||||
|
mkdir -p "${PROJECT_DIR}/.cache/composer"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v "${PROJECT_DIR}:/app" \
|
||||||
|
-v "${PROJECT_DIR}/.cache/composer:/tmp/composer-cache" \
|
||||||
|
php:${PHP_VERSION}-cli \
|
||||||
|
composer install --no-progress --no-suggest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Single Source of Truth**: Use runTests.sh for all test execution
|
||||||
|
2. **CI/CD Alignment**: CI should use same script as local development
|
||||||
|
3. **Docker Isolation**: Run tests in containers for consistency
|
||||||
|
4. **Fast Feedback**: Run lint and unit tests first (fastest)
|
||||||
|
5. **Matrix Testing**: Test multiple PHP versions and databases
|
||||||
|
6. **Caching**: Cache dependencies to speed up execution
|
||||||
|
7. **Verbose Mode**: Use `-v` flag for debugging test failures
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Docker Permission Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with current user
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
--user $(id -u):$(id -g) \
|
||||||
|
-v "${PROJECT_DIR}:/app" \
|
||||||
|
php:${PHP_VERSION}-cli \
|
||||||
|
bin/phpunit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Connection Errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify database is accessible
|
||||||
|
docker run --rm --network host mysql:8.0 \
|
||||||
|
mysql -h localhost -u root -p -e "SELECT 1"
|
||||||
|
|
||||||
|
# Use SQLite for simple tests
|
||||||
|
./Build/Scripts/runTests.sh -s functional -d sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update dependencies
|
||||||
|
./Build/Scripts/runTests.sh -s unit -u
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [TYPO3 Tea Extension runTests.sh](https://github.com/TYPO3BestPractices/tea/blob/main/Build/Scripts/runTests.sh)
|
||||||
|
- [TYPO3 Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/)
|
||||||
|
- [PHPUnit Documentation](https://phpunit.de/documentation.html)
|
||||||
|
|
||||||
|
## Multi-PHP Version Testing
|
||||||
|
|
||||||
|
TYPO3 extensions should support multiple PHP versions to maximize compatibility. This section covers testing strategies for all supported PHP versions.
|
||||||
|
|
||||||
|
### Standard Approach: Using Build/Scripts/runTests.sh
|
||||||
|
|
||||||
|
The `runTests.sh` script with Docker containers is the recommended TYPO3 testing approach. It provides isolated environments and consistent results.
|
||||||
|
|
||||||
|
#### Testing Across PHP Versions with Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test with PHP 8.2
|
||||||
|
./Build/Scripts/runTests.sh -s unit -p 8.2
|
||||||
|
./Build/Scripts/runTests.sh -s functional -p 8.2 -d sqlite
|
||||||
|
|
||||||
|
# Test with PHP 8.3
|
||||||
|
./Build/Scripts/runTests.sh -s unit -p 8.3
|
||||||
|
./Build/Scripts/runTests.sh -s functional -p 8.3 -d sqlite
|
||||||
|
|
||||||
|
# Test with PHP 8.4
|
||||||
|
./Build/Scripts/runTests.sh -s unit -p 8.4
|
||||||
|
./Build/Scripts/runTests.sh -s functional -p 8.4 -d sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
**Advantages:**
|
||||||
|
- Isolated Docker containers per PHP version
|
||||||
|
- Consistent environment across local and CI/CD
|
||||||
|
- No need to install multiple PHP versions locally
|
||||||
|
- Handles database setup automatically
|
||||||
|
- Based on official TYPO3 best practices
|
||||||
|
|
||||||
|
#### Complete Test Matrix Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Test all supported PHP versions and databases
|
||||||
|
|
||||||
|
PHP_VERSIONS=("8.2" "8.3" "8.4")
|
||||||
|
DATABASES=("sqlite" "mysql" "postgres")
|
||||||
|
|
||||||
|
for PHP in "${PHP_VERSIONS[@]}"; do
|
||||||
|
echo "Testing PHP ${PHP}..."
|
||||||
|
|
||||||
|
# Unit tests
|
||||||
|
./Build/Scripts/runTests.sh -s unit -p "${PHP}"
|
||||||
|
|
||||||
|
# Functional tests with different databases
|
||||||
|
for DB in "${DATABASES[@]}"; do
|
||||||
|
echo " Functional tests with ${DB}..."
|
||||||
|
./Build/Scripts/runTests.sh -s functional -p "${PHP}" -d "${DB}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Quality tools (run once per PHP version)
|
||||||
|
./Build/Scripts/runTests.sh -s lint -p "${PHP}"
|
||||||
|
./Build/Scripts/runTests.sh -s phpstan -p "${PHP}"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative Approach: Native PHP Versions (Without Docker)
|
||||||
|
|
||||||
|
For CI/CD environments or when Docker is unavailable, use locally installed PHP versions.
|
||||||
|
|
||||||
|
#### Testing with Native PHP Installations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test with PHP 8.2
|
||||||
|
php8.2 /usr/local/bin/composer update --no-interaction
|
||||||
|
php8.2 .Build/bin/phpunit --configuration=Build/phpunit/UnitTests.xml
|
||||||
|
typo3DatabaseDriver=pdo_sqlite php8.2 .Build/bin/phpunit --configuration=Build/phpunit/FunctionalTests.xml
|
||||||
|
|
||||||
|
# Test with PHP 8.3
|
||||||
|
php8.3 /usr/local/bin/composer update --no-interaction
|
||||||
|
php8.3 .Build/bin/phpunit --configuration=Build/phpunit/UnitTests.xml
|
||||||
|
typo3DatabaseDriver=pdo_sqlite php8.3 .Build/bin/phpunit --configuration=Build/phpunit/FunctionalTests.xml
|
||||||
|
|
||||||
|
# Test with PHP 8.4
|
||||||
|
php8.4 /usr/local/bin/composer update --no-interaction
|
||||||
|
php8.4 .Build/bin/phpunit --configuration=Build/phpunit/UnitTests.xml
|
||||||
|
typo3DatabaseDriver=pdo_sqlite php8.4 .Build/bin/phpunit --configuration=Build/phpunit/FunctionalTests.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL**: Always run `composer update` with the target PHP version FIRST. This ensures:
|
||||||
|
- Correct PHPUnit version selection (PHPUnit 11 for PHP 8.2, PHPUnit 12 for PHP 8.3+)
|
||||||
|
- Proper dependency resolution for the PHP version
|
||||||
|
- Compatible autoloader generation
|
||||||
|
|
||||||
|
### PHPUnit Version Compatibility
|
||||||
|
|
||||||
|
The `typo3/testing-framework` supports both PHPUnit 11 and 12, allowing Composer to automatically select the compatible version:
|
||||||
|
|
||||||
|
| PHP Version | PHPUnit Version | Auto-Selected by Composer |
|
||||||
|
|-------------|-----------------|---------------------------|
|
||||||
|
| 8.2 | 11.x | ✅ Yes |
|
||||||
|
| 8.3 | 11.x or 12.x | ✅ Yes (12.x preferred) |
|
||||||
|
| 8.4 | 11.x or 12.x | ✅ Yes (12.x preferred) |
|
||||||
|
|
||||||
|
**Example: Automatic PHPUnit Selection**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PHP 8.2 automatically gets PHPUnit 11
|
||||||
|
$ php8.2 /usr/local/bin/composer update
|
||||||
|
# Installing typo3/testing-framework (v8.0.14)
|
||||||
|
# Installing phpunit/phpunit (11.5.42)
|
||||||
|
|
||||||
|
# PHP 8.3 automatically gets PHPUnit 12
|
||||||
|
$ php8.3 /usr/local/bin/composer update
|
||||||
|
# Installing typo3/testing-framework (v8.0.14)
|
||||||
|
# Installing phpunit/phpunit (12.4.1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitHub Actions CI/CD Integration
|
||||||
|
|
||||||
|
#### Using runTests.sh (Recommended)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php: ['8.2', '8.3', '8.4']
|
||||||
|
suite: ['unit', 'functional']
|
||||||
|
database: ['sqlite', 'mysql', 'postgres']
|
||||||
|
exclude:
|
||||||
|
# Only test sqlite for unit tests
|
||||||
|
- suite: unit
|
||||||
|
database: mysql
|
||||||
|
- suite: unit
|
||||||
|
database: postgres
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run ${{ matrix.suite }} tests on PHP ${{ matrix.php }}
|
||||||
|
run: |
|
||||||
|
Build/Scripts/runTests.sh \
|
||||||
|
-s ${{ matrix.suite }} \
|
||||||
|
-p ${{ matrix.php }} \
|
||||||
|
-d ${{ matrix.database }}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using Native PHP (Alternative)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php: ['8.2', '8.3', '8.4']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up PHP ${{ matrix.php }}
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
extensions: dom, libxml, sqlite3
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer update --no-interaction --no-progress
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: .Build/bin/phpunit -c Build/phpunit/UnitTests.xml
|
||||||
|
|
||||||
|
- name: Run functional tests
|
||||||
|
env:
|
||||||
|
typo3DatabaseDriver: pdo_sqlite
|
||||||
|
run: .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Pitfalls
|
||||||
|
|
||||||
|
#### ❌ Wrong: Testing Without Updating Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# This will use wrong PHPUnit version
|
||||||
|
php8.2 .Build/bin/phpunit -c Build/phpunit/UnitTests.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Uses PHPUnit version from previous `composer update`, may be incompatible.
|
||||||
|
|
||||||
|
#### ✅ Right: Update Dependencies First
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# This ensures correct PHPUnit version
|
||||||
|
php8.2 /usr/local/bin/composer update --no-interaction
|
||||||
|
php8.2 .Build/bin/phpunit -c Build/phpunit/UnitTests.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ❌ Wrong: Removing PHP Version Support Due to Test Failures
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Don't do this!
|
||||||
|
# composer.json: "php": "^8.3 || ^8.4" # Removed 8.2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Unnecessarily reduces compatibility. Fix the testing approach instead.
|
||||||
|
|
||||||
|
#### ✅ Right: Fix Testing Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run composer update with the problematic PHP version
|
||||||
|
php8.2 /usr/local/bin/composer update
|
||||||
|
# Composer automatically selects compatible dependencies
|
||||||
|
# Now tests work correctly
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Standard Approach First**: Use `Build/Scripts/runTests.sh` with Docker whenever possible
|
||||||
|
2. **Update Before Testing**: Always run `composer update` with target PHP version first (native approach)
|
||||||
|
3. **Trust Composer**: Let Composer select compatible PHPUnit versions automatically
|
||||||
|
4. **Test All Versions**: Run full test suite on all supported PHP versions in CI
|
||||||
|
5. **SQLite for Speed**: Use SQLite for local functional testing (fastest)
|
||||||
|
6. **Matrix Testing**: Use CI matrix to test all PHP versions in parallel
|
||||||
|
7. **Don't Remove Support**: Fix testing process, don't remove PHP version support
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Docker Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update Docker images
|
||||||
|
./Build/Scripts/runTests.sh -u
|
||||||
|
|
||||||
|
# Check Docker is running
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# Clean up old containers
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Native PHP Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify PHP version
|
||||||
|
php8.2 --version
|
||||||
|
|
||||||
|
# Check installed extensions
|
||||||
|
php8.2 -m | grep -E 'dom|libxml|sqlite'
|
||||||
|
|
||||||
|
# Clean and reinstall dependencies
|
||||||
|
rm -rf .Build vendor composer.lock
|
||||||
|
php8.2 /usr/local/bin/composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PHPUnit Version Conflicts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear Composer cache
|
||||||
|
php8.2 /usr/local/bin/composer clear-cache
|
||||||
|
|
||||||
|
# Show why PHPUnit version was selected
|
||||||
|
php8.2 /usr/local/bin/composer why phpunit/phpunit
|
||||||
|
|
||||||
|
# Force dependency resolution
|
||||||
|
rm composer.lock
|
||||||
|
php8.2 /usr/local/bin/composer update --with-all-dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Aspect | Docker/runTests.sh | Native PHP |
|
||||||
|
|--------|-------------------|------------|
|
||||||
|
| **Use Case** | Standard TYPO3 development | CI/CD, no Docker available |
|
||||||
|
| **Isolation** | ✅ Full container isolation | ⚠️ System-wide PHP |
|
||||||
|
| **Setup** | Docker required | Multiple PHP versions required |
|
||||||
|
| **Speed** | Slower (container overhead) | Faster (native execution) |
|
||||||
|
| **Consistency** | ✅ Guaranteed environment | ⚠️ Depends on system config |
|
||||||
|
| **Recommendation** | **Recommended** | Alternative when needed |
|
||||||
|
|
||||||
|
**Choose Docker/runTests.sh for:**
|
||||||
|
- Local development and testing
|
||||||
|
- Consistent environment across team
|
||||||
|
- Official TYPO3 best practices
|
||||||
|
|
||||||
|
**Choose Native PHP for:**
|
||||||
|
- GitHub Actions/GitLab CI environments
|
||||||
|
- Systems without Docker
|
||||||
|
- Performance-critical testing pipelines
|
||||||
929
references/unit-testing.md
Normal file
929
references/unit-testing.md
Normal file
@@ -0,0 +1,929 @@
|
|||||||
|
# Unit Testing in TYPO3
|
||||||
|
|
||||||
|
Unit tests are fast, isolated tests that verify individual components without external dependencies like databases or file systems.
|
||||||
|
|
||||||
|
## When to Use Unit Tests
|
||||||
|
|
||||||
|
✅ **Ideal for:**
|
||||||
|
- Testing pure business logic
|
||||||
|
- Validators, calculators, transformers
|
||||||
|
- Value objects and DTOs
|
||||||
|
- Utilities and helper functions
|
||||||
|
- Domain models without persistence
|
||||||
|
- **Controllers with dependency injection** (new in TYPO3 13)
|
||||||
|
- **Services with injected dependencies**
|
||||||
|
|
||||||
|
❌ **Not suitable for:**
|
||||||
|
- Database operations (use functional tests)
|
||||||
|
- File system operations
|
||||||
|
- Methods using `BackendUtility` or global state
|
||||||
|
- Complex TYPO3 framework integration
|
||||||
|
- Parent class behavior from framework classes
|
||||||
|
|
||||||
|
## Base Class
|
||||||
|
|
||||||
|
All unit tests extend `TYPO3\TestingFramework\Core\Unit\UnitTestCase`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Unit\Domain\Validator;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
|
||||||
|
use Vendor\Extension\Domain\Validator\EmailValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for EmailValidator.
|
||||||
|
*
|
||||||
|
* @covers \Vendor\Extension\Domain\Validator\EmailValidator
|
||||||
|
*/
|
||||||
|
final class EmailValidatorTest extends UnitTestCase
|
||||||
|
{
|
||||||
|
private EmailValidator $subject;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->subject = new EmailValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validEmailPassesValidation(): void
|
||||||
|
{
|
||||||
|
$result = $this->subject->validate('user@example.com');
|
||||||
|
|
||||||
|
self::assertFalse($result->hasErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function invalidEmailFailsValidation(): void
|
||||||
|
{
|
||||||
|
$result = $this->subject->validate('invalid-email');
|
||||||
|
|
||||||
|
self::assertTrue($result->hasErrors());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** TYPO3 13+ with PHPUnit 11/12 uses PHP attributes (`#[Test]`) instead of `@test` annotations.
|
||||||
|
> Use `private` instead of `protected` for properties when possible (better encapsulation).
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
### 1. No External Dependencies
|
||||||
|
|
||||||
|
Unit tests should NOT:
|
||||||
|
- Access the database
|
||||||
|
- Read/write files
|
||||||
|
- Make HTTP requests
|
||||||
|
- Use TYPO3 framework services
|
||||||
|
|
||||||
|
### 2. Fast Execution
|
||||||
|
|
||||||
|
Unit tests should run in milliseconds:
|
||||||
|
- No I/O operations
|
||||||
|
- Minimal object instantiation
|
||||||
|
- Use mocks for dependencies
|
||||||
|
|
||||||
|
### 3. Test Independence
|
||||||
|
|
||||||
|
Each test should:
|
||||||
|
- Be runnable standalone
|
||||||
|
- Not depend on execution order
|
||||||
|
- Clean up in tearDown()
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
### Arrange-Act-Assert Pattern
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function calculatesTotalPrice(): void
|
||||||
|
{
|
||||||
|
// Arrange: Set up test data
|
||||||
|
$cart = new ShoppingCart();
|
||||||
|
$cart->addItem(new Item('product1', 10.00, 2));
|
||||||
|
$cart->addItem(new Item('product2', 5.50, 1));
|
||||||
|
|
||||||
|
// Act: Execute the code under test
|
||||||
|
$total = $cart->calculateTotal();
|
||||||
|
|
||||||
|
// Assert: Verify the result
|
||||||
|
self::assertSame(25.50, $total);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### setUp() and tearDown()
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
// Initialize test subject and dependencies
|
||||||
|
$this->subject = new Calculator();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
// Clean up resources
|
||||||
|
unset($this->subject);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Dependency Injection (TYPO3 13+)
|
||||||
|
|
||||||
|
Modern TYPO3 13 controllers and services use constructor injection. Here's how to test them:
|
||||||
|
|
||||||
|
### Basic Constructor Injection Test
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Unit\Controller;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
|
||||||
|
use Vendor\Extension\Controller\ImageController;
|
||||||
|
|
||||||
|
final class ImageControllerTest extends UnitTestCase
|
||||||
|
{
|
||||||
|
private ImageController $subject;
|
||||||
|
|
||||||
|
/** @var ResourceFactory&MockObject */
|
||||||
|
private ResourceFactory $resourceFactoryMock;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
/** @var ResourceFactory&MockObject $resourceFactoryMock */
|
||||||
|
$resourceFactoryMock = $this->createMock(ResourceFactory::class);
|
||||||
|
|
||||||
|
$this->resourceFactoryMock = $resourceFactoryMock;
|
||||||
|
$this->subject = new ImageController($this->resourceFactoryMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function getFileRetrievesFileFromFactory(): void
|
||||||
|
{
|
||||||
|
$fileId = 123;
|
||||||
|
$fileMock = $this->createMock(\TYPO3\CMS\Core\Resource\File::class);
|
||||||
|
|
||||||
|
$this->resourceFactoryMock
|
||||||
|
->expects(self::once())
|
||||||
|
->method('getFileObject')
|
||||||
|
->with($fileId)
|
||||||
|
->willReturn($fileMock);
|
||||||
|
|
||||||
|
$result = $this->subject->getFile($fileId);
|
||||||
|
|
||||||
|
self::assertSame($fileMock, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Dependencies with Intersection Types
|
||||||
|
|
||||||
|
PHPUnit mocks require proper type hints using intersection types for PHPStan compliance:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Unit\Controller;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use TYPO3\CMS\Core\Log\LogManager;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
|
||||||
|
use Vendor\Extension\Controller\ImageController;
|
||||||
|
use Vendor\Extension\Utils\ImageProcessor;
|
||||||
|
|
||||||
|
final class ImageControllerTest extends UnitTestCase
|
||||||
|
{
|
||||||
|
private ImageController $subject;
|
||||||
|
|
||||||
|
/** @var ResourceFactory&MockObject */
|
||||||
|
private ResourceFactory $resourceFactoryMock;
|
||||||
|
|
||||||
|
/** @var ImageProcessor&MockObject */
|
||||||
|
private ImageProcessor $imageProcessorMock;
|
||||||
|
|
||||||
|
/** @var LogManager&MockObject */
|
||||||
|
private LogManager $logManagerMock;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
/** @var ResourceFactory&MockObject $resourceFactoryMock */
|
||||||
|
$resourceFactoryMock = $this->createMock(ResourceFactory::class);
|
||||||
|
|
||||||
|
/** @var ImageProcessor&MockObject $imageProcessorMock */
|
||||||
|
$imageProcessorMock = $this->createMock(ImageProcessor::class);
|
||||||
|
|
||||||
|
/** @var LogManager&MockObject $logManagerMock */
|
||||||
|
$logManagerMock = $this->createMock(LogManager::class);
|
||||||
|
|
||||||
|
$this->resourceFactoryMock = $resourceFactoryMock;
|
||||||
|
$this->imageProcessorMock = $imageProcessorMock;
|
||||||
|
$this->logManagerMock = $logManagerMock;
|
||||||
|
|
||||||
|
$this->subject = new ImageController(
|
||||||
|
$this->resourceFactoryMock,
|
||||||
|
$this->imageProcessorMock,
|
||||||
|
$this->logManagerMock,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function processImageUsesInjectedProcessor(): void
|
||||||
|
{
|
||||||
|
$fileMock = $this->createMock(\TYPO3\CMS\Core\Resource\File::class);
|
||||||
|
$processedFileMock = $this->createMock(\TYPO3\CMS\Core\Resource\ProcessedFile::class);
|
||||||
|
|
||||||
|
$this->imageProcessorMock
|
||||||
|
->expects(self::once())
|
||||||
|
->method('process')
|
||||||
|
->with($fileMock, ['width' => 800])
|
||||||
|
->willReturn($processedFileMock);
|
||||||
|
|
||||||
|
$result = $this->subject->processImage($fileMock, ['width' => 800]);
|
||||||
|
|
||||||
|
self::assertSame($processedFileMock, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Points:**
|
||||||
|
- Use intersection types: `ResourceFactory&MockObject` for proper PHPStan type checking
|
||||||
|
- Assign mocks to properly typed variables before passing to constructor
|
||||||
|
- This pattern works with PHPUnit 11/12 and PHPStan Level 10
|
||||||
|
|
||||||
|
### Handling $GLOBALS and Singleton State
|
||||||
|
|
||||||
|
Some TYPO3 components still use global state. Handle this properly:
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class BackendControllerTest extends UnitTestCase
|
||||||
|
{
|
||||||
|
protected bool $resetSingletonInstances = true;
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function checksBackendUserPermissions(): void
|
||||||
|
{
|
||||||
|
// Mock backend user
|
||||||
|
$backendUserMock = $this->createMock(BackendUserAuthentication::class);
|
||||||
|
$backendUserMock->method('isAdmin')->willReturn(true);
|
||||||
|
|
||||||
|
$GLOBALS['BE_USER'] = $backendUserMock;
|
||||||
|
|
||||||
|
$result = $this->subject->hasAccess();
|
||||||
|
|
||||||
|
self::assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function returnsFalseWhenNoBackendUser(): void
|
||||||
|
{
|
||||||
|
$GLOBALS['BE_USER'] = null;
|
||||||
|
|
||||||
|
$result = $this->subject->hasAccess();
|
||||||
|
|
||||||
|
self::assertFalse($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Set `protected bool $resetSingletonInstances = true;` when tests interact with TYPO3 singletons to prevent test pollution.
|
||||||
|
|
||||||
|
## Mocking Dependencies
|
||||||
|
|
||||||
|
Use PHPUnit's built-in mocking (PHPUnit 11/12):
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Unit\Service;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
|
||||||
|
use Vendor\Extension\Domain\Model\User;
|
||||||
|
use Vendor\Extension\Domain\Repository\UserRepository;
|
||||||
|
use Vendor\Extension\Service\UserService;
|
||||||
|
|
||||||
|
final class UserServiceTest extends UnitTestCase
|
||||||
|
{
|
||||||
|
private UserService $subject;
|
||||||
|
|
||||||
|
/** @var UserRepository&MockObject */
|
||||||
|
private UserRepository $repositoryMock;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
/** @var UserRepository&MockObject $repositoryMock */
|
||||||
|
$repositoryMock = $this->createMock(UserRepository::class);
|
||||||
|
|
||||||
|
$this->repositoryMock = $repositoryMock;
|
||||||
|
$this->subject = new UserService($this->repositoryMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function findsUserByEmail(): void
|
||||||
|
{
|
||||||
|
$email = 'test@example.com';
|
||||||
|
$user = new User('John');
|
||||||
|
|
||||||
|
$this->repositoryMock
|
||||||
|
->expects(self::once())
|
||||||
|
->method('findByEmail')
|
||||||
|
->with($email)
|
||||||
|
->willReturn($user);
|
||||||
|
|
||||||
|
$result = $this->subject->getUserByEmail($email);
|
||||||
|
|
||||||
|
self::assertSame('John', $result->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function throwsExceptionWhenUserNotFound(): void
|
||||||
|
{
|
||||||
|
$email = 'nonexistent@example.com';
|
||||||
|
|
||||||
|
$this->repositoryMock
|
||||||
|
->method('findByEmail')
|
||||||
|
->with($email)
|
||||||
|
->willReturn(null);
|
||||||
|
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('User not found');
|
||||||
|
|
||||||
|
$this->subject->getUserByEmail($email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** TYPO3 13+ with PHPUnit 11/12 uses `createMock()` instead of Prophecy.
|
||||||
|
> Prophecy is deprecated and should not be used in new tests.
|
||||||
|
|
||||||
|
## Assertions
|
||||||
|
|
||||||
|
### Common Assertions
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Equality
|
||||||
|
self::assertEquals($expected, $actual);
|
||||||
|
self::assertSame($expected, $actual); // Strict comparison
|
||||||
|
|
||||||
|
// Boolean
|
||||||
|
self::assertTrue($condition);
|
||||||
|
self::assertFalse($condition);
|
||||||
|
|
||||||
|
// Null checks
|
||||||
|
self::assertNull($value);
|
||||||
|
self::assertNotNull($value);
|
||||||
|
|
||||||
|
// Type checks
|
||||||
|
self::assertIsString($value);
|
||||||
|
self::assertIsInt($value);
|
||||||
|
self::assertIsArray($value);
|
||||||
|
self::assertInstanceOf(User::class, $object);
|
||||||
|
|
||||||
|
// Collections
|
||||||
|
self::assertCount(3, $array);
|
||||||
|
self::assertEmpty($array);
|
||||||
|
self::assertContains('item', $array);
|
||||||
|
|
||||||
|
// Exceptions
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('Invalid input');
|
||||||
|
$subject->methodThatThrows();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Over Generic
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ Too generic
|
||||||
|
self::assertTrue($result > 0);
|
||||||
|
self::assertEquals(true, $isValid);
|
||||||
|
|
||||||
|
// ✅ Specific and clear
|
||||||
|
self::assertGreaterThan(0, $result);
|
||||||
|
self::assertTrue($isValid);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Providers
|
||||||
|
|
||||||
|
Test multiple scenarios with data providers:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider validEmailProvider
|
||||||
|
*/
|
||||||
|
public function validatesEmails(string $email, bool $expected): void
|
||||||
|
{
|
||||||
|
$result = $this->subject->isValid($email);
|
||||||
|
self::assertSame($expected, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validEmailProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid email' => ['user@example.com', true],
|
||||||
|
'email with subdomain' => ['user@mail.example.com', true],
|
||||||
|
'missing @' => ['userexample.com', false],
|
||||||
|
'missing domain' => ['user@', false],
|
||||||
|
'empty string' => ['', false],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Private/Protected Methods
|
||||||
|
|
||||||
|
**Preferred Approach**: Test through public API whenever possible:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ Best approach - test through public interface
|
||||||
|
$result = $subject->publicMethodThatUsesPrivateMethod();
|
||||||
|
self::assertSame($expected, $result);
|
||||||
|
```
|
||||||
|
|
||||||
|
**When Reflection is Acceptable**: Sometimes protected methods contain complex logic that deserves dedicated testing (e.g., URL validation, attribute resolution). In these cases, use a helper method:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Unit\Controller;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use ReflectionMethod;
|
||||||
|
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
|
||||||
|
use Vendor\Extension\Controller\ImageController;
|
||||||
|
|
||||||
|
final class ImageControllerTest extends UnitTestCase
|
||||||
|
{
|
||||||
|
private ImageController $subject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to access protected methods.
|
||||||
|
*
|
||||||
|
* @param array<int, mixed> $args
|
||||||
|
*/
|
||||||
|
private function callProtectedMethod(string $methodName, array $args): mixed
|
||||||
|
{
|
||||||
|
$reflection = new ReflectionMethod($this->subject, $methodName);
|
||||||
|
$reflection->setAccessible(true);
|
||||||
|
|
||||||
|
return $reflection->invokeArgs($this->subject, $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function isExternalImageReturnsTrueForHttpsUrls(): void
|
||||||
|
{
|
||||||
|
$result = $this->callProtectedMethod('isExternalImage', ['https://example.com/image.jpg']);
|
||||||
|
|
||||||
|
self::assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function isExternalImageReturnsFalseForLocalPaths(): void
|
||||||
|
{
|
||||||
|
$result = $this->callProtectedMethod('isExternalImage', ['/fileadmin/images/test.jpg']);
|
||||||
|
|
||||||
|
self::assertFalse($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Considerations**:
|
||||||
|
- Only use reflection when testing protected methods with complex logic worth testing independently
|
||||||
|
- Never test private methods - refactor to protected if testing is needed
|
||||||
|
- Prefer testing through public API when the logic is simple
|
||||||
|
- Document why reflection testing is used for a specific method
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### PHPUnit XML (Build/phpunit/UnitTests.xml)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<phpunit
|
||||||
|
bootstrap="../../vendor/autoload.php"
|
||||||
|
cacheResult="false"
|
||||||
|
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
failOnDeprecation="true"
|
||||||
|
failOnNotice="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
failOnRisky="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit tests">
|
||||||
|
<directory>../../Tests/Unit/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **One Assert Per Test**: Focus tests on single behavior
|
||||||
|
2. **Clear Test Names**: Describe what is tested and expected result
|
||||||
|
3. **Arrange-Act-Assert**: Follow consistent structure
|
||||||
|
4. **No Logic in Tests**: Tests should be simple and readable
|
||||||
|
5. **Test Edge Cases**: Empty strings, null, zero, negative numbers
|
||||||
|
6. **Use Data Providers**: Test multiple scenarios efficiently
|
||||||
|
7. **Mock External Dependencies**: Keep tests isolated and fast
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
❌ **Testing Framework Code**
|
||||||
|
```php
|
||||||
|
// Don't test TYPO3 core functionality
|
||||||
|
$this->assertTrue(is_array([])); // Useless test
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Slow Tests**
|
||||||
|
```php
|
||||||
|
// Don't access file system in unit tests
|
||||||
|
file_put_contents('/tmp/test.txt', 'data');
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Test Interdependence**
|
||||||
|
```php
|
||||||
|
// Don't depend on test execution order
|
||||||
|
/** @depends testCreate */
|
||||||
|
public function testUpdate(): void { }
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Focused, Fast, Isolated Tests**
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function calculatesPriceWithDiscount(): void
|
||||||
|
{
|
||||||
|
$calculator = new PriceCalculator();
|
||||||
|
$price = $calculator->calculate(100.0, 0.2);
|
||||||
|
self::assertSame(80.0, $price);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Unit Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via runTests.sh
|
||||||
|
Build/Scripts/runTests.sh -s unit
|
||||||
|
|
||||||
|
# Via PHPUnit directly
|
||||||
|
vendor/bin/phpunit -c Build/phpunit/UnitTests.xml
|
||||||
|
|
||||||
|
# Via Composer
|
||||||
|
composer ci:test:php:unit
|
||||||
|
|
||||||
|
# Single test file
|
||||||
|
vendor/bin/phpunit Tests/Unit/Domain/Validator/EmailValidatorTest.php
|
||||||
|
|
||||||
|
# Single test method
|
||||||
|
vendor/bin/phpunit --filter testValidEmail
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting Common Issues
|
||||||
|
|
||||||
|
### PHPStan Errors with Mocks
|
||||||
|
|
||||||
|
**Problem**: PHPStan complains about mock type mismatches.
|
||||||
|
```
|
||||||
|
Method expects ResourceFactory but got ResourceFactory&MockObject
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Use intersection type annotations:
|
||||||
|
```php
|
||||||
|
/** @var ResourceFactory&MockObject */
|
||||||
|
private ResourceFactory $resourceFactoryMock;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
/** @var ResourceFactory&MockObject $resourceFactoryMock */
|
||||||
|
$resourceFactoryMock = $this->createMock(ResourceFactory::class);
|
||||||
|
|
||||||
|
$this->resourceFactoryMock = $resourceFactoryMock;
|
||||||
|
$this->subject = new MyController($this->resourceFactoryMock);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Undefined Array Key Warnings
|
||||||
|
|
||||||
|
**Problem**: Tests throw warnings about missing array keys.
|
||||||
|
```
|
||||||
|
Undefined array key "fileId"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Always provide all required keys in mock arrays:
|
||||||
|
```php
|
||||||
|
// ❌ Incomplete mock data
|
||||||
|
$requestMock->method('getQueryParams')->willReturn([
|
||||||
|
'fileId' => 123,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ✅ Complete mock data
|
||||||
|
$requestMock->method('getQueryParams')->willReturn([
|
||||||
|
'fileId' => 123,
|
||||||
|
'table' => 'tt_content',
|
||||||
|
'P' => [],
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests Requiring Functional Setup
|
||||||
|
|
||||||
|
**Problem**: Unit tests fail with cache or framework errors.
|
||||||
|
```
|
||||||
|
NoSuchCacheException: A cache with identifier "runtime" does not exist.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Identify methods that require TYPO3 framework infrastructure and move them to functional tests:
|
||||||
|
- Methods using `BackendUtility::getPagesTSconfig()`
|
||||||
|
- Methods calling parent class framework behavior
|
||||||
|
- Methods requiring global state like `$GLOBALS['TYPO3_CONF_VARS']`
|
||||||
|
|
||||||
|
Add comments explaining the limitation:
|
||||||
|
```php
|
||||||
|
// Note: getMaxDimensions tests require functional test setup due to BackendUtility dependency
|
||||||
|
// These are better tested in functional tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Singleton State Pollution
|
||||||
|
|
||||||
|
**Problem**: Tests interfere with each other due to singleton state.
|
||||||
|
|
||||||
|
**Solution**: Enable singleton reset in your test class:
|
||||||
|
```php
|
||||||
|
final class MyControllerTest extends UnitTestCase
|
||||||
|
{
|
||||||
|
protected bool $resetSingletonInstances = true;
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function testWithGlobals(): void
|
||||||
|
{
|
||||||
|
$GLOBALS['BE_USER'] = $this->createMock(BackendUserAuthentication::class);
|
||||||
|
// Test will clean up automatically
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exception Flow Issues
|
||||||
|
|
||||||
|
**Problem**: Catching and re-throwing exceptions masks the original error.
|
||||||
|
```php
|
||||||
|
// ❌ Inner exception caught by outer catch
|
||||||
|
try {
|
||||||
|
$file = $this->factory->getFile($id);
|
||||||
|
if ($file->isDeleted()) {
|
||||||
|
throw new RuntimeException('Deleted', 1234);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw new RuntimeException('Not found', 5678);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Separate concerns - catch only what you need:
|
||||||
|
```php
|
||||||
|
// ✅ Proper exception flow
|
||||||
|
try {
|
||||||
|
$file = $this->factory->getFile($id);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw new RuntimeException('Not found', 5678, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($file->isDeleted()) {
|
||||||
|
throw new RuntimeException('Deleted', 1234);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing DataHandler Hooks
|
||||||
|
|
||||||
|
DataHandler hooks (`processDatamap_*`, `processCmdmap_*`) require careful testing as they interact with TYPO3 globals.
|
||||||
|
|
||||||
|
### Example: Testing processDatamap_postProcessFieldArray
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Unit\Database;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||||
|
use TYPO3\CMS\Core\Context\Context;
|
||||||
|
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||||
|
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||||
|
use TYPO3\CMS\Core\Log\LogManager;
|
||||||
|
use TYPO3\CMS\Core\Log\Logger;
|
||||||
|
use TYPO3\CMS\Core\Resource\DefaultUploadFolderResolver;
|
||||||
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
|
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
|
||||||
|
use Vendor\Extension\Database\MyDataHandlerHook;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for MyDataHandlerHook.
|
||||||
|
*
|
||||||
|
* @covers \Vendor\Extension\Database\MyDataHandlerHook
|
||||||
|
*/
|
||||||
|
final class MyDataHandlerHookTest extends UnitTestCase
|
||||||
|
{
|
||||||
|
protected bool $resetSingletonInstances = true;
|
||||||
|
|
||||||
|
private MyDataHandlerHook $subject;
|
||||||
|
|
||||||
|
/** @var ExtensionConfiguration&MockObject */
|
||||||
|
private ExtensionConfiguration $extensionConfigurationMock;
|
||||||
|
|
||||||
|
/** @var LogManager&MockObject */
|
||||||
|
private LogManager $logManagerMock;
|
||||||
|
|
||||||
|
/** @var ResourceFactory&MockObject */
|
||||||
|
private ResourceFactory $resourceFactoryMock;
|
||||||
|
|
||||||
|
/** @var Context&MockObject */
|
||||||
|
private Context $contextMock;
|
||||||
|
|
||||||
|
/** @var RequestFactory&MockObject */
|
||||||
|
private RequestFactory $requestFactoryMock;
|
||||||
|
|
||||||
|
/** @var DefaultUploadFolderResolver&MockObject */
|
||||||
|
private DefaultUploadFolderResolver $uploadFolderResolverMock;
|
||||||
|
|
||||||
|
/** @var Logger&MockObject */
|
||||||
|
private Logger $loggerMock;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Create all required mocks with intersection types for PHPStan compliance
|
||||||
|
/** @var ExtensionConfiguration&MockObject $extensionConfigurationMock */
|
||||||
|
$extensionConfigurationMock = $this->createMock(ExtensionConfiguration::class);
|
||||||
|
|
||||||
|
/** @var LogManager&MockObject $logManagerMock */
|
||||||
|
$logManagerMock = $this->createMock(LogManager::class);
|
||||||
|
|
||||||
|
/** @var ResourceFactory&MockObject $resourceFactoryMock */
|
||||||
|
$resourceFactoryMock = $this->createMock(ResourceFactory::class);
|
||||||
|
|
||||||
|
/** @var Context&MockObject $contextMock */
|
||||||
|
$contextMock = $this->createMock(Context::class);
|
||||||
|
|
||||||
|
/** @var RequestFactory&MockObject $requestFactoryMock */
|
||||||
|
$requestFactoryMock = $this->createMock(RequestFactory::class);
|
||||||
|
|
||||||
|
/** @var DefaultUploadFolderResolver&MockObject $uploadFolderResolverMock */
|
||||||
|
$uploadFolderResolverMock = $this->createMock(DefaultUploadFolderResolver::class);
|
||||||
|
|
||||||
|
/** @var Logger&MockObject $loggerMock */
|
||||||
|
$loggerMock = $this->createMock(Logger::class);
|
||||||
|
|
||||||
|
// Configure extension configuration mock with willReturnCallback
|
||||||
|
$extensionConfigurationMock
|
||||||
|
->method('get')
|
||||||
|
->willReturnCallback(function ($extension, $key) {
|
||||||
|
if ($extension === 'my_extension') {
|
||||||
|
return match ($key) {
|
||||||
|
'enableFeature' => true,
|
||||||
|
'timeout' => 30,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure log manager to return logger mock
|
||||||
|
$logManagerMock
|
||||||
|
->method('getLogger')
|
||||||
|
->with(MyDataHandlerHook::class)
|
||||||
|
->willReturn($loggerMock);
|
||||||
|
|
||||||
|
// Assign mocks to properties
|
||||||
|
$this->extensionConfigurationMock = $extensionConfigurationMock;
|
||||||
|
$this->logManagerMock = $logManagerMock;
|
||||||
|
$this->resourceFactoryMock = $resourceFactoryMock;
|
||||||
|
$this->contextMock = $contextMock;
|
||||||
|
$this->requestFactoryMock = $requestFactoryMock;
|
||||||
|
$this->uploadFolderResolverMock = $uploadFolderResolverMock;
|
||||||
|
$this->loggerMock = $loggerMock;
|
||||||
|
|
||||||
|
// Create subject with all dependencies
|
||||||
|
$this->subject = new MyDataHandlerHook(
|
||||||
|
$this->extensionConfigurationMock,
|
||||||
|
$this->logManagerMock,
|
||||||
|
$this->resourceFactoryMock,
|
||||||
|
$this->contextMock,
|
||||||
|
$this->requestFactoryMock,
|
||||||
|
$this->uploadFolderResolverMock,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function constructorInitializesWithDependencyInjection(): void
|
||||||
|
{
|
||||||
|
// Verify subject was created successfully with all dependencies
|
||||||
|
self::assertInstanceOf(MyDataHandlerHook::class, $this->subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function processDatamapPostProcessFieldArrayHandlesFieldCorrectly(): void
|
||||||
|
{
|
||||||
|
$status = 'update';
|
||||||
|
$table = 'tt_content';
|
||||||
|
$id = '123';
|
||||||
|
$fieldArray = ['bodytext' => '<p>Content with processing</p>'];
|
||||||
|
|
||||||
|
/** @var DataHandler&MockObject $dataHandlerMock */
|
||||||
|
$dataHandlerMock = $this->createMock(DataHandler::class);
|
||||||
|
|
||||||
|
// Mock TCA configuration for RTE field
|
||||||
|
$GLOBALS['TCA']['tt_content']['columns']['bodytext']['config'] = [
|
||||||
|
'type' => 'text',
|
||||||
|
'enableRichtext' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test the hook processes the field
|
||||||
|
$this->subject->processDatamap_postProcessFieldArray(
|
||||||
|
$status,
|
||||||
|
$table,
|
||||||
|
$id,
|
||||||
|
$fieldArray,
|
||||||
|
$dataHandlerMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert field was processed (actual assertion depends on implementation)
|
||||||
|
self::assertNotEmpty($fieldArray['bodytext']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function constructorLoadsExtensionConfiguration(): void
|
||||||
|
{
|
||||||
|
/** @var ExtensionConfiguration&MockObject $configMock */
|
||||||
|
$configMock = $this->createMock(ExtensionConfiguration::class);
|
||||||
|
$configMock
|
||||||
|
->expects(self::exactly(2))
|
||||||
|
->method('get')
|
||||||
|
->willReturnCallback(function ($extension, $key) {
|
||||||
|
self::assertSame('my_extension', $extension);
|
||||||
|
|
||||||
|
return match ($key) {
|
||||||
|
'enableFeature' => true,
|
||||||
|
'timeout' => 30,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
new MyDataHandlerHook(
|
||||||
|
$configMock,
|
||||||
|
$this->logManagerMock,
|
||||||
|
$this->resourceFactoryMock,
|
||||||
|
$this->contextMock,
|
||||||
|
$this->requestFactoryMock,
|
||||||
|
$this->uploadFolderResolverMock,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Testing Patterns for DataHandler Hooks:**
|
||||||
|
|
||||||
|
1. **Intersection Types for PHPStan**: Use `ResourceFactory&MockObject` for strict type compliance
|
||||||
|
2. **TCA Globals**: Set `$GLOBALS['TCA']` in tests to simulate TYPO3 table configuration
|
||||||
|
3. **Extension Configuration**: Use `willReturnCallback` with `match` expressions for flexible config mocking
|
||||||
|
4. **DataHandler Mock**: Create mock for `$dataHandler` parameter (required in hook signature)
|
||||||
|
5. **Reset Singletons**: Always set `protected bool $resetSingletonInstances = true;`
|
||||||
|
6. **Constructor DI**: Inject all dependencies via constructor (TYPO3 13+ best practice)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [TYPO3 Unit Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/UnitTests.html)
|
||||||
|
- [PHPUnit Documentation](https://phpunit.de/documentation.html)
|
||||||
|
- [PHPUnit 11 Migration Guide](https://phpunit.de/announcements/phpunit-11.html)
|
||||||
|
- [TYPO3 DataHandler Hooks](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Hooks/DataHandler/Index.html)
|
||||||
224
scripts/generate-test.sh
Executable file
224
scripts/generate-test.sh
Executable file
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
#
|
||||||
|
# Generate TYPO3 test class
|
||||||
|
#
|
||||||
|
# Usage: ./generate-test.sh <type> <ClassName>
|
||||||
|
# Example: ./generate-test.sh unit EmailValidator
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
TEST_TYPE="$1"
|
||||||
|
CLASS_NAME="$2"
|
||||||
|
|
||||||
|
if [ -z "${TEST_TYPE}" ] || [ -z "${CLASS_NAME}" ]; then
|
||||||
|
echo "Usage: $0 <type> <ClassName>"
|
||||||
|
echo
|
||||||
|
echo "Types:"
|
||||||
|
echo " unit - Unit test (fast, no database)"
|
||||||
|
echo " functional - Functional test (with database)"
|
||||||
|
echo " acceptance - Acceptance test (browser-based)"
|
||||||
|
echo
|
||||||
|
echo "Example:"
|
||||||
|
echo " $0 unit EmailValidator"
|
||||||
|
echo " $0 functional ProductRepository"
|
||||||
|
echo " $0 acceptance LoginCest"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate test type
|
||||||
|
case ${TEST_TYPE} in
|
||||||
|
unit|functional|acceptance)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Error: Invalid test type '${TEST_TYPE}'${NC}"
|
||||||
|
echo "Valid types: unit, functional, acceptance"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Determine paths
|
||||||
|
PROJECT_DIR="$(pwd)"
|
||||||
|
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
# Check if Tests directory exists
|
||||||
|
if [ ! -d "${PROJECT_DIR}/Tests" ]; then
|
||||||
|
echo -e "${RED}Error: Tests directory not found${NC}"
|
||||||
|
echo "Run setup-testing.sh first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set target directory based on test type
|
||||||
|
case ${TEST_TYPE} in
|
||||||
|
unit)
|
||||||
|
TEST_DIR="${PROJECT_DIR}/Tests/Unit"
|
||||||
|
TEST_SUFFIX="Test"
|
||||||
|
;;
|
||||||
|
functional)
|
||||||
|
TEST_DIR="${PROJECT_DIR}/Tests/Functional"
|
||||||
|
TEST_SUFFIX="Test"
|
||||||
|
;;
|
||||||
|
acceptance)
|
||||||
|
TEST_DIR="${PROJECT_DIR}/Tests/Acceptance"
|
||||||
|
TEST_SUFFIX="Cest"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Extract namespace from composer.json
|
||||||
|
NAMESPACE=$(php -r '
|
||||||
|
$composer = json_decode(file_get_contents("composer.json"), true);
|
||||||
|
foreach ($composer["autoload"]["psr-4"] ?? [] as $ns => $path) {
|
||||||
|
if (strpos($path, "Classes") !== false) {
|
||||||
|
echo rtrim($ns, "\\");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
')
|
||||||
|
|
||||||
|
if [ -z "${NAMESPACE}" ]; then
|
||||||
|
echo -e "${RED}Error: Could not determine namespace from composer.json${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine test file path
|
||||||
|
TEST_FILE="${TEST_DIR}/${CLASS_NAME}${TEST_SUFFIX}.php"
|
||||||
|
|
||||||
|
# Check if file already exists
|
||||||
|
if [ -f "${TEST_FILE}" ]; then
|
||||||
|
echo -e "${RED}Error: Test file already exists: ${TEST_FILE}${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create test file directory if needed
|
||||||
|
mkdir -p "$(dirname "${TEST_FILE}")"
|
||||||
|
|
||||||
|
echo -e "${GREEN}Generating ${TEST_TYPE} test for ${CLASS_NAME}...${NC}"
|
||||||
|
|
||||||
|
# Generate test class based on type
|
||||||
|
case ${TEST_TYPE} in
|
||||||
|
unit)
|
||||||
|
cat > "${TEST_FILE}" << EOF
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ${NAMESPACE}\\Tests\\Unit;
|
||||||
|
|
||||||
|
use TYPO3\\TestingFramework\\Core\\Unit\\UnitTestCase;
|
||||||
|
use ${NAMESPACE}\\${CLASS_NAME};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit test for ${CLASS_NAME}
|
||||||
|
*/
|
||||||
|
final class ${CLASS_NAME}${TEST_SUFFIX} extends UnitTestCase
|
||||||
|
{
|
||||||
|
protected ${CLASS_NAME} \$subject;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
\$this->subject = new ${CLASS_NAME}();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function canBeInstantiated(): void
|
||||||
|
{
|
||||||
|
self::assertInstanceOf(${CLASS_NAME}::class, \$this->subject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
|
||||||
|
functional)
|
||||||
|
cat > "${TEST_FILE}" << EOF
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ${NAMESPACE}\\Tests\\Functional;
|
||||||
|
|
||||||
|
use TYPO3\\TestingFramework\\Core\\Functional\\FunctionalTestCase;
|
||||||
|
use ${NAMESPACE}\\${CLASS_NAME};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional test for ${CLASS_NAME}
|
||||||
|
*/
|
||||||
|
final class ${CLASS_NAME}${TEST_SUFFIX} extends FunctionalTestCase
|
||||||
|
{
|
||||||
|
protected ${CLASS_NAME} \$subject;
|
||||||
|
|
||||||
|
protected array \$testExtensionsToLoad = [
|
||||||
|
'typo3conf/ext/your_extension',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
\$this->subject = \$this->get(${CLASS_NAME}::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function canBeInstantiated(): void
|
||||||
|
{
|
||||||
|
self::assertInstanceOf(${CLASS_NAME}::class, \$this->subject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create fixture file
|
||||||
|
FIXTURE_FILE="${PROJECT_DIR}/Tests/Functional/Fixtures/${CLASS_NAME}.csv"
|
||||||
|
if [ ! -f "${FIXTURE_FILE}" ]; then
|
||||||
|
echo "# Fixture for ${CLASS_NAME}${TEST_SUFFIX}" > "${FIXTURE_FILE}"
|
||||||
|
echo -e "${GREEN}✓ Created fixture: ${FIXTURE_FILE}${NC}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
acceptance)
|
||||||
|
cat > "${TEST_FILE}" << EOF
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ${NAMESPACE}\\Tests\\Acceptance;
|
||||||
|
|
||||||
|
use ${NAMESPACE}\\Tests\\Acceptance\\AcceptanceTester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acceptance test for ${CLASS_NAME/Cest/} workflow
|
||||||
|
*/
|
||||||
|
final class ${CLASS_NAME}${TEST_SUFFIX}
|
||||||
|
{
|
||||||
|
public function _before(AcceptanceTester \$I): void
|
||||||
|
{
|
||||||
|
// Setup before each test
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exampleTest(AcceptanceTester \$I): void
|
||||||
|
{
|
||||||
|
\$I->amOnPage('/');
|
||||||
|
\$I->see('Welcome');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Created: ${TEST_FILE}${NC}"
|
||||||
|
echo
|
||||||
|
echo "Run test:"
|
||||||
|
echo " vendor/bin/phpunit ${TEST_FILE}"
|
||||||
|
echo
|
||||||
|
echo "Or via composer:"
|
||||||
|
echo " composer ci:test:php:${TEST_TYPE}"
|
||||||
180
scripts/setup-testing.sh
Executable file
180
scripts/setup-testing.sh
Executable file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
#
|
||||||
|
# Setup TYPO3 testing infrastructure
|
||||||
|
#
|
||||||
|
# This script initializes testing infrastructure for TYPO3 extensions:
|
||||||
|
# - Composer dependencies
|
||||||
|
# - PHPUnit configurations
|
||||||
|
# - Directory structure
|
||||||
|
# - Optional: Docker Compose for acceptance tests
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Script configuration
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
PROJECT_DIR="$(pwd)"
|
||||||
|
|
||||||
|
WITH_ACCEPTANCE=false
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while getopts ":a" opt; do
|
||||||
|
case ${opt} in
|
||||||
|
a)
|
||||||
|
WITH_ACCEPTANCE=true
|
||||||
|
;;
|
||||||
|
\?)
|
||||||
|
echo "Usage: $0 [-a]"
|
||||||
|
echo " -a Include acceptance testing setup (Docker Compose, Codeception)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== TYPO3 Testing Infrastructure Setup ===${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check if composer.json exists
|
||||||
|
if [ ! -f "${PROJECT_DIR}/composer.json" ]; then
|
||||||
|
echo -e "${RED}Error: composer.json not found in current directory${NC}"
|
||||||
|
echo "Please run this script from your TYPO3 extension root directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Install testing framework dependencies
|
||||||
|
echo -e "${YELLOW}[1/6] Installing testing framework dependencies...${NC}"
|
||||||
|
if ! grep -q "typo3/testing-framework" "${PROJECT_DIR}/composer.json"; then
|
||||||
|
composer require --dev "typo3/testing-framework:^8.0 || ^9.0" --no-update
|
||||||
|
echo -e "${GREEN}✓ Added typo3/testing-framework${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓ typo3/testing-framework already present${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install PHPUnit if not present
|
||||||
|
if ! grep -q "phpunit/phpunit" "${PROJECT_DIR}/composer.json"; then
|
||||||
|
composer require --dev "phpunit/phpunit:^10.5 || ^11.0" --no-update
|
||||||
|
echo -e "${GREEN}✓ Added phpunit/phpunit${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
composer update --no-progress
|
||||||
|
|
||||||
|
# 2. Create directory structure
|
||||||
|
echo -e "${YELLOW}[2/6] Creating directory structure...${NC}"
|
||||||
|
mkdir -p "${PROJECT_DIR}/Tests/Unit"
|
||||||
|
mkdir -p "${PROJECT_DIR}/Tests/Functional/Fixtures"
|
||||||
|
mkdir -p "${PROJECT_DIR}/Build/phpunit"
|
||||||
|
mkdir -p "${PROJECT_DIR}/Build/Scripts"
|
||||||
|
echo -e "${GREEN}✓ Directories created${NC}"
|
||||||
|
|
||||||
|
# 3. Copy PHPUnit configurations
|
||||||
|
echo -e "${YELLOW}[3/6] Installing PHPUnit configurations...${NC}"
|
||||||
|
if [ ! -f "${PROJECT_DIR}/Build/phpunit/UnitTests.xml" ]; then
|
||||||
|
cp "${SKILL_DIR}/templates/UnitTests.xml" "${PROJECT_DIR}/Build/phpunit/"
|
||||||
|
echo -e "${GREEN}✓ Created UnitTests.xml${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ UnitTests.xml already exists (skipped)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${PROJECT_DIR}/Build/phpunit/FunctionalTests.xml" ]; then
|
||||||
|
cp "${SKILL_DIR}/templates/FunctionalTests.xml" "${PROJECT_DIR}/Build/phpunit/"
|
||||||
|
echo -e "${GREEN}✓ Created FunctionalTests.xml${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ FunctionalTests.xml already exists (skipped)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${PROJECT_DIR}/Build/phpunit/FunctionalTestsBootstrap.php" ]; then
|
||||||
|
cp "${SKILL_DIR}/templates/FunctionalTestsBootstrap.php" "${PROJECT_DIR}/Build/phpunit/"
|
||||||
|
echo -e "${GREEN}✓ Created FunctionalTestsBootstrap.php${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ FunctionalTestsBootstrap.php already exists (skipped)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Create AGENTS.md templates
|
||||||
|
echo -e "${YELLOW}[4/6] Creating AGENTS.md templates...${NC}"
|
||||||
|
for dir in "${PROJECT_DIR}/Tests/Unit" "${PROJECT_DIR}/Tests/Functional"; do
|
||||||
|
if [ ! -f "${dir}/AGENTS.md" ]; then
|
||||||
|
cp "${SKILL_DIR}/templates/AGENTS.md" "${dir}/"
|
||||||
|
echo -e "${GREEN}✓ Created ${dir}/AGENTS.md${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ ${dir}/AGENTS.md already exists (skipped)${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 5. Setup composer scripts
|
||||||
|
echo -e "${YELLOW}[5/6] Adding composer test scripts...${NC}"
|
||||||
|
if ! grep -q "ci:test:php:unit" "${PROJECT_DIR}/composer.json"; then
|
||||||
|
echo -e "${GREEN}ℹ Add these scripts to your composer.json:${NC}"
|
||||||
|
cat << 'EOF'
|
||||||
|
|
||||||
|
"scripts": {
|
||||||
|
"ci:test": [
|
||||||
|
"@ci:test:php:lint",
|
||||||
|
"@ci:test:php:phpstan",
|
||||||
|
"@ci:test:php:unit",
|
||||||
|
"@ci:test:php:functional"
|
||||||
|
],
|
||||||
|
"ci:test:php:lint": "phplint",
|
||||||
|
"ci:test:php:phpstan": "phpstan analyze --configuration Build/phpstan.neon --no-progress",
|
||||||
|
"ci:test:php:unit": "phpunit -c Build/phpunit/UnitTests.xml",
|
||||||
|
"ci:test:php:functional": "phpunit -c Build/phpunit/FunctionalTests.xml"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓ Test scripts already configured${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Setup acceptance testing if requested
|
||||||
|
if [ "${WITH_ACCEPTANCE}" = true ]; then
|
||||||
|
echo -e "${YELLOW}[6/6] Setting up acceptance testing...${NC}"
|
||||||
|
|
||||||
|
# Install Codeception
|
||||||
|
if ! grep -q "codeception/codeception" "${PROJECT_DIR}/composer.json"; then
|
||||||
|
composer require --dev codeception/codeception codeception/module-webdriver --no-update
|
||||||
|
composer update --no-progress
|
||||||
|
echo -e "${GREEN}✓ Installed Codeception${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create acceptance test directory
|
||||||
|
mkdir -p "${PROJECT_DIR}/Tests/Acceptance"
|
||||||
|
|
||||||
|
# Copy Docker Compose and Codeception config
|
||||||
|
if [ ! -f "${PROJECT_DIR}/Build/docker-compose.yml" ]; then
|
||||||
|
cp "${SKILL_DIR}/templates/docker/docker-compose.yml" "${PROJECT_DIR}/Build/"
|
||||||
|
echo -e "${GREEN}✓ Created docker-compose.yml${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${PROJECT_DIR}/codeception.yml" ]; then
|
||||||
|
cp "${SKILL_DIR}/templates/docker/codeception.yml" "${PROJECT_DIR}/"
|
||||||
|
echo -e "${GREEN}✓ Created codeception.yml${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize Codeception
|
||||||
|
if [ ! -d "${PROJECT_DIR}/Tests/Acceptance/_support" ]; then
|
||||||
|
vendor/bin/codecept bootstrap
|
||||||
|
echo -e "${GREEN}✓ Initialized Codeception${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}[6/6] Skipping acceptance testing setup (use -a flag to include)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}=== Setup Complete ===${NC}"
|
||||||
|
echo
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Generate your first test:"
|
||||||
|
echo " ${SKILL_DIR}/scripts/generate-test.sh unit MyClass"
|
||||||
|
echo
|
||||||
|
echo "2. Run tests:"
|
||||||
|
echo " composer ci:test:php:unit"
|
||||||
|
echo " composer ci:test:php:functional"
|
||||||
|
echo
|
||||||
|
echo "3. Add CI/CD workflow (optional):"
|
||||||
|
echo " cp ${SKILL_DIR}/templates/github-actions-tests.yml .github/workflows/tests.yml"
|
||||||
135
scripts/validate-setup.sh
Executable file
135
scripts/validate-setup.sh
Executable file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
#
|
||||||
|
# Validate TYPO3 testing infrastructure setup
|
||||||
|
#
|
||||||
|
# Checks:
|
||||||
|
# - Required dependencies
|
||||||
|
# - PHPUnit configurations
|
||||||
|
# - Directory structure
|
||||||
|
# - Docker (for acceptance tests)
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
PROJECT_DIR="$(pwd)"
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== TYPO3 Testing Setup Validation ===${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check composer.json
|
||||||
|
echo -e "${YELLOW}[1/5] Checking composer.json dependencies...${NC}"
|
||||||
|
if [ ! -f "${PROJECT_DIR}/composer.json" ]; then
|
||||||
|
echo -e "${RED}✗ composer.json not found${NC}"
|
||||||
|
((ERRORS++))
|
||||||
|
else
|
||||||
|
if grep -q "typo3/testing-framework" "${PROJECT_DIR}/composer.json"; then
|
||||||
|
echo -e "${GREEN}✓ typo3/testing-framework installed${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ typo3/testing-framework missing${NC}"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "phpunit/phpunit" "${PROJECT_DIR}/composer.json"; then
|
||||||
|
echo -e "${GREEN}✓ phpunit/phpunit installed${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ phpunit/phpunit missing${NC}"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check PHPUnit configurations
|
||||||
|
echo -e "${YELLOW}[2/5] Checking PHPUnit configurations...${NC}"
|
||||||
|
if [ -f "${PROJECT_DIR}/Build/phpunit/UnitTests.xml" ]; then
|
||||||
|
echo -e "${GREEN}✓ UnitTests.xml present${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ UnitTests.xml missing${NC}"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${PROJECT_DIR}/Build/phpunit/FunctionalTests.xml" ]; then
|
||||||
|
echo -e "${GREEN}✓ FunctionalTests.xml present${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ FunctionalTests.xml missing${NC}"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${PROJECT_DIR}/Build/phpunit/FunctionalTestsBootstrap.php" ]; then
|
||||||
|
echo -e "${GREEN}✓ FunctionalTestsBootstrap.php present${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ FunctionalTestsBootstrap.php missing${NC}"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check directory structure
|
||||||
|
echo -e "${YELLOW}[3/5] Checking directory structure...${NC}"
|
||||||
|
for dir in "Tests/Unit" "Tests/Functional" "Tests/Functional/Fixtures"; do
|
||||||
|
if [ -d "${PROJECT_DIR}/${dir}" ]; then
|
||||||
|
echo -e "${GREEN}✓ ${dir}/ exists${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ ${dir}/ missing${NC}"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check AGENTS.md files
|
||||||
|
echo -e "${YELLOW}[4/5] Checking AGENTS.md documentation...${NC}"
|
||||||
|
for dir in "Tests/Unit" "Tests/Functional"; do
|
||||||
|
if [ -f "${PROJECT_DIR}/${dir}/AGENTS.md" ]; then
|
||||||
|
echo -e "${GREEN}✓ ${dir}/AGENTS.md present${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ ${dir}/AGENTS.md missing${NC}"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check Docker (optional, for acceptance tests)
|
||||||
|
echo -e "${YELLOW}[5/5] Checking Docker availability (for acceptance tests)...${NC}"
|
||||||
|
if command -v docker &> /dev/null; then
|
||||||
|
echo -e "${GREEN}✓ Docker installed${NC}"
|
||||||
|
|
||||||
|
if docker ps &> /dev/null; then
|
||||||
|
echo -e "${GREEN}✓ Docker daemon running${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ Docker daemon not running${NC}"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ Docker not installed (required for acceptance tests)${NC}"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}=== Validation Summary ===${NC}"
|
||||||
|
|
||||||
|
if [ ${ERRORS} -eq 0 ] && [ ${WARNINGS} -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✓ All checks passed!${NC}"
|
||||||
|
echo
|
||||||
|
echo "Your testing infrastructure is ready to use."
|
||||||
|
echo "Generate your first test:"
|
||||||
|
echo " ~/.claude/skills/typo3-testing/scripts/generate-test.sh unit MyClass"
|
||||||
|
exit 0
|
||||||
|
elif [ ${ERRORS} -eq 0 ]; then
|
||||||
|
echo -e "${YELLOW}⚠ ${WARNINGS} warnings found${NC}"
|
||||||
|
echo
|
||||||
|
echo "Basic setup is complete, but some optional components are missing."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ ${ERRORS} errors found${NC}"
|
||||||
|
if [ ${WARNINGS} -gt 0 ]; then
|
||||||
|
echo -e "${YELLOW}⚠ ${WARNINGS} warnings found${NC}"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "Run setup script to fix errors:"
|
||||||
|
echo " ~/.claude/skills/typo3-testing/scripts/setup-testing.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
154
templates/AGENTS.md
Normal file
154
templates/AGENTS.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Testing Context for AI Assistants
|
||||||
|
|
||||||
|
This directory contains tests for the TYPO3 extension.
|
||||||
|
|
||||||
|
## Test Type
|
||||||
|
|
||||||
|
**[Unit|Functional|Acceptance]** tests
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
|
||||||
|
<!-- Describe what this directory tests and why -->
|
||||||
|
<!-- Example: "Unit tests for domain models - validates business logic without database" -->
|
||||||
|
<!-- Example: "Functional tests for repositories - verifies database queries and persistence" -->
|
||||||
|
<!-- Example: "Acceptance tests for checkout workflow - validates complete user journey from cart to payment" -->
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
|
||||||
|
**Key Scenarios:**
|
||||||
|
|
||||||
|
**Not Covered:** <!-- What is intentionally not tested here -->
|
||||||
|
|
||||||
|
## Testing Framework
|
||||||
|
|
||||||
|
- **TYPO3 Testing Framework** (typo3/testing-framework)
|
||||||
|
- **PHPUnit** for assertions and test execution
|
||||||
|
- **[Additional tools for this test type]:**
|
||||||
|
- Unit: Prophecy for mocking
|
||||||
|
- Functional: CSV fixtures for database data
|
||||||
|
- Acceptance: Codeception + Selenium for browser automation
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
### Base Class
|
||||||
|
|
||||||
|
Tests in this directory extend:
|
||||||
|
- **Unit**: `TYPO3\TestingFramework\Core\Unit\UnitTestCase`
|
||||||
|
- **Functional**: `TYPO3\TestingFramework\Core\Functional\FunctionalTestCase`
|
||||||
|
- **Acceptance**: Codeception Cest classes
|
||||||
|
|
||||||
|
### Naming Convention
|
||||||
|
|
||||||
|
- **Unit/Functional**: `*Test.php` (e.g., `ProductTest.php`, `ProductRepositoryTest.php`)
|
||||||
|
- **Acceptance**: `*Cest.php` (e.g., `LoginCest.php`, `CheckoutCest.php`)
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
### setUp() and tearDown()
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
// Initialize test dependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
// Clean up resources
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
|
||||||
|
Use specific assertions over generic ones:
|
||||||
|
- `self::assertTrue()`, `self::assertFalse()` for booleans
|
||||||
|
- `self::assertSame()` for strict equality
|
||||||
|
- `self::assertInstanceOf()` for type checks
|
||||||
|
- `self::assertCount()` for arrays/collections
|
||||||
|
|
||||||
|
### Fixtures (Functional Tests Only)
|
||||||
|
|
||||||
|
```php
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/MyFixture.csv');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fixture Files:** `Tests/Functional/Fixtures/`
|
||||||
|
|
||||||
|
**Strategy:**
|
||||||
|
- Keep fixtures minimal (only required data)
|
||||||
|
- One fixture per test scenario
|
||||||
|
- Document fixture contents in test or below
|
||||||
|
|
||||||
|
### Mocking (Unit Tests Only)
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
|
||||||
|
$repository = $this->prophesize(UserRepository::class);
|
||||||
|
$repository->findByEmail('test@example.com')->willReturn($user);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All tests in this directory
|
||||||
|
composer ci:test:php:[unit|functional|acceptance]
|
||||||
|
|
||||||
|
# Via runTests.sh
|
||||||
|
Build/Scripts/runTests.sh -s [unit|functional|acceptance]
|
||||||
|
|
||||||
|
# Specific test file
|
||||||
|
vendor/bin/phpunit Tests/[Unit|Functional]/Path/To/TestFile.php
|
||||||
|
|
||||||
|
# Specific test method
|
||||||
|
vendor/bin/phpunit --filter testMethodName
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fixtures Documentation (Functional Tests)
|
||||||
|
|
||||||
|
<!-- Document what each fixture contains -->
|
||||||
|
|
||||||
|
### `Fixtures/BasicProducts.csv`
|
||||||
|
- 3 products in category 1
|
||||||
|
- 2 products in category 2
|
||||||
|
- All products visible and published
|
||||||
|
|
||||||
|
### `Fixtures/PageTree.csv`
|
||||||
|
- Root page (uid: 1)
|
||||||
|
- Products page (uid: 2, pid: 1)
|
||||||
|
- Services page (uid: 3, pid: 1)
|
||||||
|
|
||||||
|
## Test Dependencies
|
||||||
|
|
||||||
|
<!-- List any special dependencies or requirements -->
|
||||||
|
|
||||||
|
- [ ] Database (functional tests only)
|
||||||
|
- [ ] Docker (acceptance tests only)
|
||||||
|
- [ ] Specific TYPO3 extensions: <!-- list if any -->
|
||||||
|
- [ ] External services: <!-- list if any -->
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
<!-- Document common test failures and solutions -->
|
||||||
|
|
||||||
|
**Database connection errors:**
|
||||||
|
- Verify database driver configuration in `FunctionalTests.xml`
|
||||||
|
- Check Docker database service is running
|
||||||
|
|
||||||
|
**Fixture import errors:**
|
||||||
|
- Verify CSV format (proper escaping, matching table structure)
|
||||||
|
- Check file paths are correct relative to test class
|
||||||
|
|
||||||
|
**Flaky tests:**
|
||||||
|
- Use proper waits in acceptance tests (`waitForElement`)
|
||||||
|
- Avoid timing dependencies in unit/functional tests
|
||||||
|
- Ensure test independence (no shared state)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Unit Testing Guide](~/.claude/skills/typo3-testing/references/unit-testing.md)
|
||||||
|
- [Functional Testing Guide](~/.claude/skills/typo3-testing/references/functional-testing.md)
|
||||||
|
- [Acceptance Testing Guide](~/.claude/skills/typo3-testing/references/acceptance-testing.md)
|
||||||
|
- [TYPO3 Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/)
|
||||||
36
templates/FunctionalTests.xml
Normal file
36
templates/FunctionalTests.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="../../vendor/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="FunctionalTestsBootstrap.php"
|
||||||
|
cacheResult="false"
|
||||||
|
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
failOnDeprecation="true"
|
||||||
|
failOnNotice="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
colors="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Functional tests">
|
||||||
|
<directory>../../Tests/Functional/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<php>
|
||||||
|
<const name="TYPO3_TESTING_FUNCTIONAL_REMOVE_ERROR_HANDLER" value="true" />
|
||||||
|
<env name="TYPO3_CONTEXT" value="Testing"/>
|
||||||
|
<env name="typo3DatabaseDriver" value="mysqli" force="true"/>
|
||||||
|
<env name="typo3DatabaseHost" value="localhost" force="true"/>
|
||||||
|
<env name="typo3DatabasePort" value="3306" force="true"/>
|
||||||
|
<env name="typo3DatabaseName" value="typo3_test" force="true"/>
|
||||||
|
<env name="typo3DatabaseUsername" value="root" force="true"/>
|
||||||
|
<env name="typo3DatabasePassword" value="" force="true"/>
|
||||||
|
</php>
|
||||||
|
<coverage>
|
||||||
|
<report>
|
||||||
|
<clover outputFile="../../var/log/coverage/clover.xml"/>
|
||||||
|
<html outputDirectory="../../var/log/coverage/html"/>
|
||||||
|
<text outputFile="php://stdout" showOnlySummary="true"/>
|
||||||
|
</report>
|
||||||
|
</coverage>
|
||||||
|
</phpunit>
|
||||||
21
templates/FunctionalTestsBootstrap.php
Normal file
21
templates/FunctionalTestsBootstrap.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap file for TYPO3 functional tests
|
||||||
|
*
|
||||||
|
* This file initializes the testing environment for functional tests.
|
||||||
|
* It sets up the necessary directory structure and prepares the TYPO3 instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
call_user_func(static function () {
|
||||||
|
$testbase = new \TYPO3\TestingFramework\Core\Testbase();
|
||||||
|
|
||||||
|
// Define original root path
|
||||||
|
$testbase->defineOriginalRootPath();
|
||||||
|
|
||||||
|
// Create necessary directories for test execution
|
||||||
|
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests');
|
||||||
|
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient');
|
||||||
|
});
|
||||||
26
templates/UnitTests.xml
Normal file
26
templates/UnitTests.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="../../vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="../../vendor/autoload.php"
|
||||||
|
cacheResult="false"
|
||||||
|
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
failOnDeprecation="true"
|
||||||
|
failOnNotice="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
colors="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit tests">
|
||||||
|
<directory>../../Tests/Unit/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<coverage>
|
||||||
|
<report>
|
||||||
|
<clover outputFile="../../var/log/coverage/clover.xml"/>
|
||||||
|
<html outputDirectory="../../var/log/coverage/html"/>
|
||||||
|
<text outputFile="php://stdout" showOnlySummary="true"/>
|
||||||
|
</report>
|
||||||
|
</coverage>
|
||||||
|
</phpunit>
|
||||||
39
templates/docker/codeception.yml
Normal file
39
templates/docker/codeception.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
paths:
|
||||||
|
tests: Tests/Acceptance
|
||||||
|
output: var/log/acceptance
|
||||||
|
data: Tests/Acceptance/_data
|
||||||
|
support: Tests/Acceptance/_support
|
||||||
|
envs: Tests/Acceptance/_envs
|
||||||
|
|
||||||
|
actor_suffix: Tester
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
enabled:
|
||||||
|
- Codeception\Extension\RunFailed
|
||||||
|
|
||||||
|
suites:
|
||||||
|
acceptance:
|
||||||
|
actor: AcceptanceTester
|
||||||
|
path: .
|
||||||
|
modules:
|
||||||
|
enabled:
|
||||||
|
- WebDriver:
|
||||||
|
url: http://web:8000
|
||||||
|
browser: chrome
|
||||||
|
host: selenium
|
||||||
|
port: 4444
|
||||||
|
wait: 2
|
||||||
|
window_size: 1920x1080
|
||||||
|
capabilities:
|
||||||
|
chromeOptions:
|
||||||
|
args: ["--no-sandbox", "--disable-dev-shm-usage"]
|
||||||
|
- \\Helper\\Acceptance
|
||||||
|
config:
|
||||||
|
WebDriver:
|
||||||
|
browser: '%BROWSER%'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
shuffle: false
|
||||||
|
lint: true
|
||||||
|
colors: true
|
||||||
|
memory_limit: 1024M
|
||||||
51
templates/docker/docker-compose.yml
Normal file
51
templates/docker/docker-compose.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: php:8.4-apache
|
||||||
|
container_name: typo3-test-web
|
||||||
|
volumes:
|
||||||
|
- ../../../:/var/www/html
|
||||||
|
ports:
|
||||||
|
- "8000:80"
|
||||||
|
environment:
|
||||||
|
- TYPO3_CONTEXT=Testing
|
||||||
|
- typo3DatabaseDriver=mysqli
|
||||||
|
- typo3DatabaseHost=db
|
||||||
|
- typo3DatabaseName=typo3_test
|
||||||
|
- typo3DatabaseUsername=typo3
|
||||||
|
- typo3DatabasePassword=typo3
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- typo3-test
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: typo3-test-db
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: typo3_test
|
||||||
|
MYSQL_USER: typo3
|
||||||
|
MYSQL_PASSWORD: typo3
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- typo3-test
|
||||||
|
|
||||||
|
selenium:
|
||||||
|
image: selenium/standalone-chrome:latest
|
||||||
|
container_name: typo3-test-selenium
|
||||||
|
ports:
|
||||||
|
- "4444:4444"
|
||||||
|
shm_size: 2gb
|
||||||
|
networks:
|
||||||
|
- typo3-test
|
||||||
|
|
||||||
|
networks:
|
||||||
|
typo3-test:
|
||||||
|
driver: bridge
|
||||||
87
templates/example-tests/ExampleAcceptanceCest.php
Normal file
87
templates/example-tests/ExampleAcceptanceCest.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Acceptance;
|
||||||
|
|
||||||
|
use Vendor\Extension\Tests\Acceptance\AcceptanceTester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example acceptance test demonstrating TYPO3 testing patterns
|
||||||
|
*
|
||||||
|
* Acceptance tests use a real browser to test complete user workflows.
|
||||||
|
* They verify frontend functionality and user interactions.
|
||||||
|
*/
|
||||||
|
final class LoginCest
|
||||||
|
{
|
||||||
|
public function _before(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
// Runs before each test method
|
||||||
|
// Setup: Import fixtures, reset state, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loginAsBackendUser(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
// Navigate to login page
|
||||||
|
$I->amOnPage('/typo3');
|
||||||
|
|
||||||
|
// Fill login form
|
||||||
|
$I->fillField('username', 'admin');
|
||||||
|
$I->fillField('password', 'password');
|
||||||
|
|
||||||
|
// Submit form
|
||||||
|
$I->click('Login');
|
||||||
|
|
||||||
|
// Verify successful login
|
||||||
|
$I->see('Dashboard');
|
||||||
|
$I->seeInCurrentUrl('/typo3/module/dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loginFailsWithInvalidCredentials(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$I->amOnPage('/typo3');
|
||||||
|
|
||||||
|
$I->fillField('username', 'admin');
|
||||||
|
$I->fillField('password', 'wrong_password');
|
||||||
|
$I->click('Login');
|
||||||
|
|
||||||
|
// Verify login failed
|
||||||
|
$I->see('Login error');
|
||||||
|
$I->seeInCurrentUrl('/typo3');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchesForProducts(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
// Navigate to product listing
|
||||||
|
$I->amOnPage('/products');
|
||||||
|
|
||||||
|
// Wait for page to load
|
||||||
|
$I->waitForElement('.product-list', 5);
|
||||||
|
|
||||||
|
// Use search
|
||||||
|
$I->fillField('#search', 'laptop');
|
||||||
|
$I->click('Search');
|
||||||
|
|
||||||
|
// Wait for results
|
||||||
|
$I->waitForElement('.search-results', 5);
|
||||||
|
|
||||||
|
// Verify search results
|
||||||
|
$I->see('laptop', '.product-title');
|
||||||
|
$I->seeNumberOfElements('.product-item', [1, 10]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addsProductToCart(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$I->amOnPage('/products/1');
|
||||||
|
|
||||||
|
// Click add to cart button
|
||||||
|
$I->click('#add-to-cart');
|
||||||
|
|
||||||
|
// Wait for AJAX response
|
||||||
|
$I->waitForElement('.cart-badge', 3);
|
||||||
|
|
||||||
|
// Verify cart updated
|
||||||
|
$I->see('1', '.cart-badge');
|
||||||
|
$I->see('Product added to cart');
|
||||||
|
}
|
||||||
|
}
|
||||||
91
templates/example-tests/ExampleFunctionalTest.php
Normal file
91
templates/example-tests/ExampleFunctionalTest.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Functional\Domain\Repository;
|
||||||
|
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
|
||||||
|
use Vendor\Extension\Domain\Model\Product;
|
||||||
|
use Vendor\Extension\Domain\Repository\ProductRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example functional test demonstrating TYPO3 testing patterns
|
||||||
|
*
|
||||||
|
* Functional tests use a real database and full TYPO3 instance.
|
||||||
|
* They test repositories, controllers, and integration scenarios.
|
||||||
|
*/
|
||||||
|
final class ProductRepositoryTest extends FunctionalTestCase
|
||||||
|
{
|
||||||
|
protected ProductRepository $subject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions to load for this test
|
||||||
|
*/
|
||||||
|
protected array $testExtensionsToLoad = [
|
||||||
|
'typo3conf/ext/my_extension',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Get repository from dependency injection container
|
||||||
|
$this->subject = $this->get(ProductRepository::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function findsProductsByCategory(): void
|
||||||
|
{
|
||||||
|
// Import test data from CSV fixture
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
|
||||||
|
|
||||||
|
// Execute repository method
|
||||||
|
$products = $this->subject->findByCategory(1);
|
||||||
|
|
||||||
|
// Assert results
|
||||||
|
self::assertCount(3, $products);
|
||||||
|
self::assertInstanceOf(Product::class, $products[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function findsVisibleProductsOnly(): void
|
||||||
|
{
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/ProductsWithHidden.csv');
|
||||||
|
|
||||||
|
$products = $this->subject->findAll();
|
||||||
|
|
||||||
|
// Only visible products should be returned
|
||||||
|
self::assertCount(2, $products);
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
self::assertFalse($product->isHidden());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function persistsNewProduct(): void
|
||||||
|
{
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Pages.csv');
|
||||||
|
|
||||||
|
$product = new Product();
|
||||||
|
$product->setTitle('New Product');
|
||||||
|
$product->setPrice(19.99);
|
||||||
|
$product->setPid(1);
|
||||||
|
|
||||||
|
$this->subject->add($product);
|
||||||
|
|
||||||
|
// Persist to database
|
||||||
|
$this->persistenceManager->persistAll();
|
||||||
|
|
||||||
|
// Verify product was saved
|
||||||
|
$savedProducts = $this->subject->findAll();
|
||||||
|
self::assertCount(1, $savedProducts);
|
||||||
|
self::assertSame('New Product', $savedProducts[0]->getTitle());
|
||||||
|
}
|
||||||
|
}
|
||||||
66
templates/example-tests/ExampleUnitTest.php
Normal file
66
templates/example-tests/ExampleUnitTest.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Vendor\Extension\Tests\Unit\Domain\Validator;
|
||||||
|
|
||||||
|
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
|
||||||
|
use Vendor\Extension\Domain\Validator\EmailValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example unit test demonstrating TYPO3 testing patterns
|
||||||
|
*
|
||||||
|
* Unit tests are fast, isolated tests without external dependencies.
|
||||||
|
* They test individual components (validators, utilities, domain logic).
|
||||||
|
*/
|
||||||
|
final class EmailValidatorTest extends UnitTestCase
|
||||||
|
{
|
||||||
|
protected EmailValidator $subject;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->subject = new EmailValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function validEmailPassesValidation(): void
|
||||||
|
{
|
||||||
|
$result = $this->subject->validate('user@example.com');
|
||||||
|
|
||||||
|
self::assertFalse($result->hasErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function invalidEmailFailsValidation(): void
|
||||||
|
{
|
||||||
|
$result = $this->subject->validate('invalid-email');
|
||||||
|
|
||||||
|
self::assertTrue($result->hasErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider invalidEmailProvider
|
||||||
|
*/
|
||||||
|
public function rejectsInvalidEmails(string $email): void
|
||||||
|
{
|
||||||
|
$result = $this->subject->validate($email);
|
||||||
|
|
||||||
|
self::assertTrue($result->hasErrors(), "Email '$email' should be invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function invalidEmailProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'missing @' => ['userexample.com'],
|
||||||
|
'missing domain' => ['user@'],
|
||||||
|
'empty string' => [''],
|
||||||
|
'spaces' => ['user @example.com'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
215
templates/github-actions-tests.yml
Normal file
215
templates/github-actions-tests.yml
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: PHP Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php: ['8.1', '8.2', '8.3', '8.4']
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP ${{ matrix.php }}
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
|
||||||
|
- name: Get Composer Cache Directory
|
||||||
|
id: composer-cache
|
||||||
|
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: ${{ runner.os }}-composer-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress --prefer-dist
|
||||||
|
|
||||||
|
- name: Run PHP linting
|
||||||
|
run: composer ci:test:php:lint
|
||||||
|
|
||||||
|
phpstan:
|
||||||
|
name: PHPStan (Latest PHP)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress
|
||||||
|
|
||||||
|
- name: Run PHPStan
|
||||||
|
run: composer ci:test:php:phpstan
|
||||||
|
|
||||||
|
security:
|
||||||
|
name: Security Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress
|
||||||
|
|
||||||
|
- name: Run Composer audit
|
||||||
|
run: composer audit
|
||||||
|
|
||||||
|
cgl:
|
||||||
|
name: Code Style
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress
|
||||||
|
|
||||||
|
- name: Check code style
|
||||||
|
run: composer ci:test:php:cgl
|
||||||
|
|
||||||
|
unit:
|
||||||
|
name: Unit Tests (PHP ${{ matrix.php }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php: ['8.1', '8.2', '8.3', '8.4']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP ${{ matrix.php }}
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
coverage: xdebug
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache
|
||||||
|
key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: composer ci:test:php:unit
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: ./var/log/coverage/clover.xml
|
||||||
|
flags: unittests
|
||||||
|
name: unit-tests-php-${{ matrix.php }}
|
||||||
|
|
||||||
|
functional:
|
||||||
|
name: Functional Tests (PHP ${{ matrix.php }}, ${{ matrix.database }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php: ['8.1', '8.2', '8.3', '8.4']
|
||||||
|
database: ['mysqli', 'pdo_mysql', 'postgres', 'sqlite']
|
||||||
|
exclude:
|
||||||
|
# Reduce matrix for faster execution
|
||||||
|
- php: '8.2'
|
||||||
|
database: 'pdo_mysql'
|
||||||
|
- php: '8.3'
|
||||||
|
database: 'postgres'
|
||||||
|
- php: '8.4'
|
||||||
|
database: 'pdo_mysql'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
env:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: typo3_test
|
||||||
|
ports:
|
||||||
|
- 3306:3306
|
||||||
|
options: >-
|
||||||
|
--health-cmd="mysqladmin ping"
|
||||||
|
--health-interval=10s
|
||||||
|
--health-timeout=5s
|
||||||
|
--health-retries=3
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: typo3_test
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP ${{ matrix.php }}
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
extensions: ${{ matrix.database == 'postgres' && 'pdo_pgsql' || 'mysqli' }}
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache
|
||||||
|
key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-progress
|
||||||
|
|
||||||
|
- name: Run functional tests
|
||||||
|
env:
|
||||||
|
typo3DatabaseDriver: ${{ matrix.database }}
|
||||||
|
typo3DatabaseHost: 127.0.0.1
|
||||||
|
typo3DatabaseName: typo3_test
|
||||||
|
typo3DatabaseUsername: ${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
|
||||||
|
typo3DatabasePassword: ${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
|
||||||
|
run: composer ci:test:php:functional
|
||||||
Reference in New Issue
Block a user