Skip to content
V
For developers
Architecture, conventions, the core and security
For developers / Tests and tooling

Tests and tooling#

How this project verifies that something works β€” and what it cannot verify.

Tests#

tests/ β€” 314 files (find tests -name "*.php" | wc -l):

Suite Files What is in it
Unit/ 75 unit tests β€” Security/, Hydrators/, Filters/, Routers/, Cache/, Core/, UI/, Utils/
Integration/ 15 a pass across several layers
Manual/ 200 manual scenarios and CLI harnesses
Fixtures/, Support/ 23 data and helper classes
composer test        # Unit only
composer test-all    # everything

Structural tests#

A category of its own: tests that read the sources and guard that nothing was forgotten. A forgotten guard in one new method breaks nothing β€” the hole simply stays there.

Test What it guards
ApiPermissionCoverageTest that the permission guard is in every get*Data()
PermissionResolverTest that the resolver's SQL contains no privilege groups
FieldAccessCoverageTest that a field with a suspicious name is annotated or has an exception with a reason
ConditionallyUpdatableContractTest that the named check method exists
ExternalReadonlyCoverageTest that server-computed fields cannot be written from outside

A tautological guard proves nothing

A check comparing a value with itself always passes. Verify every guard with a counter-example β€” that is, by showing when it does not pass.

A seed in a fixture is usually a sign of missing code

If a test has to add data by hand to pass, it usually means the application does not add it. Before adding a seed, check whether it should not be created in the code.

What tests will not catch#

This is the most important section of the chapter.

Bug Why it passes What catches it
the form cannot be submitted (mixed in onSuccess) building the form and the Api endpoint beneath it both pass a POST or a live click-through
setValidationScope([]) empties the values tests go through the Api with a hand-written payload a live click-through
a CLI harness without a token returns emptiness it returns emptiness without an error checking that data really arrived
a snippet with the error message is not redrawn the failure is silent clicking through a browser

A live click-through reveals what a harness cannot

A CLI run bypasses the layers that apply in a browser β€” forms, snippets, signals, JavaScript. Before declaring something done, click through it.

A CLI harness needs an internal token

Without one it runs as guest, gets Permission denied inside the bridge and returns emptiness with no error. A test over that passes green and tests nothing. The fix: ApiBridge::setInternalAuthToken($jwtManager->generateToken(…)) β€” in detail Calling the API.

Equal Elasticsearch scores are no proof of ordering

When documents share a score, the order is decided by the index's internal order β€” and that can change with no code change at all. A test over such a list only proves the search returned something.

PHPStan#

phpstan.neon, level 9, plus 23 baseline files (ls phpstan-baseline*.neon).

A dead path in a baseline prevents the run ENTIRELY

PHPStan exits with an error and the analysis never happens β€” and because the output does not look like a finding, it is easily mistaken for "it passed". After renaming or deleting a file, go through the baselines.

The raw error count fluctuates even without code changes

Measured between 5,795 and 5,857 across runs. Accept differences by the list of findings, not by the number β€” otherwise you will be chasing noise.

The pre-commit hook#

.githooks/pre-commit (activate with git config core.hooksPath .githooks) blocks three things:

  1. debug calls in PHP β€” bdump(, dumpe(, dd( in added lines,
  2. :root in component SCSS β€” tokens belong on the component's selector, not in the global scope (the allowlist exempts the token layers),
  3. violations of the layer ratchet Manager β†’ Service β†’ Mapper β†’ Repository.

--no-verify bypasses all three

It exists for exceptions, not for haste. Whatever slips through --no-verify comes up at code review β€” or in production a month later.

Building assets#

npm run build:admin        # the administration's JS and CSS
npm run build:admin:css
npm run build:admin:js
npm run watch:admin:css

The front-end theme is built from its own folder β€” see Front-end themes.

npx sass is a different compiler version than sass

It produces a different output. Use the commands from package.json.

Skills#

skills/ β€” 19 procedures. They are not recommendations; they are the binding description of how a particular thing is done in this project.

Group For
code-standard/ conventions per layer (PHP, model, presenter, component, forms, signals, Latte, Core)
cs-admin-* the administration β€” grid, form, picker, form audit
cs-template-* templates and themes
cs-translations translations
cs-scss-structure SCSS
cs-module-refactoring, cs-payment-gateway, cs-ui-audit end-to-end procedures

Open the relevant skill before a task β€” not all of them

Every cs-conv-* asks for its own dependencies. The overview is in Code conventions.

πŸ”΄ Production and development share a database#

Every write is live

There is no "let me try it on dev first". Run experiments in a transaction with a rollback, and before rewriting anything in bulk, count how many rows it affects.

The files, by contrast, are split across machines

The database is shared, the generated files (PDFs, images, feeds) are not. A missing PDF on the development machine therefore need not mean a generator bug β€” it may simply sit somewhere else.

Redis and files are not shared between local and the dev server

Clearing the cache locally does not mean the cache on the dev server is clear. Verify where it is supposed to work.

Where to look#

I want Where
the unit tests tests/Unit/
the manual harnesses tests/Manual/
the static analysis configuration phpstan.neon + phpstan-baseline*.neon
the pre-commit checks .githooks/pre-commit
the build commands package.json, composer.json
the binding procedures skills/

Follow-up chapters: Code conventions Β· Deployment checklist Β· Calling the API Β· RBAC in the API