---
title: "Testing"
description: "Testing Guide"
type: "docs"
category: "doc"
tags: []
authors: [Anonymous]
date: "2026-09-09"
last_update: "2026-09-09"
time_minutes: 4
draft: false
unlisted: false
url: "https://www.derafu.dev/docs/core/translation/testing"
---

# Testing Guide

How to test code that uses `derafu/translation` — translatable exceptions,
ICU formatting, and resource registration.

## Testing Untranslated Behavior

Since `getMessage()` is fully formatted at construction time, most tests
don't need a translator at all:

```php
use PHPUnit\Framework\TestCase;

final class ValidationExceptionTest extends TestCase
{
    public function testPlainStringMessage(): void
    {
        $exception = new ValidationException('Email is invalid.');

        $this->assertSame('Email is invalid.', $exception->getMessage());
    }

    public function testIcuParametersAreSubstitutedEagerly(): void
    {
        $exception = new ValidationException([
            'The value {value} is not a valid email.',
            'value' => 'test@example',
        ]);

        $this->assertSame(
            'The value test@example is not a valid email.',
            $exception->getMessage(),
        );
    }
}
```

## Testing with a Stub Translator

For unit tests, a `TranslatorInterface` stub configured with
`willReturnCallback()` is usually enough — it doesn't need to verify how
many times it was called, just what it returns:

```php
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Translation\TranslatorInterface;

final class OrderExceptionTest extends TestCase
{
    public function testTranslation(): void
    {
        $translator = $this->createStub(TranslatorInterface::class);
        $translator->method('trans')->willReturnCallback(
            fn (string $id, array $parameters = []): string => match ($id) {
                'Not enough stock for "{product}".' => sprintf(
                    'No hay stock de "%s".',
                    $parameters['product'],
                ),
                default => $id,
            },
        );

        $exception = OrderException::insufficientStock('Widget');

        $this->assertSame(
            'No hay stock de "Widget".',
            $exception->trans($translator, 'es'),
        );
    }
}
```

Use `createMock()` instead of `createStub()` only when you actually need to
assert on the number of calls (e.g. `expects($this->once())`) — PHPUnit
flags `createMock()` used purely for return-value stubbing as unnecessary.

## Testing with a Real Translator

For integration tests, use the real `TranslatorFactory` +
`TranslationResourceRegistrar` against fixture translation files —
verifying the actual behavior end to end, ICU formatting included.

Fixtures for the examples below (`tests/fixtures/translations/`):

```php
// errors+intl-icu.es.php
return [
    'The value {value} is not a valid email.' => 'El valor {value} no es un correo electrónico válido.',
];
```

```php
// messages+intl-icu.en.php
return [
    'welcome' => 'Welcome!',
];
```

And an extra directory used only by the precedence test below,
`tests/fixtures/translations-override/`:

```php
// messages+intl-icu.en.php
return [
    'welcome' => 'Overridden message.',
];
```

```php
use PHPUnit\Framework\TestCase;
use Derafu\Translation\TranslationResourceRegistrar;
use Derafu\Translation\TranslatorFactory;
use Symfony\Component\Translation\Translator;

final class TranslationIntegrationTest extends TestCase
{
    private Translator $translator;

    protected function setUp(): void
    {
        $this->translator = TranslatorFactory::create('en', ['en', 'es']);

        $registrar = new TranslationResourceRegistrar($this->translator);
        $registrar->registerDirectory(__DIR__ . '/../fixtures/translations');
    }

    public function testValidationErrorIsTranslated(): void
    {
        $exception = new ValidationException([
            'The value {value} is not a valid email.',
            'value' => 'test@example',
        ]);

        $this->assertSame(
            'El valor test@example no es un correo electrónico válido.',
            $exception->trans($this->translator, 'es'),
        );
    }
}
```

## Testing Resource Registration

Test `TranslationResourceRegistrar` directly against a fixtures directory
to verify discovery and precedence, without needing any specific exception
class:

```php
use PHPUnit\Framework\TestCase;
use Derafu\Translation\TranslationResourceRegistrar;
use Derafu\Translation\TranslatorFactory;

final class TranslationResourceRegistrarTest extends TestCase
{
    public function testDiscoversDomainsAndLocales(): void
    {
        $translator = TranslatorFactory::create('en');
        $registrar = new TranslationResourceRegistrar($translator);

        $registrar->registerDirectory(__DIR__ . '/../fixtures/translations');

        sort($domains = $registrar->getRegisteredDomains());
        sort($locales = $registrar->getRegisteredLocales());

        $this->assertSame(['errors+intl-icu', 'messages+intl-icu'], $domains);
        $this->assertSame(['en', 'es'], $locales);
    }

    public function testLaterDirectoriesOverridePreviousOnesForTheSameKey(): void
    {
        $translator = TranslatorFactory::create('en');
        $registrar = new TranslationResourceRegistrar($translator);

        $registrar->registerDirectories([
            __DIR__ . '/../fixtures/translations',
            __DIR__ . '/../fixtures/translations-override',
        ]);

        $this->assertSame(
            'Overridden message.',
            $translator->trans('welcome', [], 'messages+intl-icu'),
        );
    }
}
```

## Testing Custom Resource Providers

```php
use PHPUnit\Framework\TestCase;
use Derafu\Translation\Contract\TranslationResourceProviderInterface;

final class PackageTranslationResourceProviderTest extends TestCase
{
    public function testReturnsExpectedDirectories(): void
    {
        $provider = new PackageTranslationResourceProvider();

        $directories = iterator_to_array($provider->getDirectories());

        $this->assertNotEmpty($directories);
        foreach ($directories as $directory) {
            $this->assertDirectoryExists($directory);
        }
    }
}
```

## Testing ICU Edge Cases

```php
use PHPUnit\Framework\TestCase;
use Derafu\Translation\TranslatableMessage;

final class IcuFormattingTest extends TestCase
{
    public function testMissingOtherCategoryFallsBackToRawMessage(): void
    {
        $message = new TranslatableMessage(
            '{gender, select, male{He} female{She}}',
            ['gender' => 'unknown'],
        );

        $this->assertSame(
            '{gender, select, male{He} female{She}}',
            (string) $message,
        );
    }

    public function testUnmatchedBraceFallsBackToRawMessage(): void
    {
        $message = new TranslatableMessage('Hello {name', ['name' => 'John']);

        $this->assertSame('Hello {name', (string) $message);
    }

    public function testMissingParameterLeavesPlaceholderUnexpanded(): void
    {
        $message = new TranslatableMessage('{count} items');

        $this->assertSame('{count} items', (string) $message);
    }
}
```

---

Remember:

- Test both the untranslated (`getMessage()`) and translated (`trans()`)
  paths.
- Prefer `createStub()` over `createMock()` for a `TranslatorInterface`
  double unless you're actually asserting call counts.
- Use real fixture files and a real `Translator` for integration-level
  coverage of ICU formatting and resource discovery — it's fast and
  catches naming-convention mistakes that a stub translator can't.
- Always test with the `other` case missing and with missing parameters:
  both fail *silently*, falling back to the raw message, not by throwing.



---
Last updated on 09/09/2026

