Testy a nástroje#
Čím se v tomhle projektu ověřuje, že něco funguje — a čím se to ověřit nedá.
Testy#
tests/ — 1 092 souborů PHP (find tests -name "*.php" | wc -l):
| Sada | Souborů | Co obsahuje |
|---|---|---|
Unit/ |
650 | jednotkové — po modulech (Bazaar/, Eshop/, Invoicer/ …) i průřezové (Security/, Hydrators/, Filters/, Routers/, Cache/, Core/, System/); v Unit/JavaScript/ 4 testy JS (*.test.mjs) |
Integration/ |
33 | průchod víc vrstvami: testy nad databází (*Mysql*, Transactions/), Api, Redis a Elasticsearch |
Manual/ |
354 | ruční scénáře a harnessy pro CLI; z toho 224 v archivu Manual/_archive/ |
Fixtures/, Support/ |
52 | data a pomocné třídy |
E2E/ |
16 scénářů | scénáře Playwright v prohlížeči (*.spec.mjs) |
Jeden spouštěč: tests/run.sh#
Všechny úrovně testů pouští jeden skript:
tests/run.sh # T1 (výchozí)
tests/run.sh T0 # jen statika
tests/run.sh T4 # všechno
tests/run.sh T1 --coverage
Úrovně jsou kumulativní: T2 spustí i T0 a T1.
| Úroveň | Co spustí | Potřebuje |
|---|---|---|
T0 statika |
PHPStan, ráčna baseline PHPStanu, ráčna hranic vrstev (layer_boundaries_check.php), styl komentářů (comment_guard.php --style-only) |
nic |
T1 unit |
PHPUnit sada Unit a testy JS (npm run test:js) |
nic |
T2 DB |
testy nad databází (soubory s RUN_DB_TESTS) a Elasticsearch |
vývojovou databázi; před během se uloží její záloha (tests/db-snapshot.sh) |
T3 Api |
PHPUnit sada Integration proti běžícímu vývojovému serveru |
server na http://127.0.0.1:8000, Redis |
T4 crawl |
crawl frontu i administrace, harnessy opětovného uložení (resave), sondy přihlášení, platby a inzerátu, sitemapy | běžící vývojový server |
Každý krok vypíše jeden řádek ✓/✗, plný výstup chyby je v tests/.run-logs/<krok>.log
a na konci stojí TESTS-OK nebo TESTS-FAIL. Přeskočené testy se v řádku kroku sečtou
(„N přeskočeno“), aby zelený krok, který nic neměřil, nezůstal skrytý.
Zelený běh na čistém stromu je brána nasazení
Po úspěchu bez neuložených změn v app/, config/, tests/, www/index.php
a composer.lock zapíše skript do tests/.last-run řádek <commit> <úroveň>. Skript
nasazení nasadí jen commit, pro který tu stojí aspoň T1, viz
Nasazení kódu.
T2 zapisuje do databáze
Testy úrovně T2 a výš běží proti vývojové databázi a zapisují do ní. Nikdy je
nepouštěj proti ostrým datům a nikdy dva běhy souběžně: deadlock zruší transakci testu
a jeho zápisy v databázi zůstanou.
composer test (jen sada Unit) a composer test-all (celé PHPUnit) zůstávají pro
rychlý běh bez ostatních kroků.
Elasticsearch#
Testy tests/Integration/Elasticsearch*Test.php běží v T2 proti Elasticsearch z proměnné
ES_TEST_HOSTS (čárkami oddělený seznam, single-node bez zabezpečení):
Píšou jen do vlastních indexů s prefixem test_ a po sobě je mažou; indexy aplikace
(cms_*) ani její konfiguraci nepoužívají. Bez ES_TEST_HOSTS vezmou adresy z konfigurace
aplikace. Je-li Elasticsearch nedostupný, testy se přeskočí s důvodem; v souhrnu kroku je
uvidíš jako „N přeskočeno“, ne jako chybu.
Scénáře E2E (Playwright)#
tests/E2E/scenarios/*.spec.mjs procházejí web v prohlížeči: registrace, přihlášení,
vložení a správa inzerátu, objednávka v e-shopu, hledání, přepnutí jazyka, vykreslení všech
šablon a další.
| Co | Hodnota |
|---|---|
| adresa webu | E2E_BASE_URL, výchozí http://127.0.0.1:8000 |
| kdy případ selže | vedle vlastních ověření i při odpovědi 5xx, Tracy bluescreenu, nezachycené chybě JS a dialogu JS (sonda XSS) — hlídá to společná fixture tests/E2E/support/guard.mjs |
| souhrn | tabulka scénář × případ × výsledek v tests/.e2e-report/summary.md |
E2E nejsou součástí tests/run.sh; zapisují do databáze a mohou odesílat e-maily, takže je
pouštěj jen proti vývojové instanci s blokovaným odesíláním pošty.
Pokrytí kódu#
tests/run.sh T1 --coverage spustí sadu Unit s rozšířením pcov a uloží HTML report
do tests/.coverage/ (souhrn v tests/.coverage/summary.txt). Pokrytí po modulech spočítá
z clover XML skript:
php -d pcov.enabled=1 -d pcov.directory=app vendor/bin/phpunit --testsuite Unit --coverage-clover /tmp/clover.xml
python3 tests/coverage_by_module.py /tmp/clover.xml
Modulem je segment za app/UI/<vrstva>/, zbytek se počítá jako Core.
Archiv jednorázových harnessů#
tests/Manual/_archive/ drží skripty z uzavřených etap (struktura podsložek odpovídá
původnímu tests/Manual/<Modul>/). Neudržují se: cesty v nich po přesunu nesedí a před
spuštěním je musíš upravit. Aktivní sada je tests/Manual/Verify (spouští ji T4),
tests/Manual/Admin a nástroje v System/, Security/ a Comments/.
Strukturální testy#
Zvláštní kategorie: testy, které čtou zdrojáky a hlídají, že se na něco nezapomnělo. Zapomenutý guard v jedné nové metodě totiž nic neshodí — díra tam prostě zůstane.
| Test | Co hlídá |
|---|---|
ApiPermissionCoverageTest |
že guard oprávnění je v každé internalAction*() |
ApiOperationBoundaryTest |
že tělo každé internalAction*() je celé v operation() a žádná nevolá jinou internalAction*() |
UnitOfWorkResetBoundaryTest |
že clear(), detach() a znovuotevření EntityManageru volá v app/ jen UnitOfWorkReset (a infrastruktura transakcí) |
TranslationKeyLintTest |
bez databáze: názvy a obsah souborů překladů, shodné klíče ve všech jazycích a že každý statický klíč z Latte a PHP v katalogu existuje |
PermissionResolverTest |
že v SQL resolveru nejsou skupiny oprávnění |
FieldAccessCoverageTest |
že pole s podezřelým jménem je anotované, nebo má výjimku s důvodem; že Audience::Internal je jen na ose read |
OwnerWritableFieldsTest |
co smí vlastník zapsat externím save — u každé entity s owner grantem proti allowlistu, padá v obou směrech |
QueryPathPolicyTest |
že filtr, řazení ani sloupec neprivilegovaného volajícího nesáhne na skryté pole nebo osobní údaj |
SafeUnserializeTest |
že v app/ není unserialize() bez allowed_classes |
SqlMigrationPlannerTest |
výběr a pořadí migrací, základní stav, hlášení změněných a chybějících souborů; ve složce sql/ není rollback mezi migracemi |
ConditionallyUpdatableContractTest |
že pojmenovaná kontrolní metoda existuje |
ExternalReadonlyCoverageTest |
že server-počítaná pole nejdou zapsat zvenčí |
Tautologický guard nedokazuje nic
Kontrola porovnávající hodnotu samu se sebou projde vždycky. Každý guard ověř protipříkladem — tedy tím, že ukážeš, kdy neprojde.
Seed ve fixture bývá známka chybějícího kódu
Musí-li test data doplnit ručně, aby prošel, obvykle to znamená, že je nedoplňuje aplikace. Než seed přidáš, ověř, jestli nemají vzniknout v kódu.
Co testy nezachytí#
Tohle je nejdůležitější oddíl celé kapitoly.
| Chyba | Proč projde | Čím se chytí |
|---|---|---|
formulář nejde odeslat (mixed v onSuccess) |
build formuláře i Api endpoint pod ním projdou | POST nebo živý proklik |
setValidationScope([]) vyprázdní hodnoty |
testy jdou přes Api s ručním payloadem | živý proklik |
| CLI harness bez tokenu vrací prázdno | vrátí prázdno bez chyby | zkontrolovat, že data opravdu přišla |
| nepřekreslený snippet s chybovou hláškou | selhání je němé | proklik prohlížečem |
Živý proklik odhalí to, co harness nemůže
CLI běh obchází vrstvy, které se v prohlížeči uplatní — formuláře, snippety, signály, JavaScript. Než něco prohlásíš za hotové, proklikej to.
Harness v CLI potřebuje interní token
Bez něj běží jako guest, dostane Permission denied uvnitř mostu a vrátí
prázdno bez chyby. Test nad tím projde zeleně a netestuje nic. Řešení:
ApiBridge::setInternalAuthToken($jwtManager->generateToken(…)) — podrobně
Volání API.
Shodné skóre z Elasticsearch není důkaz pořadí
Když mají dokumenty stejné skóre, pořadí určuje interní pořadí indexu — a to se může změnit bez zásahu do kódu. Test nad takovým seznamem dokazuje jen to, že hledání něco vrátilo.
PHPStan#
phpstan.neon, úroveň 9. Baseline, stuby a zavaděč Doctrine leží ve složce phpstan/
(cesty v baseline jsou relativní k ní, tedy path: ../app/…); baseline je jediný soubor
s několika vědomě potlačenými položkami. Spouští ho tests/run.sh T0.
Mrtvá cesta v baseline zabrání spuštění ÚPLNĚ
PHPStan skončí chybou a analýza vůbec neproběhne — a protože výstup nevypadá jako nález, snadno se to splete s „prošlo to“. Po přejmenování nebo smazání souboru projdi baseline.
Baseline se smí jen zmenšovat
tests/Manual/System/phpstan_baseline_ratchet.php sečte potlačené chyby ve všech
phpstan/*.neon a porovná je s nejnižším dosaženým součtem uloženým lokálně
v phpstan/var/baseline-ratchet.txt. Když součet vzroste, commit se zastaví: novou
chybu oprav v kódu, ne v baseline. Po přegenerování baseline vždy znovu spusť PHPStan
a ověř [OK].
Pre-commit hook#
.githooks/pre-commit (aktivace: git config core.hooksPath .githooks) blokuje čtyři
věci:
- ladicí volání v PHP —
bdump(,dumpe(,dd(v přidaných řádcích, :rootv komponentním SCSS — tokeny patří na selektor komponenty, ne do globálu (allowlist má výjimky pro token vrstvy),- porušení ráčny vrstev
Manager → Service → Mapper → Repository, - růst baseline PHPStanu (ráčna výš; jen když existuje složka
phpstan/).
--no-verify obchází všechny čtyři
Je to určené pro výjimky, ne pro spěch. Co projde přes --no-verify, přijde na
řadu při code review — nebo za měsíc v produkci.
Sestavení assetů#
npm run build:admin # JS i CSS administrace
npm run build:admin:css
npm run build:admin:js
npm run watch:admin:css
Front téma se sestavuje ze své složky — viz Témata frontendu.
npx sass je jiná verze překladače než sass
Vyrobí jiný výstup. Používej příkazy z package.json.
Specifikace OpenAPI#
Strojový popis Api (www/api-docs.json, prohlížeč /api-docs/) se generuje z atributů
#[OA\…] v kódu knihovnou swagger-php:
Kořen specifikace (verze, servery, schémata zabezpečení, tagy) je
app/UI/Api/OpenApiSpec.php, jednotlivé operace jsou anotované přímo na akcích presenterů.
Třída je schválně abstract: Nette DI ji při procházení app/UI přeskočí (není to služba)
a swagger-php její atributy přečte reflexí i tak.
Postup: anotuji nový endpoint#
- Přidej atributy
#[OA\…]k akci presenteru (vzorapp/UI/Api/System/Presenters/AuthPresenter.php). - Přidej cestu souboru do skriptu
api-docsvcomposer.json— generátor čte jen vyjmenované soubory, ne celou složku. - Spusť
composer api-docsa zkontroluj operaci na/api-docs/.
Soubor mimo seznam ve skriptu se do specifikace nedostane
Generátor nehlásí chybu, operace prostě chybí. I soubory schémat, na která se
anotace odkazují (Result, JwtPayload), musí být v seznamu.
Skills#
skills/ — 19 postupů. Nejsou to doporučení, je to závazný popis, jak se
v tomhle projektu dělá konkrétní věc.
| Skupina | Pro co |
|---|---|
code-standard/ |
konvence po vrstvách (PHP, model, presenter, komponenta, formuláře, signály, Latte, Core) |
cs-admin-* |
administrace — grid, formulář, picker, audit formuláře |
cs-template-* |
šablony a témata |
cs-translations |
překlady |
cs-scss-structure |
SCSS |
cs-module-refactoring, cs-payment-gateway, cs-ui-audit |
ucelené postupy |
Před úkolem otevři příslušný skill — ne všechny
Každý cs-conv-* si o své závislosti řekne sám. Přehled je v
Konvence kódu.
Diagnostické logování#
Aplikační Debugger::log($zprava, 'kanál') neprojde do log/ automaticky. Kanály filtruje
App\Core\Logging\Logger, dekorátor kolem Tracy loggeru. Zapojuje ho jen
config/Shared/logging.neon, který přepisuje DI službu tracy.logger, takže platí na webu,
v cronu i v každém CLI skriptu, který si staví kontejner ze stejné konfigurace.
| Kanál | Rozhoduje |
|---|---|
Tracy úrovně (debug, info, warning, error, exception, critical) a auth |
projdou vždy, filtr je nevypne |
tečková cesta (save.error, eshop.import) |
strom logging.channels; první true/false na cestě platí i pro nevyjmenované potomky (save: true zapne i save.cokoli) |
| kanál, který ve stromu není | mlčí (fail-closed) |
logging.all: true |
zapne všechno najednou |
Kanál je zároveň název souboru (Tracy ho převede na malá písmena):
Debugger::log($e, 'eshop.import') píše do log/eshop.import.log.
Postup: přidávám nový kanál#
- Vyber podkanál pod existující větví (
delete.advert,invoicer.payment), nebo nový holý název. - Zapiš ho do
config/Shared/logging.neonjakotrue, nebo vědoměfalse. - Spusť
tests/Unit/Core/LoggingChannelRegistrationTest.php. Hlídá, že každý kanál použitý vapp/je v souboru a že holý kanál nekoliduje s větví. - Smaž
temp/cache. Strom je parametr DI kontejneru, bez smazání platí starý.
Postup: zapínám diagnostiku při hledání chyby#
- Do
config/Shared/local.neonserveru dej podparameters:klíčlogging: all: true, nebo jen jednu větev (logging: channels: modal: trace: true). - Smaž
temp/cache. Nasazení není potřeba. - Po nalezení chyby přepínač vrať a cache smaž znovu;
all: truena produkci zapisuje i šum, kvůli kterému byly kanály vypnuté.
Holý kanál, který je ve stromu větví, se nikdy nezapne
Debugger::log($x, 'delete') mlčí, protože delete je v konfiguraci pole
(delete.rollback, delete.advert…) a na cestě není žádné true. Přepsat větev na
delete: true by zase zrušilo nastavení podkanálů. Použij podkanál.
Kanál z konstanty test nevidí
Registrační test čte jen řetězcový literál ve druhém argumentu (a vynechává složky
Legacy/). Kanál předaný konstantou (self::LOG_CHANNEL) nebo proměnnou musíš do
logging.neon zapsat sám, jinak se jeho log tiše zahodí.
Úklid logů sahá jen do kořene log/
Cron log/purge-old (App\Core\Logging\LogRetention) maže výpisy Tracy a otáčí velké
*.log jen přímo v log/. Výpis, který chceš uchovat, přesuň do podadresáře (třeba
log/saved/). Otočení na .log.1 za běhu nic neztratí: Tracy soubor otevírá při
každém zápisu a další řádek založí nový.
🔴 Vývoj na vlastní kopii dat#
Nezkoušej na ostrých datech
Vyvíjej a testuj nad vlastní kopií databáze, ne nad daty běžícího webu. I tam dělej pokusy v transakci s návratem zpět, a než něco hromadně přepíšeš, spočítej si, kolika řádků se to týká.
Databáze, soubory a cache jsou tři různá místa
Vygenerované soubory (PDF, obrázky, feedy) leží na disku stroje, který je vytvořil, a cache v Redisu, ke kterému je ten stroj připojený. Když dva stroje míří na stejnou databázi, chybějící PDF nemusí znamenat chybu generátoru a vyprázdnění cache na jednom nevyprázdní druhý. Ověřuj tam, kde to má fungovat.
Kam sáhnout#
| Chci | Kde |
|---|---|
| všechny úrovně testů | tests/run.sh [T0–T4] [--coverage], viz Jeden spouštěč |
| jednotkové testy | tests/Unit/ |
| scénáře v prohlížeči | tests/E2E/scenarios/, npm run test:e2e |
| ruční harnessy | tests/Manual/ |
| konfiguraci statické analýzy | phpstan.neon + phpstan/ |
| pre-commit kontroly | .githooks/pre-commit |
| příkazy sestavení | package.json, composer.json |
| závazné postupy | skills/ |
| zapnout nebo zaregistrovat logovací kanál | config/Shared/logging.neon, viz Diagnostické logování |
| spuštění SQL migrací, náhled bez zápisu | php bin/migrate.php [--dry-run], viz Migrace SQL |
| rozjeté kopie frontových skriptů mezi šablonami | php bin/theme-js-sync.php [--canonical=…] [--apply], viz Synchronizace skriptů |
| které klíče nastavení se čtou | php bin/settings-usage.php, viz Sonda čtení nastavení |
Navazující kapitoly: Konvence kódu · Checklist nasazení · Volání API · RBAC v API