# TYPO3 Testing Standards **Source:** TYPO3 Core API Reference - Testing **Purpose:** Unit, functional, and acceptance testing standards for TYPO3 extensions ## Testing Framework TYPO3 uses **typo3/testing-framework** for comprehensive testing: ```bash # Install testing framework composer require --dev \ "typo3/testing-framework":"^8.0.9" \ "phpunit/phpunit":"^10.5" ``` ## Unit Testing ### Unit Test Structure ```php // ✅ Right: Proper unit test structure subject = new CalculationService(); } /** * @test */ public function addReturnsCorrectSum(): void { $result = $this->subject->add(2, 3); $this->assertEquals(5, $result); } /** * @test */ public function multiplyReturnsCorrectProduct(): void { $result = $this->subject->multiply(4, 5); $this->assertEquals(20, $result); } } ``` ### PHPUnit Configuration ```xml ../../Tests/Unit/ ``` ### Running Unit Tests ```bash # Direct execution vendor/bin/phpunit -c Build/phpunit/UnitTests.xml # DDEV execution ddev exec php vendor/bin/phpunit -c Build/phpunit/UnitTests.xml # Run specific test vendor/bin/phpunit -c Build/phpunit/UnitTests.xml --filter "CalculationServiceTest" ``` ### Unit Test Best Practices **✅ Do:** - Test single units (methods, functions) in isolation - Mock external dependencies - Test edge cases and boundary conditions - Use descriptive test method names - Follow naming: `methodNameReturns` - Keep tests fast (no database, no external services) **❌ Don't:** - Access database in unit tests - Depend on file system - Make HTTP requests - Test framework internals - Write integration tests as unit tests ## Functional Testing ### Functional Test Structure ```php // ✅ Right: Proper functional test structure importCSVDataSet(__DIR__ . '/Fixtures/products.csv'); // Set up backend user $this->setUpBackendUser(1); // Initialize subject $this->subject = $this->get(ProductRepository::class); } /** * @test */ public function findAllReturnsAllProducts(): void { $products = $this->subject->findAll(); $this->assertCount(3, $products); } /** * @test */ public function findByPriceRangeReturnsMatchingProducts(): void { $products = $this->subject->findByPriceRange(10.0, 50.0); $this->assertCount(2, $products); } } ``` ### PHPUnit Functional Configuration ```xml ../../Tests/Functional/ ``` ### Running Functional Tests ```bash # With MySQL/MariaDB ddev exec \ typo3DatabaseDriver='mysqli' \ typo3DatabaseHost='db' \ typo3DatabasePort=3306 \ typo3DatabaseUsername='root' \ typo3DatabasePassword='root' \ typo3DatabaseName='func' \ php vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml # With SQLite (simpler) ddev exec \ typo3DatabaseDriver=pdo_sqlite \ php vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml # With PostgreSQL ddev exec \ typo3DatabaseDriver='pdo_pgsql' \ typo3DatabaseHost='postgres' \ typo3DatabasePort=5432 \ typo3DatabaseUsername='postgres' \ typo3DatabasePassword='postgres' \ typo3DatabaseName='func' \ php vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml ``` ### Test Data Fixtures ```csv # Tests/Functional/Fixtures/products.csv tx_myext_product,uid,pid,title,price,available ,1,0,Product A,29.99,1 ,2,0,Product B,49.99,1 ,3,0,Product C,99.99,0 ``` ### Loading Extensions in Tests ```php // Load extension under test protected array $testExtensionsToLoad = [ 'typo3conf/ext/my_extension', ]; // Load additional core extensions protected array $coreExtensionsToLoad = [ 'typo3/cms-workspaces', ]; // Load fixture extensions protected array $testExtensionsToLoad = [ 'typo3conf/ext/my_extension', 'typo3conf/ext/my_extension/Tests/Functional/Fixtures/Extensions/fixture_extension', ]; ``` ## Acceptance Testing ### Codeception Setup ```yaml # Tests/codeception.yml namespace: Vendor\ExtensionKey\Tests\Acceptance\Support suites: acceptance: actor: AcceptanceTester path: . modules: enabled: - Asserts - WebDriver: url: https://myproject.ddev.site browser: chrome host: ddev-myproject-chrome wait: 1 window_size: 1280x1024 extensions: enabled: - Codeception\Extension\RunFailed - Codeception\Extension\Recorder paths: tests: Acceptance output: ../var/log/_output data: . support: Acceptance/Support settings: shuffle: false lint: true colors: true ``` ### Acceptance Test Structure ```php // ✅ Right: Backend acceptance test useExistingSession('admin'); } /** * @param BackendTester $I */ public function moduleCanBeAccessed(BackendTester $I): void { $I->click(Topbar::$dropdownToggleSelector, '#typo3-cms-backend-backend-toolbaritems-helptoolbaritem'); $I->canSee('My Module'); $I->click('My Module'); $I->switchToContentFrame(); $I->see('Module Content', 'h1'); } /** * @param BackendTester $I */ public function formSubmissionWorks(BackendTester $I): void { $I->amOnPage('/typo3/module/my-module'); $I->switchToContentFrame(); $I->fillField('title', 'Test Title'); $I->click('Save'); $I->see('Record saved successfully'); } } ``` ### Frontend Acceptance Test ```php // ✅ Right: Frontend acceptance test amOnPage('/'); $I->see('Welcome to TYPO3'); $I->seeElement('h1'); } /** * @param AcceptanceTester $I */ public function navigationWorks(AcceptanceTester $I): void { $I->amOnPage('/'); $I->click('Products'); $I->see('Our Products', 'h1'); $I->seeInCurrentUrl('/products'); } } ``` ### Running Acceptance Tests ```bash # Run acceptance tests via DDEV ddev exec bin/codecept run acceptance -d -c Tests/codeception.yml # Run specific test ddev exec bin/codecept run acceptance ModuleCest -c Tests/codeception.yml # Generate new test ddev exec bin/codecept generate:cest acceptance MyNewTest -c Tests/codeception.yml ``` ## Test Organization ### Directory Structure ``` Tests/ ├── Unit/ │ ├── Controller/ │ │ └── ProductControllerTest.php │ ├── Domain/ │ │ ├── Model/ │ │ │ └── ProductTest.php │ │ └── Repository/ │ │ └── ProductRepositoryTest.php │ └── Service/ │ └── CalculationServiceTest.php ├── Functional/ │ ├── Domain/ │ │ └── Repository/ │ │ ├── ProductRepositoryTest.php │ │ └── Fixtures/ │ │ └── products.csv │ └── Controller/ │ └── ProductControllerTest.php └── Acceptance/ ├── Backend/ │ └── ModuleCest.php ├── Frontend/ │ └── FrontendPagesCest.php └── Support/ ├── AcceptanceTester.php └── BackendTester.php ``` ### Naming Conventions **Unit Tests:** - Pattern: `Test.php` - Example: `ProductRepository.php` → `ProductRepositoryTest.php` - Location: Mirror `Classes/` structure in `Tests/Unit/` **Functional Tests:** - Pattern: `Test.php` - Example: `ProductRepository.php` → `ProductRepositoryTest.php` - Location: Mirror `Classes/` structure in `Tests/Functional/` **Acceptance Tests:** - Pattern: `Cest.php` - Example: `ModuleCest.php`, `LoginCest.php` - Location: `Tests/Acceptance/Backend/` or `Tests/Acceptance/Frontend/` ## PHPUnit Attributes (PHP 8.0+) ```php // ✅ Right: Using PHPUnit attributes assertEquals(5, $this->subject->add(2, 3)); } public static function priceDataProvider(): \Generator { yield 'standard price' => [ 'price' => 100.0, 'taxRate' => 0.19, 'expected' => 119.0, ]; yield 'zero price' => [ 'price' => 0.0, 'taxRate' => 0.19, 'expected' => 0.0, ]; } #[Test] #[DataProvider('priceDataProvider')] public function calculatePriceWithTax(float $price, float $taxRate, float $expected): void { $result = $this->subject->calculatePriceWithTax($price, $taxRate); $this->assertEquals($expected, $result); } } ``` ## CI/CD Integration ### GitHub Actions ```yaml # .github/workflows/tests.yml name: Tests on: [push, pull_request] jobs: tests: runs-on: ubuntu-latest strategy: matrix: php: ['8.1', '8.2', '8.3'] typo3: ['12.4', '13.0'] steps: - name: Checkout uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - name: Install dependencies run: composer install - name: Lint PHP run: find . -name \*.php ! -path "./vendor/*" -exec php -l {} \; - name: Unit Tests run: vendor/bin/phpunit -c Build/phpunit/UnitTests.xml - name: Functional Tests run: | typo3DatabaseDriver=pdo_sqlite \ vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml ``` ## Conformance Checklist ### Unit Tests - [ ] Unit tests extend `UnitTestCase` - [ ] Tests located in `Tests/Unit/` mirroring `Classes/` - [ ] Test files named `Test.php` - [ ] No database access in unit tests - [ ] No file system access in unit tests - [ ] All public methods tested - [ ] Edge cases and boundaries tested - [ ] #[Test] attribute or @test annotation used ### Functional Tests - [ ] Functional tests extend `FunctionalTestCase` - [ ] Tests located in `Tests/Functional/` - [ ] `setUp()` calls `parent::setUp()` first - [ ] Extensions loaded via `$testExtensionsToLoad` - [ ] Test data loaded via `importCSVDataSet()` - [ ] Database operations tested - [ ] Backend user initialized when needed ### Acceptance Tests - [ ] Acceptance tests use Codeception - [ ] Tests located in `Tests/Acceptance/` - [ ] Test files named `Cest.php` - [ ] codeception.yml properly configured - [ ] Backend tests use `useExistingSession('admin')` - [ ] Frame switching used correctly - [ ] Tests verify user-visible behavior ### General - [ ] PHPUnit configuration files present - [ ] All tests pass locally - [ ] CI/CD pipeline configured - [ ] Test coverage >70% for new code - [ ] Data providers use named arguments - [ ] Descriptive test method names