--- name: typo3-testing description: TYPO3 extension testing (unit, functional, E2E, architecture, mutation). Use when setting up test infrastructure, writing tests, or configuring CI/CD. version: 1.0.0 typo3_compatibility: "13.0 - 14.x" triggers: - testing - phpunit - playwright - phpat - unit test - functional test - e2e - coverage --- # TYPO3 Testing Skill Comprehensive testing infrastructure for TYPO3 extensions: unit, functional, E2E, architecture, and mutation testing. ## Test Type Selection | Type | Use When | Speed | Framework | |------|----------|-------|-----------| | **Unit** | Pure logic, validators, utilities | Fast (ms) | PHPUnit | | **Functional** | DB interactions, repositories | Medium (s) | PHPUnit + TYPO3 | | **Architecture** | Layer constraints, dependencies | Fast (ms) | PHPat | | **E2E** | User workflows, browser | Slow (s-min) | Playwright | | **Mutation** | Test quality verification | CI only | Infection | ## Test Infrastructure Setup ### Directory Structure ``` Tests/ ├── Functional/ │ ├── Controller/ │ ├── Repository/ │ └── Fixtures/ ├── Unit/ │ ├── Service/ │ └── Validator/ ├── Architecture/ │ └── ArchitectureTest.php └── E2E/ └── playwright/ ``` ### PHPUnit Configuration ```xml Tests/Unit Tests/Functional Tests/Architecture Classes Classes/Domain/Model ``` ### Functional Test Configuration ```xml Tests/Functional ``` ## Unit Testing ### Basic Unit Test ```php subject = new PriceCalculator(); } #[Test] public function calculateNetPriceReturnsCorrectValue(): void { $grossPrice = 119.00; $taxRate = 19.0; $netPrice = $this->subject->calculateNetPrice($grossPrice, $taxRate); self::assertEqualsWithDelta(100.00, $netPrice, 0.01); } #[Test] public function calculateNetPriceThrowsExceptionForNegativePrice(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(1234567890); $this->subject->calculateNetPrice(-10.00, 19.0); } } ``` ### Mocking Dependencies ```php itemRepositoryMock = $this->createMock(ItemRepository::class); $this->loggerMock = $this->createMock(LoggerInterface::class); $this->subject = new ItemService( $this->itemRepositoryMock, $this->loggerMock, ); } #[Test] public function findActiveItemsReturnsFilteredItems(): void { $items = [/* mock items */]; $this->itemRepositoryMock ->expects(self::once()) ->method('findByActive') ->with(true) ->willReturn($items); $result = $this->subject->findActiveItems(); self::assertSame($items, $result); } } ``` ## Functional Testing ### Repository Test ```php importCSVDataSet(__DIR__ . '/Fixtures/Items.csv'); $this->subject = $this->get(ItemRepository::class); } #[Test] public function findByUidReturnsCorrectItem(): void { $item = $this->subject->findByUid(1); self::assertNotNull($item); self::assertSame('Test Item', $item->getTitle()); } #[Test] public function findAllReturnsAllItems(): void { $items = $this->subject->findAll(); self::assertCount(3, $items); } } ``` ### CSV Fixture Format ```csv # Tests/Functional/Repository/Fixtures/Items.csv "tx_myext_items" ,"uid","pid","title","active","deleted" ,1,1,"Test Item",1,0 ,2,1,"Another Item",1,0 ,3,1,"Inactive Item",0,0 ``` ### Controller Functional Test ```php importCSVDataSet(__DIR__ . '/Fixtures/Pages.csv'); $this->importCSVDataSet(__DIR__ . '/Fixtures/Items.csv'); $request = new ServerRequest('https://example.com/items', 'GET'); $controller = $this->get(ItemController::class); $response = $controller->listAction(); self::assertSame(200, $response->getStatusCode()); self::assertStringContainsString('text/html', $response->getHeaderLine('Content-Type')); } } ``` ## Architecture Testing with PHPat ### Installation ```bash composer require --dev phpat/phpat ``` ### Architecture Test ```php classes(Selector::inNamespace('Vendor\MyExtension\Domain\Model')) ->shouldNotDependOn() ->classes( Selector::inNamespace('Vendor\MyExtension\Controller'), Selector::inNamespace('Vendor\MyExtension\Infrastructure'), ); } public function testServicesShouldNotDependOnControllers(): Rule { return PHPat::rule() ->classes(Selector::inNamespace('Vendor\MyExtension\Service')) ->shouldNotDependOn() ->classes(Selector::inNamespace('Vendor\MyExtension\Controller')); } public function testRepositoriesShouldImplementInterface(): Rule { return PHPat::rule() ->classes(Selector::classname('/.*Repository$/', true)) ->excluding(Selector::classname('/.*Interface$/', true)) ->shouldImplement() ->classes(Selector::classname('/.*RepositoryInterface$/', true)); } public function testOnlyServicesCanAccessRepositories(): Rule { return PHPat::rule() ->classes(Selector::inNamespace('Vendor\MyExtension\Domain\Repository')) ->canOnlyBeAccessedBy() ->classes( Selector::inNamespace('Vendor\MyExtension\Service'), Selector::inNamespace('Vendor\MyExtension\Tests'), ); } } ``` ### PHPat Configuration ```neon # phpstan.neon includes: - vendor/phpat/phpat/extension.neon parameters: level: 9 paths: - Classes - Tests ``` ## E2E Testing with Playwright ### Setup ```bash # Install Playwright npm init playwright@latest # Configure for TYPO3 mkdir -p Tests/E2E/playwright ``` ### Playwright Configuration ```typescript // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './Tests/E2E', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: process.env.BASE_URL || 'https://my-extension.ddev.site', trace: 'on-first-retry', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], }); ``` ### E2E Test Example ```typescript // Tests/E2E/item-list.spec.ts import { test, expect } from '@playwright/test'; test.describe('Item List', () => { test('displays items correctly', async ({ page }) => { await page.goto('/items'); await expect(page.locator('h1')).toContainText('Items'); await expect(page.locator('.item-card')).toHaveCount(3); }); test('filters items by category', async ({ page }) => { await page.goto('/items'); await page.selectOption('[data-testid="category-filter"]', 'electronics'); await expect(page.locator('.item-card')).toHaveCount(1); }); test('creates new item', async ({ page }) => { await page.goto('/items/new'); await page.fill('[name="title"]', 'New Test Item'); await page.fill('[name="description"]', 'Test description'); await page.click('[type="submit"]'); await expect(page).toHaveURL(/\/items\/\d+/); await expect(page.locator('h1')).toContainText('New Test Item'); }); }); ``` ## Mutation Testing with Infection ### Installation ```bash composer require --dev infection/infection ``` ### Configuration ```json // infection.json5 { "$schema": "vendor/infection/infection/resources/schema.json", "source": { "directories": ["Classes"], "excludes": ["Domain/Model"] }, "logs": { "text": "var/log/infection.log", "html": "var/log/infection.html" }, "mutators": { "@default": true }, "minMsi": 70, "minCoveredMsi": 80 } ``` ### Run Mutation Tests ```bash vendor/bin/infection --threads=4 ``` ## Test Commands ```bash # Unit tests vendor/bin/phpunit -c Tests/UnitTests.xml # Functional tests vendor/bin/phpunit -c Tests/FunctionalTests.xml # Architecture tests vendor/bin/phpstan analyse # All tests with coverage vendor/bin/phpunit --coverage-html var/log/coverage # E2E tests npx playwright test # Mutation tests vendor/bin/infection ``` ## CI/CD Configuration ```yaml # .github/workflows/tests.yml name: Tests on: [push, pull_request] jobs: unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' coverage: xdebug - run: composer install - run: vendor/bin/phpunit -c Tests/UnitTests.xml --coverage-clover coverage.xml functional: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: test ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' - run: composer install - run: vendor/bin/phpunit -c Tests/FunctionalTests.xml architecture: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' - run: composer install - run: vendor/bin/phpstan analyse ``` ## Scoring Requirements | Criterion | Requirement | |-----------|-------------| | Unit tests | Required, 70%+ coverage | | Functional tests | Required for DB operations | | Architecture tests | **PHPat required** for full conformance | | PHPStan | Level 9+ (level 10 recommended) | | E2E tests | Optional, bonus points | | Mutation | 70%+ MSI for bonus points | --- ## Credits & Attribution This skill is based on the excellent work by **[Netresearch DTT GmbH](https://www.netresearch.de/)**. Original repository: https://github.com/netresearch/typo3-testing-skill **Copyright (c) Netresearch DTT GmbH** - Methodology and best practices Adapted by webconsulting.at for this skill collection