typo3-testing

TYPO3 extension testing (unit, functional, E2E, architecture, mutation). Use when setting up test infrastructure, writing tests, or configuring CI/CD.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "typo3-testing" with this command: npx skills add dirnbauer/webconsulting-skills/dirnbauer-webconsulting-skills-typo3-testing

TYPO3 Testing Skill

Comprehensive testing infrastructure for TYPO3 extensions: unit, functional, E2E, architecture, and mutation testing.

TYPO3 API First: Always use TYPO3's built-in APIs, core features, and established conventions before creating custom implementations. Do not reinvent what TYPO3 already provides. Always verify that the APIs and methods you use exist and are not deprecated in your target TYPO3 version (v13 or v14) by checking the official TYPO3 documentation.

Test Type Selection

TypeUse WhenSpeedFramework
UnitPure logic, validators, utilitiesFast (ms)PHPUnit
FunctionalDB interactions, repositoriesMedium (s)PHPUnit + TYPO3
ArchitectureLayer constraints, dependenciesFast (ms)PHPat
E2EUser workflows, browserSlow (s-min)Playwright
MutationTest quality verificationCI onlyInfection

Test Infrastructure Setup

Directory Structure

Tests/
├── Functional/
│   ├── Controller/
│   ├── Repository/
│   └── Fixtures/
├── Unit/
│   ├── Service/
│   └── Validator/
├── Architecture/
│   └── ArchitectureTest.php
└── E2E/
    └── playwright/

PHPUnit Configuration

<?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/typo3/testing-framework/Resources/Core/Build/UnitTestsBootstrap.php"
         colors="true"
         cacheResult="false">
    <testsuites>
        <testsuite name="Unit">
            <directory>Tests/Unit</directory>
        </testsuite>
        <testsuite name="Functional">
            <directory>Tests/Functional</directory>
        </testsuite>
        <testsuite name="Architecture">
            <directory>Tests/Architecture</directory>
        </testsuite>
    </testsuites>
    
    <coverage>
        <report>
            <clover outputFile="var/log/coverage.xml"/>
            <html outputDirectory="var/log/coverage"/>
        </report>
    </coverage>
    
    <source>
        <include>
            <directory>Classes</directory>
        </include>
        <exclude>
            <directory>Classes/Domain/Model</directory>
        </exclude>
    </source>
</phpunit>

Functional Test Configuration

<!-- FunctionalTests.xml -->
<?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/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
         colors="true">
    <testsuites>
        <testsuite name="Functional">
            <directory>Tests/Functional</directory>
        </testsuite>
    </testsuites>
</phpunit>

Unit Testing

Basic Unit Test

<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Unit\Service;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Vendor\MyExtension\Service\PriceCalculator;

final class PriceCalculatorTest extends TestCase
{
    private PriceCalculator $subject;

    protected function setUp(): void
    {
        parent::setUp();
        $this->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

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Unit\Service;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Vendor\MyExtension\Service\ItemService;
use Vendor\MyExtension\Domain\Repository\ItemRepository;

final class ItemServiceTest extends TestCase
{
    private ItemRepository&MockObject $itemRepositoryMock;
    private LoggerInterface&MockObject $loggerMock;
    private ItemService $subject;

    protected function setUp(): void
    {
        parent::setUp();
        
        $this->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

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Functional\Repository;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\MyExtension\Domain\Repository\ItemRepository;

final class ItemRepositoryTest extends FunctionalTestCase
{
    protected array $testExtensionsToLoad = [
        'typo3conf/ext/my_extension',
    ];

    private ItemRepository $subject;

    protected function setUp(): void
    {
        parent::setUp();
        
        $this->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

# 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

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Functional\Controller;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\MyExtension\Controller\ItemController;

final class ItemControllerTest extends FunctionalTestCase
{
    protected array $testExtensionsToLoad = [
        'typo3conf/ext/my_extension',
    ];

    #[Test]
    public function listActionReturnsHtmlResponse(): void
    {
        $this->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

composer require --dev phpat/phpat

Architecture Test

<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Architecture;

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class ArchitectureTest
{
    public function testDomainModelsShouldNotDependOnInfrastructure(): Rule
    {
        return PHPat::rule()
            ->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

# phpstan.neon
includes:
    - vendor/phpat/phpat/extension.neon

parameters:
    level: 9
    paths:
        - Classes
        - Tests

E2E Testing with Playwright

Setup

# Install Playwright
npm init playwright@latest

# Configure for TYPO3
mkdir -p Tests/E2E/playwright

Playwright Configuration

// 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

// 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

composer require --dev infection/infection

Configuration

// 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

vendor/bin/infection --threads=4

Test Commands

# 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

# .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

CriterionRequirement
Unit testsRequired, 70%+ coverage
Functional testsRequired for DB operations
Architecture testsPHPat required for full conformance
PHPStanLevel 9+ (level 10 recommended)
E2E testsOptional, bonus points
Mutation70%+ MSI for bonus points

v14-Only Testing Changes

The following testing-related changes apply when testing against TYPO3 v14.

Testing Framework Version [v14 only]

TYPO3 v14 requires typo3/testing-framework:^9.0. The v13 version (^8.2) is not compatible:

{
    "require-dev": {
        "typo3/testing-framework": "^8.2 || ^9.0"
    }
}

Fluid 5.0 in Functional Tests [v14 only]

Functional tests rendering Fluid templates must account for Fluid 5.0 strict typing. ViewHelper arguments must match expected types exactly or tests will fail with type errors.

TCA Read-Only in Tests [v14 only]

$GLOBALS['TCA'] is read-only after boot in v14. Test fixtures that modify TCA at runtime must use TcaSchemaFactory or configure TCA in Configuration/TCA/ fixtures instead.

Deprecated Method Removal [v14 only]

Test code using deprecated TYPO3 APIs (e.g., $GLOBALS['TSFE'], Extbase annotations, MailMessage->send()) will fail in v14. Run tests against both v13 and v14 in CI to catch these early.


Credits & Attribution

Thanks to Netresearch DTT GmbH for their contributions to the TYPO3 community.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

ai-search-optimization

No summary provided by upstream source.

Repository SourceNeeds Review
General

document-processing

No summary provided by upstream source.

Repository SourceNeeds Review
General

typo3-content-blocks

No summary provided by upstream source.

Repository SourceNeeds Review
General

php-modernization

No summary provided by upstream source.

Repository SourceNeeds Review