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