A comprehensive Claude Code skill for creating and managing TYPO3 extension tests.
- Test Creation: Generate Unit, Functional, and Acceptance tests
- Infrastructure Setup: Automated testing infrastructure installation
- CI/CD Integration: GitHub Actions and GitLab CI templates
- Quality Tools: PHPStan, Rector, php-cs-fixer integration
- Fixture Management: Database fixture templates and tooling
- Test Orchestration: runTests.sh script pattern from TYPO3 best practices
Install the skill globally in Claude Code:
cd ~/.claude/skills
git clone https://github.com/netresearch/typo3-testing-skill.git typo3-testingOr via Claude Code marketplace:
/plugin marketplace add netresearch/claude-code-marketplace
/plugin install typo3-testing-
Setup testing infrastructure:
cd your-extension ~/.claude/skills/typo3-testing/scripts/setup-testing.sh
-
Generate a test:
~/.claude/skills/typo3-testing/scripts/generate-test.sh unit MyService -
Run tests:
Build/Scripts/runTests.sh -s unit composer ci:test
Fast, isolated tests without external dependencies. Perfect for testing services, utilities, and domain logic.
Tests with database and full TYPO3 instance. Use for repositories, controllers, and integration scenarios.
Browser-based end-to-end tests using Codeception and Selenium. For testing complete user workflows.
The tea extension demonstrates production-grade PHPUnit configuration with parallel execution, strict mode, and comprehensive coverage analysis.
PHPUnit 10+ supports parallel test execution for significant performance improvements:
Configuration (Build/phpunit/UnitTests.xml):
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../.Build/vendor/phpunit/phpunit/phpunit.xsd"
executionOrder="random"
failOnRisky="true"
failOnWarning="true"
stopOnFailure="false"
beStrictAboutTestsThatDoNotTestAnything="true"
colors="true"
cacheDirectory=".Build/.phpunit.cache">
<testsuites>
<testsuite name="Unit Tests">
<directory>../../Tests/Unit/</directory>
</testsuite>
</testsuites>
<coverage includeUncoveredFiles="true">
<report>
<clover outputFile=".Build/coverage/clover.xml"/>
<html outputDirectory=".Build/coverage/html"/>
<text outputFile="php://stdout" showUncoveredFiles="false"/>
</report>
</coverage>
</phpunit>Key Features:
executionOrder="random": Detects hidden test dependencies by randomizing test orderfailOnRisky="true": Treats risky tests as failures (tests without assertions)failOnWarning="true": Fails on warnings like deprecated function usagebeStrictAboutTestsThatDoNotTestAnything="true": Ensures every test has assertions
The tea extension maintains separate PHPUnit configurations:
Unit Tests (Build/phpunit/UnitTests.xml):
- No database bootstrap
- Fast execution (milliseconds per test)
- Strict mode enabled
- Code coverage analysis
Functional Tests (Build/phpunit/FunctionalTests.xml):
- Database bootstrap included
- TYPO3 testing framework integration
- SQLite for fast in-memory testing
- Test doubles for external services
Example functional test configuration:
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../.Build/vendor/phpunit/phpunit/phpunit.xsd"
stopOnFailure="false"
colors="true"
cacheDirectory=".Build/.phpunit.cache">
<testsuites>
<testsuite name="Functional Tests">
<directory>../../Tests/Functional/</directory>
</testsuite>
</testsuites>
<php>
<ini name="display_errors" value="1"/>
<env name="TYPO3_CONTEXT" value="Testing"/>
</php>
</phpunit>Enforce minimum coverage requirements via composer scripts:
{
"scripts": {
"ci:coverage:check": [
"@ci:tests:unit",
"phpunit --configuration Build/phpunit/UnitTests.xml --coverage-text --coverage-clover=.Build/coverage/clover.xml",
"phpunit-coverage-check .Build/coverage/clover.xml 70"
]
}
}Progressive Coverage Targets:
- MVP Extensions: 50% minimum
- Production Extensions: 70% minimum
- Reference Extensions: 80%+ target
The tea extension demonstrates an elegant CSV-based pattern for functional test fixtures, significantly improving test readability and maintainability.
Traditional fixture loading in TYPO3 functional tests uses SQL files or PHP arrays:
// ❌ Traditional approach: Verbose and hard to read
protected function setUp(): void
{
parent::setUp();
$this->importCSVDataSet(__DIR__ . '/Fixtures/Database/pages.csv');
$this->importCSVDataSet(__DIR__ . '/Fixtures/Database/tt_content.csv');
$this->importCSVDataSet(__DIR__ . '/Fixtures/Database/tx_tea_domain_model_product_tea.csv');
}Fixture File (Tests/Functional/Fixtures/Database/tea.csv):
tx_tea_domain_model_product_tea
uid,pid,title,description,owner
1,1,"Earl Grey","Classic black tea",1
2,1,"Green Tea","Organic green tea",1
3,2,"Oolong Tea","Traditional oolong",2
Loading in Test:
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
final class TeaRepositoryTest extends FunctionalTestCase
{
protected array $testExtensionsToLoad = [
'typo3conf/ext/tea',
];
protected function setUp(): void
{
parent::setUp();
$this->importCSVDataSet(__DIR__ . '/Fixtures/Database/tea.csv');
}
/**
* @test
*/
public function findAllReturnsAllRecords(): void
{
$result = $this->subject->findAll();
self::assertCount(3, $result);
}
}Even More Powerful: Assert database state using CSV format:
Expected State File (Tests/Functional/Fixtures/Database/AssertTeaAfterCreate.csv):
tx_tea_domain_model_product_tea
uid,pid,title,description,owner
1,1,"Earl Grey","Classic black tea",1
2,1,"Green Tea","Organic green tea",1
3,2,"Oolong Tea","Traditional oolong",2
4,1,"New Tea","Newly created tea",1
Assertion in Test:
/**
* @test
*/
public function createPersistsNewTea(): void
{
$newTea = new Tea();
$newTea->setTitle('New Tea');
$newTea->setDescription('Newly created tea');
$this->subject->add($newTea);
$this->persistenceManager->persistAll();
// Assert entire database state matches expected CSV
$this->assertCSVDataSet(__DIR__ . '/Fixtures/Database/AssertTeaAfterCreate.csv');
}- Readability: CSV format is human-readable and version control friendly
- Maintainability: Easy to modify fixtures without PHP syntax knowledge
- Comprehensive Assertions: Assert entire table state in single call
- Change Detection: Diff tools show fixture changes clearly
- Cross-Test Reuse: Same CSV fixtures reusable across multiple tests
Minimal Fixtures: Include only necessary columns for the test:
tx_tea_domain_model_product_tea
uid,title
1,"Earl Grey"
2,"Green Tea"
Named Test Data: Use descriptive titles to make test intent clear:
tx_tea_domain_model_product_tea
uid,title,deleted
1,"Active Tea",0
2,"Deleted Tea",1
Fixture Organization:
Tests/Functional/
├── Fixtures/
│ └── Database/
│ ├── tea_initial.csv # Initial state
│ ├── tea_after_create.csv # Expected after creation
│ ├── tea_after_update.csv # Expected after update
│ └── tea_after_delete.csv # Expected after deletion
The tea extension demonstrates comprehensive multi-database testing across SQLite, MariaDB, MySQL, and PostgreSQL, ensuring compatibility across all TYPO3-supported database systems.
Different databases have subtle behavioral differences:
- SQLite: Case-insensitive LIKE, limited ALTER TABLE support
- MySQL: Case sensitivity varies by OS and configuration
- MariaDB: Different optimizer behavior, JSON handling differences
- PostgreSQL: Strict type casting, different string comparison semantics
Extensions using advanced SQL features (e.g., JSON columns, full-text search, stored procedures) must test across all target databases.
The tea extension uses Build/Scripts/runTests.sh for orchestrated multi-database testing:
#!/usr/bin/env bash
# Run functional tests against SQLite (default, fast)
./Build/Scripts/runTests.sh -s functional
# Run functional tests against MariaDB 10.11
./Build/Scripts/runTests.sh -s functional -d mariadb -i 10.11
# Run functional tests against MySQL 8.0
./Build/Scripts/runTests.sh -s functional -d mysql -i 8.0
# Run functional tests against PostgreSQL 16
./Build/Scripts/runTests.sh -s functional -d postgres -i 16Script Responsibilities:
- Docker container orchestration
- Database initialization and schema setup
- Test execution with proper environment variables
- Cleanup and teardown
GitHub Actions (.github/workflows/ci.yml):
name: CI
on: [push, pull_request]
jobs:
functional-tests:
name: Functional Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4']
typo3: ['12.4', '13.0']
database:
- type: 'sqlite'
- type: 'mariadb'
version: '10.11'
- type: 'mysql'
version: '8.0'
- type: 'postgres'
version: '16'
steps:
- uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: pdo_sqlite, pdo_mysql, pdo_pgsql
- name: Composer Install
run: composer install --no-progress
- name: Functional Tests
run: |
if [ "${{ matrix.database.type }}" = "sqlite" ]; then
./Build/Scripts/runTests.sh -s functional
else
./Build/Scripts/runTests.sh -s functional -d ${{ matrix.database.type }} -i ${{ matrix.database.version }}
fiThis matrix runs tests across:
- 3 PHP versions × 2 TYPO3 versions × 4 databases = 24 test combinations
SQLite Advantages:
- Fast (in-memory execution)
- No external dependencies
- Ideal for local development
SQLite Limitations:
// ❌ Won't work on SQLite (lacks ALTER TABLE support)
$connection->executeUpdate('ALTER TABLE tt_content ADD COLUMN new_field VARCHAR(255)');
// ✅ Use TYPO3 API instead (cross-database compatible)
$schemaManager = $connection->getSchemaManager();
$column = new Column('new_field', Type::getType('string'), ['length' => 255]);
$schemaManager->addColumn('tt_content', $column);PostgreSQL Strict Typing:
// ❌ MySQL/MariaDB allow implicit conversion, PostgreSQL doesn't
$queryBuilder->where(
$queryBuilder->expr()->eq('uid', '123') // String '123' vs INT uid
);
// ✅ Explicit type casting works everywhere
$queryBuilder->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(123, \PDO::PARAM_INT))
);Developers can run multi-database tests locally:
# Quick SQLite test during development
composer ci:tests:functional
# Comprehensive multi-DB test before pushing
./Build/Scripts/runTests.sh -s functional -d mariadb
./Build/Scripts/runTests.sh -s functional -d mysql
./Build/Scripts/runTests.sh -s functional -d postgresFor complex scenarios, use docker-compose.yml:
version: '3.8'
services:
mariadb:
image: mariadb:10.11
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
ports:
- "3306:3306"
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: root
POSTGRES_DB: test
ports:
- "5432:5432"
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
ports:
- "3307:3306"The runTests.sh script from the tea extension provides comprehensive test orchestration with Docker-based isolation.
1. Test Suite Selection:
./Build/Scripts/runTests.sh -s unit # Unit tests only
./Build/Scripts/runTests.sh -s functional # Functional tests
./Build/Scripts/runTests.sh -s acceptance # Acceptance tests
./Build/Scripts/runTests.sh -s lint # PHP linting
./Build/Scripts/runTests.sh -s phpstan # Static analysis2. Database Selection:
./Build/Scripts/runTests.sh -s functional -d sqlite # Default
./Build/Scripts/runTests.sh -s functional -d mariadb # MariaDB
./Build/Scripts/runTests.sh -s functional -d mysql # MySQL
./Build/Scripts/runTests.sh -s functional -d postgres # PostgreSQL3. Version Control:
./Build/Scripts/runTests.sh -s functional -d mariadb -i 10.11
./Build/Scripts/runTests.sh -s functional -d postgres -i 16
./Build/Scripts/runTests.sh -p 8.3 # PHP version4. Cleanup and Maintenance:
./Build/Scripts/runTests.sh -s clean # Remove containers
./Build/Scripts/runTests.sh -s composer update # Update dependenciesKey Components:
#!/usr/bin/env bash
# Parse command line arguments
while getopts "s:d:i:p:h" option; do
case ${option} in
s) TEST_SUITE=${OPTARG} ;;
d) DATABASE=${OPTARG} ;;
i) DATABASE_VERSION=${OPTARG} ;;
p) PHP_VERSION=${OPTARG} ;;
h) showHelp; exit 0 ;;
esac
done
# Set defaults
DATABASE=${DATABASE:-sqlite}
PHP_VERSION=${PHP_VERSION:-8.2}
# Container configuration
CONTAINER_NAME="typo3-testing-${DATABASE}"
DOCKER_IMAGE="typo3/core-testing-${DATABASE}:${DATABASE_VERSION}"
# Execute test suite in container
docker run \
--name ${CONTAINER_NAME} \
--rm \
-v $(pwd):/app \
-w /app \
${DOCKER_IMAGE} \
/bin/bash -c "composer ci:tests:${TEST_SUITE}"- Isolation: Each test run in clean container environment
- Reproducibility: Same environment locally and in CI
- Version Flexibility: Test against multiple PHP/TYPO3/DB versions
- Developer Convenience: Single command for all test types
- CI Integration: Same script used locally and in CI
Composer scripts delegate to runTests.sh:
{
"scripts": {
"ci:tests:unit": "Build/Scripts/runTests.sh -s unit",
"ci:tests:functional": "Build/Scripts/runTests.sh -s functional",
"ci:tests:functional:mariadb": "Build/Scripts/runTests.sh -s functional -d mariadb",
"ci:tests:functional:postgres": "Build/Scripts/runTests.sh -s functional -d postgres",
"ci:tests": [
"@ci:tests:unit",
"@ci:tests:functional"
]
}
}This maintains the local-CI parity principle: developers and CI use identical commands.
Development Workflow:
# Quick unit test during coding
composer ci:tests:unit
# Functional test before commit
composer ci:tests:functional
# Full test suite before push
composer ci:testsPre-Release Workflow:
# Test against all databases
./Build/Scripts/runTests.sh -s functional -d sqlite
./Build/Scripts/runTests.sh -s functional -d mariadb -i 10.11
./Build/Scripts/runTests.sh -s functional -d mysql -i 8.0
./Build/Scripts/runTests.sh -s functional -d postgres -i 16
# Test against multiple PHP versions
./Build/Scripts/runTests.sh -s unit -p 8.2
./Build/Scripts/runTests.sh -s unit -p 8.3
./Build/Scripts/runTests.sh -s unit -p 8.4CI/CD Workflow:
# .github/workflows/ci.yml
- name: Unit Tests
run: composer ci:tests:unit
- name: Functional Tests (SQLite)
run: composer ci:tests:functional
- name: Functional Tests (MariaDB)
run: composer ci:tests:functional:mariadb
- name: Functional Tests (PostgreSQL)
run: composer ci:tests:functional:postgres- SKILL.md - Main workflow guide with decision trees
- references/ - Detailed testing documentation
- templates/ - PHPUnit configs, AGENTS.md, examples
- PHP 8.1+
- Composer
- Docker (for functional and acceptance tests)
- TYPO3 v12 or v13
- TYPO3 Testing Framework
- TYPO3 Best Practices: tea extension
- TYPO3 community best practices
GPL-2.0-or-later
Netresearch DTT GmbH, Leipzig