Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Pro vývojáře / Testy a nástroje

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í):

ES_TEST_HOSTS=http://127.0.0.1:9200 tests/run.sh T2

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ší.

npm run test:e2e
E2E_BASE_URL=http://127.0.0.1:8000 npm run test:e2e
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:

  1. ladicí volání v PHP — bdump(, dumpe(, dd( v přidaných řádcích,
  2. :root v komponentním SCSS — tokeny patří na selektor komponenty, ne do globálu (allowlist má výjimky pro token vrstvy),
  3. porušení ráčny vrstev Manager → Service → Mapper → Repository,
  4. 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:

composer api-docs

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#

  1. Přidej atributy #[OA\…] k akci presenteru (vzor app/UI/Api/System/Presenters/AuthPresenter.php).
  2. Přidej cestu souboru do skriptu api-docs v composer.json — generátor čte jen vyjmenované soubory, ne celou složku.
  3. Spusť composer api-docs a 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#

  1. Vyber podkanál pod existující větví (delete.advert, invoicer.payment), nebo nový holý název.
  2. Zapiš ho do config/Shared/logging.neon jako true, nebo vědomě false.
  3. Spusť tests/Unit/Core/LoggingChannelRegistrationTest.php. Hlídá, že každý kanál použitý v app/ je v souboru a že holý kanál nekoliduje s větví.
  4. Smaž temp/cache. Strom je parametr DI kontejneru, bez smazání platí starý.

Postup: zapínám diagnostiku při hledání chyby#

  1. Do config/Shared/local.neon serveru dej pod parameters: klíč logging: all: true, nebo jen jednu větev (logging: channels: modal: trace: true).
  2. Smaž temp/cache. Nasazení není potřeba.
  3. Po nalezení chyby přepínač vrať a cache smaž znovu; all: true na 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

Nastavení cookies