infection/abstract-testframework-adapter
Interfaces and base classes for building Infection test framework adapters. Provides a common abstraction layer to integrate different PHP test runners with Infection’s mutation testing, making adapters consistent, reusable, and easier to implement.
## Getting Started
For Laravel developers integrating Infection for mutation testing, start by installing the package via Composer (though it’s primarily a dependency for adapters):
```bash
composer require infection/abstract-testframework-adapter
First use case: If you’re building a custom Infection adapter for a test framework (e.g., Pest, Laravel’s built-in tests), extend AbstractTestFrameworkAdapter and implement the required methods. Focus on these core steps:
use Infection\AbstractTestFrameworkAdapter\AbstractTestFrameworkAdapter;
class PestAdapter extends AbstractTestFrameworkAdapter
{
// Implement required methods
}
infection.json:
{
"test_framework": "PestAdapter",
"test_framework_adapter": "path/to/PestAdapter"
}
HasSyntaxErrorDetection (if needed) to handle syntax errors in test output:
use Infection\AbstractTestFrameworkAdapter\HasSyntaxErrorDetection;
class PestAdapter extends AbstractTestFrameworkAdapter implements HasSyntaxErrorDetection
{
public function hasSyntaxError(string $output): bool
{
return str_contains($output, 'ParseError') || str_contains($output, 'syntax error');
}
}
vendor/bin/infection --test-framework=PestAdapter
Where to look first:
src/TestFrameworkAdapterInterface.php for required methods.src/AbstractTestFrameworkAdapter.php for base implementations.src/HasSyntaxErrorDetection.php for syntax error handling (new in 0.5.0).Extend AbstractTestFrameworkAdapter:
Override methods like runTests(), getName(), and getVersion() to tailor behavior for your framework. Use the base class’s utilities (e.g., createProcess(), mapExitCode()) to avoid reinventing boilerplate.
Handle test execution:
Implement runTests() to return an iterable of TestResult objects or a TestResult array. Example for Pest:
public function runTests(array $files): iterable
{
$process = $this->createProcess(['pest', '--testsuite', implode(' ', $files)]);
$process->run();
yield from $this->parseOutput($process->getOutput());
}
Parse output:
Use TestResult objects to standardize results. For syntax errors, implement hasSyntaxError() to filter malformed test files early:
public function hasSyntaxError(string $output): bool
{
return preg_match('/(ParseError|syntax error)/', $output) === 1;
}
Version compatibility:
Use getVersion() to check framework support and branch logic (e.g., PHPUnit 9 vs. 10 output formats). Example:
public function getVersion(): string
{
return '2.0'; // Pest version
}
public function register()
{
$this->app->singleton(PestAdapter::class, function ($app) {
return new PestAdapter($app['path.tests']);
});
}
use Illuminate\Support\Facades\Cache;
public function runTests(array $files): iterable
{
$cacheKey = 'infection:pest:results:' . md5(implode(',', $files));
if (Cache::has($cacheKey)) {
yield from Cache::get($cacheKey);
return;
}
// ... execute tests ...
Cache::put($cacheKey, $results, now()->addHours(1));
yield from $results;
}
use Psr\Log\LoggerInterface;
public function __construct(private LoggerInterface $logger)
{
}
protected function parseOutput(string $output): array
{
$this->logger->debug('Raw test output:', ['output' => $output]);
// ... parse logic ...
}
Process facade or Mockery to test adapter logic without running real tests:
$mockProcess = Mockery::mock(Process::class);
$mockProcess->shouldReceive('run')->andReturn(true);
$mockProcess->shouldReceive('getOutput')->andReturn('Test output');
$adapter = new PestAdapter($mockProcess);
Exit code mismatches:
1 for both syntax errors and test failures). Override mapExitCode() to normalize:
protected function mapExitCode(int $exitCode): int
{
return $exitCode === 1 && $this->hasSyntaxError($this->output)
? TestFrameworkAdapterInterface::EXIT_CODE_SYNTAX_ERROR
: parent::mapExitCode($exitCode);
}
php -r 'exit(1);' to verify your mapping handles edge cases.Syntax error detection false positives:
hasSyntaxError() method may flag legitimate test failures (e.g., FatalErrorException in Pest). Refine the regex or add framework-specific checks:
public function hasSyntaxError(string $output): bool
{
return str_contains($output, 'ParseError') &&
!str_contains($output, 'FatalErrorException');
}
Parallelization conflicts:
public function runTests(array $files): iterable
{
if ($this->isParallelMode()) {
throw new \RuntimeException('Parallel mode not supported for ' . $this->getName());
}
// ... sequential logic ...
}
Version skew:
getVersion() may not match the installed framework version. Use composer.json or runtime checks:
public function getVersion(): string
{
return file_exists($this->getComposerPath())
? json_decode(file_get_contents($this->getComposerPath()), true)['version']
: 'unknown';
}
Temporary file cleanup:
getTemporaryDirectory() or implement onTestEnd():
protected function onTestEnd(): void
{
foreach (glob($this->getTemporaryDirectory() . '/*') as $file) {
if (is_file($file)) unlink($file);
}
}
INFECTION_VERBOSE env var to log adapter interactions:
INFECTION_VERBOSE=1 vendor/bin/infection
TestResult objects:
Dump results to debug parsing:
foreach ($this->runTests([$file]) as $result) {
dump($result->getStatus(), $result->getOutput());
}
vendor/bin/infection --test-framework=PestAdapter --only-tests=tests/Feature/ExampleTest.php
Custom argument handling:
Override getAdditionalArguments() to inject framework-specific flags:
protected function getAdditionalArguments(array $files): array
{
return ['--coverage-clover=' . storage_path('logs/clover.xml')];
}
Process customization:
Extend createProcess() to modify the underlying Symfony/Process instance:
protected function createProcess(array $command): Process
{
$process = parent::createProcess($command);
$process->setTimeout(300); // 5-minute timeout
return $process;
}
Syntax error granularity:
Enhance hasSyntaxError() to return line numbers or file paths:
public function getSyntaxErrors(string $output): array
How can I help you explore Laravel packages today?