
Symfony:api Platform Tests Skill
- 418 installs
- 190 repo stars
- Updated August 6, 2026
- makfly/superpowers-symfony
symfony:api-platform-tests is an agent skill that writes PHPUnit and API Platform functional tests verifying endpoints, serialization, filters, security, and regression behavior before Symfony API release.
About
symfony:api-platform-tests is an agent skill in makfly/superpowers-symfony for testing API Platform resources with ApiPlatform\Symfony\Bundle\Test\ApiTestCase. It defines operation-level contracts, implements resources and DTO mappings, and validates collections, items, filters, pagination, authentication, and JSON schema assertions using Zenstruck Foundry v2 factories and DAMADoctrineTestBundle rollbacks. The reference guide covers create, update, patch, delete, search, range, and order filters plus denormalization error cases on API Platform v3 through v4. Developers invoke it when evolving API contracts and need happy-path and negative-path functional coverage that stays aligned with serialization and security policies.
- Bootstrap API test clients with KernelTestCase
- Assert JSON-LD, JSON, and OpenAPI contracts
- Cover filters, pagination, and custom operations
- Test authenticated and forbidden request paths
- Stabilize fixtures and database isolation
Symfony:Api Platform Tests by the numbers
- 418 all-time installs (skills.sh)
- Ranked #649 of 2,189 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/makfly/superpowers-symfony --skill symfonyapi-platform-testsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 418 |
|---|---|
| repo stars | ★ 190 |
| Last updated | August 6, 2026 |
| Repository | makfly/superpowers-symfony ↗ |
How do you test API Platform endpoints in Symfony?
Write PHPUnit and API Platform functional tests that verify endpoints, serialization, filters, security, and regression behavior before release.
Who is it for?
Symfony backend engineers shipping API Platform v3–v4 APIs who need ApiTestCase functional tests with Foundry data and schema checks.
Skip if: Non-Symfony REST projects, frontend-only testing, or teams not using API Platform's ApiTestCase client.
When should I use this skill?
A developer asks to test API Platform resources, assert hydra collections, verify filters, or cover authentication and validation failures in Symfony APIs.
What you get
PHPUnit ApiTestCase suites, Foundry-backed fixtures, schema assertions, and verified happy and negative API contract results.
- PHPUnit ApiTestCase functional test classes
- Foundry factory-backed API fixtures
- Documented contract and security verification results
By the numbers
- Parent superpowers-symfony repository bundles 44 skill definitions
- Reference guide lists 6 API Platform testing best practices
- Documents ApiTestCase patterns for API Platform v3 through v4
Files
Api Platform Tests (Symfony)
Use when
- Designing or evolving API Platform contracts and operations.
- Aligning serialization, validation, and security behavior.
Default workflow
1. Define operation-level contract and payload boundaries. 2. Implement resource/DTO/provider/processor changes with explicit mapping. 3. Apply operation-specific validation and security constraints. 4. Validate functional behavior across happy and negative paths.
Guardrails
- Keep API contract explicit and version-aware.
- Avoid exposing internal entity fields implicitly.
- Prevent drift between docs and actual serialization.
Progressive disclosure
- Use this file for execution posture and risk controls.
- Open references when deep implementation details are needed.
Output contract
- API artifacts changed (resource/DTO/provider/processor).
- Contract/security decisions and rationale.
- Functional verification results.
References
reference.mddocs/complexity-tiers.md
Reference
Testing API Platform
Version note.ApiPlatform\Symfony\Bundle\Test\ApiTestCaseand the Client/assertion API are stable v3→v4 (the namespace wasApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCaseonly in v2). The examples below use Zenstruck Foundry v2 (#[ResetDatabase]attribute style); see "Foundry v2 reset" for the trait-based PHPUnit 9 fallback.
Setup
composer require --dev api-platform/symfony # v4: the test client ships with the Symfony bridge
composer require --dev zenstruck/foundry
composer require --dev dama/doctrine-test-bundle # transactional rollback between testsBasic API Tests
Test Collection
<?php
// tests/Functional/Api/ProductTest.php
namespace App\Tests\Functional\Api;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Tests\Factory\ProductFactory;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
class ProductTest extends ApiTestCase
{
use Factories;
use ResetDatabase;
public function testGetCollection(): void
{
ProductFactory::createMany(30);
$response = static::createClient()->request('GET', '/api/products');
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/api/contexts/Product',
'@type' => 'hydra:Collection',
'hydra:totalItems' => 30,
]);
$this->assertCount(20, $response->toArray()['hydra:member']); // Default pagination
}
public function testGetItem(): void
{
$product = ProductFactory::createOne(['name' => 'Test Product']);
$response = static::createClient()->request(
'GET',
'/api/products/' . $product->getId()
);
$this->assertResponseIsSuccessful();
$this->assertJsonContains([
'@type' => 'Product',
'name' => 'Test Product',
]);
}
public function testGetItemNotFound(): void
{
static::createClient()->request('GET', '/api/products/999999');
$this->assertResponseStatusCodeSame(404);
}
}Test Create
public function testCreateProduct(): void
{
$response = static::createClient()->request('POST', '/api/products', [
'json' => [
'name' => 'New Product',
'price' => 1999,
'description' => 'A great product',
],
]);
$this->assertResponseStatusCodeSame(201);
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@type' => 'Product',
'name' => 'New Product',
'price' => 1999,
]);
$this->assertMatchesResourceItemJsonSchema(Product::class);
}
public function testCreateProductValidation(): void
{
static::createClient()->request('POST', '/api/products', [
'json' => [
'name' => '', // Invalid: empty
'price' => -100, // Invalid: negative
],
]);
$this->assertResponseStatusCodeSame(422);
$this->assertJsonContains([
'@type' => 'ConstraintViolationList',
]);
}Test Update
public function testUpdateProduct(): void
{
$product = ProductFactory::createOne(['name' => 'Old Name']);
static::createClient()->request('PUT', '/api/products/' . $product->getId(), [
'json' => [
'name' => 'New Name',
'price' => $product->getPrice(),
],
]);
$this->assertResponseIsSuccessful();
$this->assertJsonContains(['name' => 'New Name']);
}
public function testPatchProduct(): void
{
$product = ProductFactory::createOne(['name' => 'Old Name']);
static::createClient()->request('PATCH', '/api/products/' . $product->getId(), [
'headers' => ['Content-Type' => 'application/merge-patch+json'],
'json' => ['name' => 'Patched Name'],
]);
$this->assertResponseIsSuccessful();
$this->assertJsonContains(['name' => 'Patched Name']);
}Test Delete
public function testDeleteProduct(): void
{
$product = ProductFactory::createOne();
static::createClient()->request('DELETE', '/api/products/' . $product->getId());
$this->assertResponseStatusCodeSame(204);
// Verify deleted
static::createClient()->request('GET', '/api/products/' . $product->getId());
$this->assertResponseStatusCodeSame(404);
}Testing with Authentication
public function testAuthenticatedUserCanCreate(): void
{
$user = UserFactory::createOne();
static::createClient()->request('POST', '/api/products', [
'auth_bearer' => $this->getToken($user),
'json' => [
'name' => 'New Product',
'price' => 1999,
],
]);
$this->assertResponseStatusCodeSame(201);
}
public function testUnauthenticatedUserCannotCreate(): void
{
static::createClient()->request('POST', '/api/products', [
'json' => [
'name' => 'New Product',
'price' => 1999,
],
]);
$this->assertResponseStatusCodeSame(401);
}
public function testOnlyOwnerCanUpdate(): void
{
$owner = UserFactory::createOne();
$otherUser = UserFactory::createOne();
$product = ProductFactory::createOne(['owner' => $owner]);
// Owner can update
static::createClient()->request('PUT', '/api/products/' . $product->getId(), [
'auth_bearer' => $this->getToken($owner),
'json' => ['name' => 'Updated'],
]);
$this->assertResponseIsSuccessful();
// Other user cannot
static::createClient()->request('PUT', '/api/products/' . $product->getId(), [
'auth_bearer' => $this->getToken($otherUser),
'json' => ['name' => 'Hacked'],
]);
$this->assertResponseStatusCodeSame(403);
}Testing Filters
public function testSearchFilter(): void
{
ProductFactory::createOne(['name' => 'Apple iPhone']);
ProductFactory::createOne(['name' => 'Samsung Galaxy']);
ProductFactory::createOne(['name' => 'Apple iPad']);
$response = static::createClient()->request('GET', '/api/products?name=Apple');
$this->assertResponseIsSuccessful();
$this->assertCount(2, $response->toArray()['hydra:member']);
}
public function testRangeFilter(): void
{
ProductFactory::createOne(['price' => 500]);
ProductFactory::createOne(['price' => 1500]);
ProductFactory::createOne(['price' => 3000]);
$response = static::createClient()->request(
'GET',
'/api/products?price[gte]=1000&price[lte]=2000'
);
$this->assertResponseIsSuccessful();
$this->assertCount(1, $response->toArray()['hydra:member']);
}
public function testOrderFilter(): void
{
ProductFactory::createOne(['name' => 'Zebra']);
ProductFactory::createOne(['name' => 'Apple']);
ProductFactory::createOne(['name' => 'Banana']);
$response = static::createClient()->request('GET', '/api/products?order[name]=asc');
$this->assertResponseIsSuccessful();
$data = $response->toArray()['hydra:member'];
$this->assertEquals('Apple', $data[0]['name']);
$this->assertEquals('Banana', $data[1]['name']);
$this->assertEquals('Zebra', $data[2]['name']);
}Testing Pagination
public function testPagination(): void
{
ProductFactory::createMany(50);
// First page
$response = static::createClient()->request('GET', '/api/products');
$data = $response->toArray();
$this->assertCount(20, $data['hydra:member']); // Default per page
$this->assertEquals(50, $data['hydra:totalItems']);
$this->assertArrayHasKey('hydra:view', $data);
$this->assertArrayHasKey('hydra:next', $data['hydra:view']);
// Second page
$response = static::createClient()->request('GET', '/api/products?page=2');
$data = $response->toArray();
$this->assertCount(20, $data['hydra:member']);
}
public function testCustomItemsPerPage(): void
{
ProductFactory::createMany(20);
$response = static::createClient()->request('GET', '/api/products?itemsPerPage=5');
$data = $response->toArray();
$this->assertCount(5, $data['hydra:member']);
}Testing Schema
public function testResponseMatchesSchema(): void
{
ProductFactory::createOne();
static::createClient()->request('GET', '/api/products');
$this->assertMatchesResourceCollectionJsonSchema(Product::class);
}
public function testItemMatchesSchema(): void
{
$product = ProductFactory::createOne();
static::createClient()->request('GET', '/api/products/' . $product->getId());
$this->assertMatchesResourceItemJsonSchema(Product::class);
}Foundry v2 reset
Foundry v2 factories return real objects (no Proxy), and the database reset is exposed both as a trait and a PHPUnit 10+ attribute:
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use Zenstruck\Foundry\Attribute\ResetDatabase; // PHPUnit 10+ / Foundry 2.9
use Zenstruck\Foundry\Test\Factories;
#[ResetDatabase]
final class ProductTest extends ApiTestCase
{
use Factories;
// ...
}PHPUnit 9 fallback: use Zenstruck\Foundry\Test\ResetDatabase; + use Zenstruck\Foundry\Test\Factories; traits. DAMADoctrineTestBundle wraps each test in a rolled-back transaction (no truncation needed).
Asserting denormalization errors (v4)
When a resource sets collectDenormalizationErrors: true, a payload with type mismatches returns 422 with every offending field collected (rather than failing on the first one). Assert the violation list:
public function testTypeMismatchCollectsAllErrors(): void
{
static::createClient()->request('POST', '/api/products', [
'json' => [
'name' => 123, // expected string
'price' => 'free', // expected int
],
]);
$this->assertResponseStatusCodeSame(422);
$this->assertJsonContains(['@type' => 'ConstraintViolationList']);
}Best Practices
1. Use Foundry factories: Consistent test data 2. Reset database: Use ResetDatabase trait 3. Test both success and failure: Validation, auth, not found 4. Test filters and pagination: These are common API features 5. Schema assertions: Verify response structure 6. Authentication tests: Test both authenticated and anonymous
Skill Operating Checklist
Design checklist
- Confirm operation boundaries and invariants first.
- Minimize scope while preserving contract correctness.
- Test both happy path and negative path behavior.
Validation commands
- ./vendor/bin/phpunit --filter=Api
- ./vendor/bin/phpstan analyse
- php bin/console debug:router
Failure modes to test
- Invalid payload or forbidden actor.
- Boundary values / not-found cases.
- Retry or partial-failure behavior for async flows.
Related skills
How it compares
Pick symfony:api-platform-tests over generic PHPUnit skills when you need API Platform hydra assertions, Foundry fixtures, and contract-level schema checks.
FAQ
Which test base class does symfony:api-platform-tests use?
symfony:api-platform-tests uses ApiPlatform\Symfony\Bundle\Test\ApiTestCase, stable from API Platform v3 through v4. Tests call static::createClient()->request() and assert hydra collections, items, status codes, and JSON schemas.
How does symfony:api-platform-tests reset test data?
symfony:api-platform-tests recommends Zenstruck Foundry v2 factories with the #[ResetDatabase] attribute or ResetDatabase trait plus DAMADoctrineTestBundle transactional rollbacks between PHPUnit tests to keep database state isolated.
What API behaviors should symfony:api-platform-tests cover?
symfony:api-platform-tests covers success and failure paths including validation 422 responses, authentication 401 cases, authorization 403 checks, not-found 404s, search and range filters, pagination, and assertMatchesResourceCollectionJsonSchema structure checks.