721 lines
18 KiB
Markdown
721 lines
18 KiB
Markdown
# 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)
|