| name | codecept-functional |
| description | Use the skill to create, update, fix, edit or run functional, unit, codecept tests in "src/**/tests/**, tests/**" |
Testing rule
Codeception tests MUST follow Spryker conventions: expressive naming, AAA structure, entry point focus, and helpers for reusable logic.
Critical Instructions
Test Naming: Use test prefix with given/when/then pattern or outcome-focused names
Structure: Arrange/Act/Assert comments, max 3 lines per section
Focus: Test entry points (Facades, Controllers, Commands), verify outcomes not flows
Setup: Per-test only, never global setUp for whole class
Data: Use DataBuilders for transfers, tester helpers for entities
Tester: Access facade via $this->tester->getFacade(), factory via $this->tester->getFactory()
Mocking: Mock external dependencies only (3rd party APIs), not internal components
Focus: Test entry points (Controllers, Facades, Console Commands).
Coverage Goal: Add as less tests as possible to cover core functionality.
Test Depth: Do not test method flows. Focus on outcomes.
Body Structure: Use the following four-part structure:
Readability: Use helper methods and classes when possible to keep tests clean.
Setup: NEVER use global framework setup methods for the whole test class. Setup must be done per test method (locally) when required.
Test Naming
testGivenValidQuoteWhenPlacingOrderThenOrderIsCreatedSuccessfully()
testCheckoutResponseContainsErrorIfCustomerAlreadyRegistered()
testCanWithCentAmountLessThanConfigurationReturnsTrue()
testPlaceOrder(), testSuccess(), testValidateCustomerEmailFormat()
Test Structure (Unit Tests)
public function testCheckoutSuccessfully(): void
{
$productTransfer = $this->tester->haveProduct();
$quoteTransfer = $this->buildQuoteWithProduct($productTransfer);
$result = $this->tester->getFacade()->placeOrder($quoteTransfer);
$this->assertTrue($result->getIsSuccess());
}
Extract complex setup to helper methods when >3 lines needed.
Data Builders & Helpers
$quote = (new QuoteBuilder())
->withItem($itemBuilder)
->withCustomer()
->withTotals()
->build();
$product = $this->tester->haveProduct();
$store = $this->tester->haveStore([StoreTransfer::NAME => 'DE']);
$this->tester->haveAvailabilityConcrete($sku, $store);
protected const string STORE_NAME_DE = 'DE';
Generate data builders when a transfer has no existing builder:
docker/sdk testing console transfer:databuilder:generate
Setup & Teardown
protected function setUp(): void
{
parent::setUp();
$this->tester->setDependency(Key::PLUGINS, [new Plugin()]);
}
public static function setUpBeforeClass(): void // DON'T
Data Cleanup
When TransactionHelper cannot be used (e.g. tests with @disableTransaction), register cleanup callbacks in helper have*() methods to prevent DB pollution across tests:
public function havePriceProduct(array $seed = []): PriceProductTransfer
{
$transfer = $this->getFacade()->createPriceForProduct(
(new PriceProductBuilder($seed))->build()
);
$this->getDataCleanupHelper()->_addCleanup(function () use ($transfer): void {
$this->cleanupPriceProductStore($transfer->getIdPriceProduct());
$this->cleanupPriceProduct($transfer->getIdPriceProduct());
});
return $transfer;
}
DataCleanupHelper must be enabled in codeception.yml for this to work.
DO NOT Mock Internal Components
NEVER mock: Facade, Plugins, Factory, Repository, EntityManager
ONLY mock: Config (when needed), External 3rd party APIs
$result = $this->tester->getFacade()->processEntity($transfer);
$plugin = new MyPlugin();
$plugin->setFactory($this->tester->getFactory());
$configMock = $this->createMock(Config::class);
$configMock->method('getSomething')->willReturn('value');
$this->tester->getFactory()->setConfig($configMock);
$this->tester->setDependency(DependencyProvider::PLUGINS, []);
$facadeMock = $this->createMock(FacadeInterface::class);
$pluginMock = $this->createMock(PluginInterface::);
Mocking External Dependencies
Mock 3rd party APIs to avoid real network calls.
protected function createFacadeWithMockedProvider(): FacadeInterface
{
$mockProvider = $this->createMock(ExternalProviderInterface::class);
$mockProvider->method('execute')->willReturn(new Response('Test'));
$adapter = new VendorAdapter(
provider: $mockProvider,
mapper: $this->tester->getFactory()->createDataMapper(),
config: $this->createMockConfig()->getConfigurations(),
);
$plugin = $this->createMock(ProviderPluginInterface::class);
$plugin->method('getAdapter')->willReturn($adapter);
$this->tester->setDependency(DependencyProvider::PLUGIN, $plugin);
return $this->tester->();
}
Test Class Structure
namespace SprykerTest\Zed\Checkout\Business;
use Codeception\Test\Unit;
class CheckoutFacadeTest extends Unit
{
protected CheckoutBusinessTester $tester;
}
Module-Specific Tester
Location: tests/SprykerTest/[Layer]/[Module]/_support/[Module][Layer]Tester.php
namespace SprykerTest\Zed\ExampleModule;
use Codeception\Actor;
class ExampleModuleBusinessTester extends Actor
{
use _generated\ExampleModuleBusinessTesterActions;
public function haveEntityWithItems(string $id, array $items): void
{
foreach ($items as $item) {
$this->getFacade()->createEntity(
(new EntityTransfer())->setId($id)->setItem($item)
);
}
}
public function seeEntityExists(string $id):
{
= ->()->(
( ())->(
( ())->([])
)
);
->(, ->());
}
}
Custom methods: have[Entity]() for data, see[Entity]() for assertions
Use unique IDs to avoid conflicts:
public function haveEntity(): int
{
$parent = $this->haveParentEntity([
'name' => sprintf('Parent_%s', uniqid()),
]);
$entity = $this->haveChildEntity([
'name' => sprintf('Child_%s', uniqid()),
'fkParent' => $parent->getId(),
]);
return $entity->getId();
}
After creating: Run docker/sdk testing codecept build -c path/to/codeception.yml
Communication Layer Plugin Tests
public function testPluginMethod(): void
{
$this->tester->setDependency(DependencyProvider::PLUGINS, []);
$plugin = new MyPlugin();
$plugin->setFactory($this->tester->getFactory());
$result = $plugin->doSomething();
$this->assertTrue($result);
}
public function testPluginWithConfig(): void
{
$configMock = $this->createMock(Config::class);
$configMock->method('getValue')->willReturn('test');
$this->tester->getFactory()->setConfig($configMock);
= ();
->(->tester->());
->(, ->());
}
Codeception Configuration
Location: tests/SprykerTest/[Layer]/[Module]/codeception.yml
namespace: SprykerTest\Zed\[Module]
paths:
tests: .
data: ../../../_data
support: _support
output: ../../../_output
coverage:
enabled: true
remote: false
whitelist: { include: ['../../../../src/*'] }
suites:
Business:
path: Business
actor: [Module]BusinessTester
modules:
enabled:
- Asserts
- \SprykerTest\Shared\Testify\Helper\{Environment,ConfigHelper,LocatorHelper,DependencyHelper}
- \SprykerTest\Shared\Testify\Helper\DataCleanupHelper
- \SprykerTest\Zed\Testify\Helper\Business\BusinessHelper
- \SprykerTest\Shared\Propel\Helper\TransactionHelper
Creating Custom Helpers
Location: tests/SprykerTest/[Layer]/[Module]/_support/Helper/[Module]Helper.php
namespace SprykerTest\Zed\[Module]\Helper;
use Codeception\Module;
use SprykerTest\Shared\Testify\Helper\LocatorHelperTrait;
class [Module]Helper extends Module
{
use LocatorHelperTrait;
public function have[Entity](array $seed = []): [Entity]Transfer
{
return $this->getLocator()->[module]()->facade()
->create[Entity]((new [Entity]Builder($seed))->build());
}
}
Setup: Add to codeception.yml, run docker/sdk testing codecept build, use $this->tester->have[Entity]()
Directory Structure
tests/{PyzTest,SprykerTest}/
├── Zed/Module/
│ ├── Business/
│ │ └── Facade/ # Facade tests
│ ├── Communication/ # Controller/Plugin tests
│ ├── Persistence/ # Repository/EntityManager tests
│ ├── _support/
│ │ ├── Helper/ # Custom helpers
│ │ └── PageObject/ # Page objects for UI tests
│ └── codeception.yml
├── Client/Module/
│ ├── Business/ # Client method tests
│ └── codeception.yml
├── Service/Module/
│ ├── Business/ # Stateless service tests (no DB needed)
│ └── codeception.yml
├── Glue/Module/
│ ├── Business/ # API endpoint tests
│ └── codeception.yml
├── Shared/Module/
│ ├── Business/ # Cross-layer utility tests
│ └── codeception.yml
└── Yves/Module/
├── Presentation/ # UI/JavaScript tests
└── codeception.yml
Project namespace — tests live in tests/<Ns>Test/**. The tree above and every
tests/SprykerTest/… path in this skill is the core-package layout. A project that registered a
custom namespace instead of Pyz (KernelConstants::PROJECT_NAMESPACES = ['<Ns>', 'Pyz'] in
config/Shared/config_default.php) has a third tree, and that is where the project's own tests
belong. Read the real namespaces from composer.json autoload-dev.psr-4 before choosing a path —
never assume PyzTest.
- Path + namespace:
tests/<Ns>Test/<Layer>/<Module>/…, mirroring the structure above, with
namespace: <Ns>Test\<Layer>\<Module> in its codeception.yml.
projectNamespaces: ['<Ns>', 'Pyz'] in every suite config — project namespace first = highest
precedence. LocatorHelper overwrites the runtime PROJECT_NAMESPACES with the suite's own
value, so a suite left on ['Pyz'] resolves src/Pyz and never exercises the project's src/<Ns>
overrides. Failure signature: the suite is green and every assertion is about the inherited
demoshop's values instead of the project's — nothing fails, the values are just someone else's.
<Ns>Test cests need HAND-WRITTEN @group annotations. The upstream
DocBlockTestGroupAnnotation2Sniff matches (SprykerTest|PyzTest) only, so it generates nothing
for a custom namespace. Failure signature: codecept run … -g <Group> runs zero tests out of
tests/<Ns>Test/** while the files plainly exist — inclusive group filters require every named
group to be present on the test.
Testing Console Commands
Enable ConsoleHelper in codeception.yml:
- \SprykerTest\Zed\Console\Helper\ConsoleHelper
Test:
$command = new MyConsoleCommand();
$commandTester = $this->tester->getConsoleTester($command);
$commandTester->execute([
MyConsoleCommand::ARGUMENT_FOO => 'value',
'--' . MyConsoleCommand::OPTION_BAR => 'value',
]);
$this->assertSame(MyConsoleCommand::CODE_SUCCESS, $commandTester->getStatusCode());
$this->assertStringContainsString('Expected output', $commandTester->getDisplay());
Key Test Helpers
Enable in codeception.yml modules.enabled:
Testify helpers (Shared):
ConfigHelper - Mock configs: $this->tester->getModuleConfig(), mockConfigMethod()
DependencyHelper - Mock dependencies: $this->tester->setDependency()
LocatorHelper - Access modules: $this->tester->getLocator()->module()->facade()
DataCleanupHelper - Auto cleanup test data
P&S helpers (Zed):
PublishAndSynchronizeHelper - assertEntityIsPublished(), assertEntityIsSynchronizedToStorage()
EventBehaviorHelper - triggerRuntimeEvents()
QueueHelper - assertMessagesConsumedFromEventQueue(), cleanupInMemoryQueue()
StorageHelper - assertStorageHasKey(), cleanupInMemoryStorage()
SearchHelper - assertSearchHasKey(), cleanupInMemorySearch()
Business/Communication (Zed):
BusinessHelper - Mock facade: $this->tester->mockFacadeMethod()
CommunicationHelper - Mock communication layer
ConsoleHelper - Test console commands: $this->tester->getConsoleTester()
Database:
TransactionHelper - Wrap tests in transactions, auto rollback
P&S Testing (Storage/Search)
Enable helpers: PublishAndSynchronizeHelper, EventBehaviorHelper, QueueHelper, StorageHelper, SearchHelper
Test flow:
$entity = $this->tester->haveEntity();
$this->tester->assertEntityIsPublished('event.name', 'publish.queue');
$this->tester->assertEntityIsSynchronizedToStorage('sync.queue');
$this->tester->assertStorageHasKey('storage:key');
$this->tester->updateEntity($entity);
$this->tester->assertEntityIsPublished('event.name', 'publish.queue');
$this->tester->assertEntityIsUpdatedInStorage('sync.queue');
$this->tester->deleteEntity($entity);
$this->tester->assertEntityIsPublished('event.name', 'publish.queue');
$this->tester->assertEntityIsRemovedFromStorage('sync.queue');
$this->tester->assertStorageNotHasKey('storage:key');
Troubleshooting: Add @disableTransaction if PropelException about transactions
New Module Test Setup
When adding tests to a new module, follow this checklist:
- Create the directory structure (see above)
- Create
codeception.yml with suites and helpers
- Create the tester class in
_support/
- Create helper classes in
_support/Helper/ if needed
- Build to generate tester actions:
docker/sdk testing codecept build -c tests/SprykerTest/Zed/YourModule
- Write test classes following the AAA pattern
- Run tests to verify
Always run codecept build after: creating a new module, adding helpers, or changing codeception.yml.
Running Tests
Prerequisite — the stack MUST have been booted with docker/sdk up -t. Every command in this
section is docker/sdk testing …, which needs the testing container that only -t creates. On a
stack booted with a plain docker/sdk up these commands are a silent no-op: no testing
container, SPRYKER_TESTING_ENABLED unset, and codeception falls into a phantom devtest
environment — so the output reads as project failures (or as nothing at all) with no error naming the
real cause. An already-running plain stack upgrades non-destructively: just re-run
docker/sdk up -t — no reset, no data loss. Other skills work around this by entering testing mode
first (script -q /dev/null docker/sdk testing "exit") without naming the root cause; the missing
-t is the root cause — fix it there, don't rely on the workaround.
Scope script to commands that actually allocate a TTY — docker/sdk up, docker/sdk reset,
and entering an interactive docker/sdk testing/cli shell. Never wrap docker/sdk console … or
npx cypress run in it: they need no TTY, and the wrapper fails with
tcgetattr/ioctl: Operation not supported on socket plus an empty log, which reads as a project
failure rather than a wrapper failure.
Build tests (after adding helpers/changes):
docker/sdk testing codecept build -c path/to/codeception.yml
Run all tests in a module:
docker/sdk testing codecept run -c path/to/codeception.yml
Run a specific suite:
docker/sdk testing codecept run -c path/to/codeception.yml -g Business
Run a specific test file:
docker/sdk testing codecept run tests/SprykerTest/Zed/ModuleName/Business/SomeFacadeTest.php
Run a single test method:
docker/sdk testing codecept run tests/SprykerTest/Zed/ModuleName/Business/SomeFacadeTest.php::testSpecificMethod
Verbose output:
docker/sdk testing codecept run -c path/to/codeception.yml -vvv