name: phpunit-integration-test-generation version: 3.7.2 description: Use this skill when the user asks to generate, write, or create integration tests for a Shopware 6 source class whose contract requires wired-up code — phrases like "generate integration tests for X", "write an integration test for this controller", "test this indexer", "create an integration test for the message handler". Detects supported integration patterns (controller/route, scheduled-task, message-handler, indexer, DAL-persistence flow, multi-service coordinator) and applies a template producing an INTEGRATION-001..008-compliant test using IntegrationTestBehaviour against the real DAL, container, and HTTP/messaging. When the source class is unit-shape (no persistence, no kernel state, no wiring under test), returns SKIPPED and points at phpunit-unit-test-writing. Do NOT activate for unit tests or migration tests (use phpunit-migration-test-generation). user-invocable: true context: fork agent: test-writing:test-generator allowed-tools: Read, Grep, Glob, Write, Edit, mcp__plugin_dev-tooling_php-tooling
PHPUnit Integration Test Generation
Generate Shopware-compliant PHPUnit integration tests that exercise wired-up code through IntegrationTestBehaviour, produce integration-shape assertions, and pass PHPStan and PHPUnit validation.
File Write Restrictions
Write ONLY to:
tests/integration/**— Integration test files
NEVER write to:
src/**— Source code (read-only)tests/unit/**— Out of scope (usephpunit-unit-test-generation)tests/migration/**— Out of scope (usephpunit-migration-test-generation)- Any other directory
Workflow
digraph integration_test_generation {
"User invocation" [shape=doublecircle];
"Validate input" [shape=box];
"Source is PHP class in src/?" [shape=diamond];
"Analyze source" [shape=box];
"Integration pattern detected?" [shape=diamond];
"Select template + apply" [shape=box];
"Write test file" [shape=box];
"Validate (PHPStan, PHPUnit, ECS)" [shape=box];
"All green?" [shape=diamond];
"Fix iteration (max 3)" [shape=box];
"Iterations exhausted?" [shape=diamond];
"Report SUCCESS" [shape=doublecircle];
"Report PARTIAL" [shape=doublecircle];
"Report SKIPPED (defer to unit gen)" [shape=doublecircle];
"Report FAILED" [shape=doublecircle];
"User invocation" -> "Validate input";
"Validate input" -> "Source is PHP class in src/?";
"Source is PHP class in src/?" -> "Report FAILED" [label="no"];
"Source is PHP class in src/?" -> "Analyze source" [label="yes"];
"Analyze source" -> "Integration pattern detected?";
"Integration pattern detected?" -> "Report SKIPPED (defer to unit gen)" [label="no — unit-shape SUT"];
"Integration pattern detected?" -> "Select template + apply" [label="yes"];
"Select template + apply" -> "Write test file";
"Write test file" -> "Validate (PHPStan, PHPUnit, ECS)";
"Validate (PHPStan, PHPUnit, ECS)" -> "All green?";
"All green?" -> "Report SUCCESS" [label="yes"];
"All green?" -> "Iterations exhausted?" [label="no"];
"Iterations exhausted?" -> "Report PARTIAL" [label="yes"];
"Iterations exhausted?" -> "Fix iteration (max 3)" [label="no"];
"Fix iteration (max 3)" -> "Validate (PHPStan, PHPUnit, ECS)";
}
Phase 1: Validate Input
- Verify single file provided
- Verify file exists and is a PHP class (not interface/trait/abstract)
- Verify path starts with
src/
If validation fails, return FAILED with reason.
Phase 2: Analyze Source
Read the source class and detect which integration pattern applies. See references/source-analysis.md for the full detection logic.
Step 1: Extract Metadata
- Class name, full namespace
#[Package('...')]attribute value (default to'framework'if absent)- Constructor dependencies (FQCN list)
- Area from namespace (
Core,Administration,Storefront,Elasticsearch) - Public methods and their return types
Step 2: Detect Pattern
Walk the decision table in references/source-analysis.md. Patterns are evaluated top to bottom; the first match wins.
| Pattern | Indicator |
|---|---|
controller |
Extends AbstractController or AbstractRoute / Abstract*Route, or methods carry #[Route] |
scheduled-task |
Extends ScheduledTaskHandler, OR has #[AsMessageHandler] and the __invoke() parameter is a ScheduledTask subclass |
message-handler |
Has #[AsMessageHandler] on __invoke() AND the parameter is a domain message (not a ScheduledTask) |
indexer |
Extends EntityIndexer (Shopware) |
dal-flow |
Constructor takes EntityRepository AND public methods write through it (create, update, upsert) AND the SUT contract is "the data was persisted" or "the indexer/event was triggered" |
multi-service |
Constructor takes ≥ 3 non-boundary dependencies (boundary set defined in INTEGRATION-002), and at least 2 are stateful (DAL, indexer, event dispatcher, system config) |
Step 3: Decide
- Pattern detected → Continue to Phase 3
- No pattern detected → The SUT is unit-shape. Return SKIPPED with
skip_type: unit_test_more_appropriateand reason: "Source class fits a unit-shape pattern (no persistence, no wiring, no multi-service coordination under test). Usephpunit-unit-test-generationinstead." Reference the relevant refactoring pattern inphpunit-integration-to-unit-migrating/references/refactoring-patterns.mdwhen the SUT looks like a factory, compiler pass, single subscriber, parser, constraint-only validator, or DAL materializer.
Phase 3: Generate Test
Step 1: Determine Test Path
Mirror source path with src/ → tests/integration/:
src/Core/Content/Product/ProductIndexer.php→tests/integration/Core/Content/Product/ProductIndexerTest.phpsrc/Core/Checkout/Cart/SalesChannel/CartLoadRoute.php→tests/integration/Core/Checkout/Cart/SalesChannel/CartLoadRouteTest.php
Namespace mirrors the path: Shopware\Tests\Integration\Core\Content\Product.
Step 2: Apply Template
Use the integration test template at templates/integration-test.md. The template has a base block plus one conditional section per pattern. Include exactly one pattern section based on Phase 2 detection. The base block defers all behavior trait use statements to the conditional section, because the trait choice varies by pattern:
- Always (base block): namespace,
#[CoversClass],#[Package],@internal, empty class shell controller:IntegrationTestBehaviour + SalesChannelApiTestBehaviour(or admin/storefront equivalent),IdsCollection,KernelBrowserbuilt viacreateCustomSalesChannelBrowser([...]),#[Group('store-api')], request invocation, response assertionsscheduled-task:DatabaseTransactionBehaviour + KernelTestBehaviour(lighter thanIntegrationTestBehaviour),parent::setUp(), direct$handler->run()invocation, raw SQLConnection::fetchOne(...)assertionsmessage-handler:IntegrationTestBehaviour, direct($this->handler)($message)invocation, DAL read-back assertion (bus dispatch only when transport routing is part of the SUT contract)indexer:DatabaseTransactionBehaviour + KernelTestBehaviour,parent::setUp(), realtime flow — write via DAL captures$event,$indexer->update($event)returns?EntityIndexingMessage, assert on the returned message's datadal-flow:IntegrationTestBehaviour, arrange prerequisite state via DAL, invoke SUT, assert persisted result via separate DAL readmulti-service:IntegrationTestBehaviourcomposed with domain behaviours per dependency (AppSystemTestBehaviour,GuzzleTestClientBehaviour,MailTemplateTestBehaviour), configure inputs throughSystemConfigService/ DAL, invoke SUT, assert effects across multiple collaborators
Fill placeholders using Phase 2 metadata. Use IdsCollection (Shopware\Core\Test\Stub\Framework\IdsCollection) for any test that manages more than one entity ID, and type all repository properties with the generic PHPDoc @var EntityRepository<XxxCollection>. Leave TODO: markers where the behavior-specific arrange or assertion is genuinely SUT-dependent and not derivable from class structure — these are the spots a human or follow-up step must complete.
Step 3: Rule Compliance Checklist
The generated test must satisfy all INTEGRATION-001..008 rules at write time. Self-check before validation:
- INTEGRATION-001:
use IntegrationTestBehaviour;present - INTEGRATION-002: SUT and primary collaborators retrieved from container (not mocked); only boundary types may be mocked (HTTP client, mailer, clock, randomness)
- INTEGRATION-003: any DDL, filesystem write, or cache write has a matching teardown (or is wrapped in try/finally)
- INTEGRATION-004: assertions don't depend on wall-clock time or unsourced randomness — assertions on UUIDs go through referential lookups, not literal-value equality
- INTEGRATION-005: no
#[Depends]between test methods - INTEGRATION-006: never
markTestSkippedfor missing fixtures — create them - INTEGRATION-007: arrange + setUp keeps balance with assertion shape; if assertions are unit-shape only, switch to unit test generation (Phase 2 should have caught this)
- INTEGRATION-008: at least one assertion is integration-shape (persistence read-back, event observation, container resolution, HTTP response, real-broker delivery)
Step 4: Write Test File
Write to the path determined in Step 1.
Phase 4: Validate and Fix
Validation Loop
- [ ] PHPStan passes (0 errors)
- [ ] PHPUnit passes (all tests green)
- [ ] ECS passes (code style)
Step 1: Run PHPStan
{
"paths": ["tests/integration/Path/To/GeneratedTest.php"],
"error_format": "json"
}
Step 2: Fix PHPStan Errors
Common integration test errors:
- Missing imports for
IntegrationTestBehaviour,Context,Uuid,TestDefaults,Criteria - Type mismatches on
static::getContainer()->get(...)results — useassert($x instanceof Foo)or PHPDoc when the container returns the abstract type - Unknown method on
EntityRepository— verify the entity repository service id
Step 3: Run PHPUnit
{
"paths": ["tests/integration/Path/To/GeneratedTest.php"],
"output_format": "result-only"
}
If tests fail, re-run without output_format to get failure details.
Step 4: Fix Test Failures
Common integration test failures:
- DDL/filesystem state leaked from a previous run — add cleanup in
tearDown()(INTEGRATION-003) - Foreign-key constraint violations during DAL writes — set up required parent entities first
markTestSkippedtriggered by missing fixtures — replace with fixture creation (INTEGRATION-006)- Test references the wrong entity-repository service id
Step 5: Run ECS Check and Fix
Check for violations, then apply fixes if needed.
Repeat Until Pass
Loop Steps 1-5 until all validations pass. Maximum 3 iterations — after that, proceed to Phase 5 with status PARTIAL.
Phase 5: Generate Report
For output format and examples, see references/output-format.md.
Status Determination
| Condition | Status | skip_type |
|---|---|---|
| All validations pass | SUCCESS | — |
| Test generated, validation issues remain after 3 iterations | PARTIAL | — |
| Source class is unit-shape (no integration pattern matched) | SKIPPED | unit_test_more_appropriate |
| Invalid input (not a PHP class, file not found, not in src/) | FAILED | — |