Unit testing 

Running tests 

Prerequisites 

Install development dependencies
# Install dependencies (dev deps included by default)
composer install
Copied!

Unit tests 

Run unit tests
# Recommended: Use runTests.sh (Docker-based, consistent environment)
Build/Scripts/runTests.sh -s unit

# With specific PHP version
Build/Scripts/runTests.sh -s unit -p 8.3

# Alternative: Via Composer script
composer ci:test:php:unit
Copied!

Integration tests 

Run integration tests
# Integration tests use mocked HTTP and need no provider credentials
Build/Scripts/runTests.sh -s integration
Copied!

All tests 

Run the PHP test suites and quality checks
# Run the PHP test suites and architecture rules
Build/Scripts/runTests.sh -s unit
Build/Scripts/runTests.sh -s integration
Build/Scripts/runTests.sh -s fuzzy
Build/Scripts/runTests.sh -s architecture
Build/Scripts/runTests.sh -s functional -d sqlite

# Run code quality checks
Build/Scripts/runTests.sh -s cgl -n
Build/Scripts/runTests.sh -s phpstan
Build/Scripts/runTests.sh -s rector -n -p 8.2
Copied!

The functional suite also discovers backend workflow and TCA suites from Build/FunctionalTests.xml. Some functional provider smoke tests make network requests; inspect their prerequisites before running the whole suite. Browser workflows require a running TYPO3 test instance; see E2E testing. The Python sidecars have separate unittest suites under Build/decision and Build/reranker.

Test structure 

Test directory structure
Tests/
├── Unit/
│   ├── Domain/
│   │   └── Model/
│   │       ├── CompletionResponseTest.php
│   │       ├── EmbeddingResponseTest.php
│   │       └── UsageStatisticsTest.php
│   ├── Provider/
│   │   ├── OpenAiProviderTest.php
│   │   ├── ClaudeProviderTest.php
│   │   ├── GeminiProviderTest.php
│   │   └── AbstractProviderTest.php
│   └── Service/
│       ├── LlmServiceManagerTest.php
│       └── Feature/
│           ├── CompletionServiceTest.php
│           ├── EmbeddingServiceTest.php
│           ├── VisionServiceTest.php
│           └── TranslationServiceTest.php
├── Integration/
│   ├── Provider/
│   │   └── ProviderIntegrationTest.php
│   └── Service/
│       └── ServiceIntegrationTest.php
├── Functional/
│   ├── Controller/
│   │   └── BackendControllerTest.php
│   └── Repository/
│       └── ProviderRepositoryTest.php
└── E2E/
    └── WorkflowTest.php
Copied!

Writing tests 

Unit test example 

Example: Unit test
namespace Your\Extension\Tests\Unit;

use Netresearch\NrLlm\Domain\Model\CompletionResponse;
use Netresearch\NrLlm\Domain\Model\UsageStatistics;
use Netresearch\NrLlm\Service\Feature\CompletionServiceInterface;
use Netresearch\NrLlm\Testing\FakeCompletionService;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class SummaryService
{
    public function __construct(
        private readonly CompletionServiceInterface $completion,
    ) {}

    public function summarize(string $text): string
    {
        return $this->completion->complete('Summarize: ' . $text)->content;
    }
}

final class SummaryServiceTest extends TestCase
{
    #[Test]
    public function returnsTheSummaryAndSendsTheInput(): void
    {
        $completion = new FakeCompletionService();
        $completion->responses[] = new CompletionResponse(
            content: 'A short summary.',
            model: 'test-model',
            usage: new UsageStatistics(10, 5, 15),
        );
        $subject = new SummaryService($completion);

        self::assertSame('A short summary.', $subject->summarize('Long text'));
        self::assertSame(
            [['prompt' => 'Summarize: Long text', 'options' => null]],
            $completion->completeCalls,
        );
    }
}
Copied!

Mocking providers 

Using mock provider 

Example: Mock provider
use Netresearch\NrLlm\Domain\Model\CompletionResponse;
use Netresearch\NrLlm\Domain\Model\UsageStatistics;
use Netresearch\NrLlm\Provider\Contract\ProviderInterface;

$mockProvider = $this->createMock(ProviderInterface::class);
$mockProvider
    ->method('chatCompletion')
    ->willReturn(new CompletionResponse(
        content: 'Mocked response',
        model: 'mock-model',
        usage: new UsageStatistics(100, 50, 150),
        finishReason: 'stop',
        provider: 'mock'
    ));
$mockProvider->method('isConfigured')->willReturn(true);
Copied!

Using HTTP mock 

Example: HTTP mock
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;

$mock = new MockHandler([
    new Response(200, [], json_encode([
        'choices' => [
            [
                'message' => ['content' => 'Test response'],
                'finish_reason' => 'stop',
            ],
        ],
        'model' => 'gpt-5',
        'usage' => [
            'prompt_tokens' => 10,
            'completion_tokens' => 5,
            'total_tokens' => 15,
        ],
    ])),
]);

$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);

// On an adapter constructed with its normal dependencies and configured
// by the manager, replace the transport after registerProvider():
$provider->setHttpClient($client);
Copied!