← Development workflow

7.3 Testing infrastructure

KMP uses PHPUnit for PHP behavior, Jest/jsdom for frontend units, and Playwright BDD for browser-visible flows. Tests should prove the boundary that matters: policy and persistence in PHP, local UI behavior in Jest, and integrated host/tenant/queue/Turbo behavior in Playwright.

Test lanes

Lane Primary scope Command from app/
Core unit models, behaviors, services, KMP primitives, application wiring vendor/bin/phpunit --testsuite core-unit
Core feature controllers, commands, middleware, views, HTTP behavior vendor/bin/phpunit --testsuite core-feature
Plugins plugin-owned tests plus shared plugin harness vendor/bin/phpunit --testsuite plugins
All PHP complete PHP regression vendor/bin/phpunit --testsuite all or composer test
JavaScript Stimulus/utilities in jsdom npm run test:js
UI smoke login/workflow smoke npm run test:ui:smoke
UI journey curated cross-domain journey npm run test:ui:journey
UI platform destructive platform tenant provisioning npm run test:ui:platform-provisioning
UI UAT full Playwright BDD/spec set npm run test:ui

Run one Playwright lane at a time. Lanes share the local Docker application, PostgreSQL instance, worker, scheduler, and Mailpit.

PHPUnit organization

app/phpunit.xml.dist defines four suites:

Put a test in the suite that owns the behavior, not whichever directory happens to make it run fastest.

Base classes

Always call parent setup/teardown in subclasses. Use disableTransactions() only when the feature cannot execute inside the standard wrapper. Use reseedDatabase() only for destructive tests that genuinely require a full seed reset; it is expensive and changes process-wide state.

Test database bootstrap

tests/bootstrap.php aliases the test datasource to default and disables runtime HTTP tenancy for the ordinary suite. On PostgreSQL it applies current core and loaded-plugin migrations first, then loads the data-only tests/pg_seed.sql and required workflow/reference configuration. The reset script creates the application and platform test databases before the suite.

This means ordinary table/controller tests run against one seeded tenant-shaped database. Tenancy behavior is tested separately through middleware, TenantConnectionManager, platform services/commands, and multi-host Playwright scenarios. Do not mistake KMP_TENANCY_ENABLED=false in PHPUnit bootstrap for the production request contract.

Platform-focused unit/feature tests configure and restore a platform connection explicitly, often with an isolated test schema or mock. A test that changes ConnectionManager, FactoryLocator, TenantContext, global workflow registries, behavior suppression, Configure, environment variables, or static logs must restore it in tearDown()/finally.

Stable seed data

Use named constants from BaseTestCase for the small set of supported stable IDs. Query by a unique semantic property when no constant exists. The canonical reference is app/tests/TestDataReference.md.

Avoid:

Use a unique per-test token for records created by additive browser fixtures. For destructive bulk mutation, reseed intentionally rather than relying on test order.

PHP test patterns

Authorization

Test both the controller boundary and policy logic where appropriate. Include an allowed user, denied user, out-of-branch resource, collection scope, and direct request. UI visibility is not an authorization assertion.

Workflows and queues

Exercise the user/domain trigger that starts the workflow. Assert durable state, then drain or flush through the project test helper before checking queued mail or side effects. Keep negative assertions scoped to the fixture so unrelated seeded jobs cannot make the test flaky.

Multi-tenancy

At minimum, sensitive infrastructure tests should execute tenant A, tenant B, then tenant A in one process and assert:

Middleware tests cover unhealthy platform, unknown host, inactive tenant, schema-behind tenant, platform-admin host, and cleanup on exceptions.

Commands and migrations

Commands need exit-code, dry-run, validation, error-scrubbing, and retry/idempotent coverage. Fleet commands also need selector/status rules, suspended-tenant behavior, advisory lock contention, partial failure, and final catalog verification. Use actual PostgreSQL coverage when SQL or locks are PostgreSQL-specific.

Jest/jsdom

Jest tests live under tests/js, mirroring the frontend source where practical. tests/js/setup.js provides shared DOM/browser mocks. Load a Stimulus controller through the same global registration pattern used by the app, connect it to a minimal semantic fixture, perform keyboard and pointer interactions, and assert state/announcements/cleanup.

npm run test:js
npm run test:js:watch
npm run test:js:coverage

Mock browser APIs consistently and restore globals/timers/listeners after each test. A jsdom assertion does not replace a browser check for Bootstrap focus, Turbo Frames, layout, downloads, camera/file APIs, or accessibility.

Playwright BDD

Editable feature files are under tests/ui/bdd; shared and domain step files sit beside them. npx bddgen test produces tests/ui/gen, which is generated output and should not be hand-edited. Reports/results are generated too.

Playwright lane orchestration resets the development database once, then feature fixtures add uniquely named records. Use:

The platform-provisioning lane enables destructive coverage deliberately. Do not mix it into a normal parallel lane or target a non-local environment.

Standard verifier

bash bin/verify.sh

It runs:

  1. core-unit, core-feature, and plugins with Xdebug disabled;
  2. the skipped-test budget;
  3. seed snapshot contracts;
  4. Jest;
  5. the Vite development build;
  6. PHPCS on changed PHP (syntax-only for changed embedded Queue code);
  7. the Azure deployment runtime contract; and
  8. PHPStan with the recorded baseline handling.

For a compact command-oriented companion, see the test lane quick reference.

It does not run Playwright by default. Coverage and mutation are opt-in:

bash bin/verify.sh --with-coverage=security
bash bin/verify.sh --with-coverage=all
bash bin/verify.sh --with-mutation=security

Diagnosing failures

Report exact commands and outcomes in the handoff. Never convert a real failure to a skip merely to stay inside the skip budget.