with one click
testing-symfony
// Stratégie de Tests Symfony 8.0 / PHP 8.5. Use when writing tests, reviewing test coverage, or setting up testing.
// Stratégie de Tests Symfony 8.0 / PHP 8.5. Use when writing tests, reviewing test coverage, or setting up testing.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | testing-symfony |
| description | Stratégie de Tests Symfony 8.0 / PHP 8.5. Use when writing tests, reviewing test coverage, or setting up testing. |
| context | fork |
Versions : Symfony 8.0+ | PHP 8.5 | Pest 4.5+ | PHPUnit 13+ | Playwright
| Type | Outil | Usage |
|---|---|---|
| Unit/Integration | Pest 4.5+ (PHPUnit 13, arch tests) | Tests backend Symfony |
| Browser/E2E | Pest 4 Browser Testing (Playwright natif) | Tests frontend intégrés |
| Mutation | Infection | Qualité des tests (MSI >= 80%) |
| Static Analysis | PHPStan Level 10 | Vérification statique |
Abandonner : Panther (lourd, complexe) et Behat (verbeux). Pest 4 intègre tout.
// Pest.php
<?php
use Symfony\Component\Panther\PantherTestCase;
pest()->extend(Tests\TestCase::class)->in('Feature');
pest()->extend(Tests\TestCase::class)->in('Unit');
// Browser testing
pest()->extend(PantherTestCase::class)->in('Browser');
// Arch tests
pest()->arch('strict types')
->expect('App')
->toUseStrictTypes();
pest()->arch('no helpers')
->expect('App')
->not->toUse(['dd', 'dump']);
Source : Pest 4 Configuration
// tests/Unit/Service/OrderServiceTest.php
<?php
use App\Service\OrderService;
use App\Entity\Order;
test('create order returns order with id', function () {
// Arrange
$service = new OrderService($this->getEntityManager());
// Act
$order = $service->create(['customer' => 'Alice']);
// Assert
expect($order)->toBeInstanceOf(Order::class)
->and($order->getId())->not->toBeNull()
->and($order->getCustomer())->toBe('Alice');
});
Plus besoin de Panther ou configuration externe — Playwright natif dans Pest 4.
// tests/Browser/LoginTest.php
<?php
test('user can login', function () {
$this->browse(function (Browser $browser) {
$browser->visit('/login')
->type('email', 'alice@example.com')
->type('password', 'secret')
->press('Login')
->assertPathIs('/dashboard')
->assertSee('Welcome Alice');
});
});
test('login validates email format', function () {
$this->browse(function (Browser $browser) {
$browser->visit('/login')
->type('email', 'invalid-email')
->type('password', 'secret')
->press('Login')
->assertSee('Please provide a valid email');
});
});
Source : Pest 4 Browser Testing
Presets réutilisables pour valider l'architecture.
// tests/Arch/ArchitectureTest.php
<?php
// Clean Architecture — Domain ne dépend pas d'Infrastructure
pest()->arch('domain is independent')
->expect('App\Domain')
->not->toUse(['App\Infrastructure', 'Doctrine\ORM']);
// Use Cases utilisent des interfaces
pest()->arch('use cases depend on abstractions')
->expect('App\Application\UseCase')
->toOnlyUse(['App\Domain', 'App\Application\Port']);
// Controllers respectent le namespace
pest()->arch('controllers in correct namespace')
->expect('App\Presentation\Controller')
->toBeClasses()
->toHaveSuffix('Controller')
->toOnlyBeUsedIn(['App\Presentation']);
// Pas de dd() ou dump() en prod
pest()->arch('no debug helpers')
->expect(['dd', 'dump', 'var_dump'])
->not->toBeUsedIn('App');
Source : Pest Arch Testing
# composer.json
{
"require-dev": {
"infection/infection": "^0.29"
}
}
# infection.json5
{
"source": { "directories": ["src"] },
"logs": { "badge": { "branch": "main" } },
"mutators": { "@default": true },
"minMsi": 80,
"minCoveredMsi": 85
}
# Exécution
vendor/bin/infection --threads=4
Philosophie : "Code coverage = quantité. MSI (Mutation Score Indicator) = qualité."
Source : Infection
// tests/Feature/Api/UserApiTest.php
<?php
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
test('get user returns 200', function () {
$client = static::createClient();
$client->request('GET', '/api/users/123');
expect($client->getResponse()->getStatusCode())->toBe(200)
->and(json_decode($client->getResponse()->getContent(), true))
->toHaveKey('id')
->toHaveKey('email');
});
test('create user validates email', function () {
$client = static::createClient();
$client->request('POST', '/api/users', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode([
'name' => 'Bob',
'email' => 'invalid',
]));
expect($client->getResponse()->getStatusCode())->toBe(422);
});
// tests/Fixtures/UserFixtures.php
<?php
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use App\Entity\User;
class UserFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
$user = new User();
$user->setEmail('alice@example.com');
$user->setPassword('hashed_password');
$manager->persist($user);
$manager->flush();
$this->addReference('user_alice', $user);
}
}
# phpstan.neon
parameters:
level: 10
paths:
- src
- tests
excludePaths:
- src/Kernel.php
checkMissingIterableValueType: true
checkGenericClassInNonGenericObjectType: true
reportUnmatchedIgnoredErrors: true
Source : PHPStan
Voir @.claude/rules/07-testing.md pour principes transverses.