Testování pravidel

RuleTester a formát fixtur: soubory .code, .expected a .violations, volby a cílová verze PHP v hlavičce fixtury, a co všechno tester ohlídá za vás.

Fixtury

Pravidlo se testuje nad adresářem fixtur, jedním na pravidlo. Fixtura (fixture) je až trojice souborů se stejným jménem:

  • basic.code je kód před opravou;
  • basic.expected je kód po opravě; když soubor chybí, pravidlo nesmí kód změnit;
  • basic.violations jsou očekávaná hlášení, každé na svém řádku ve tvaru řádek a zpráva:
3: The message of an exception must end with a period
6: The message of an exception must end with a period

Fixtura může v komentářích na svém začátku nést volby pravidla jako JSON, verzi PHP, pro kterou je psaná, nejdelší řádek, se kterým se měří, a slovo risky, které pravidlu dovolí i opravy měnící chování kódu:

<?php
// {"functions": ["dd", "dump"]}
// php 8.4
// lineLength 80
// risky

Bez uvedené délky se měří s řádky do 120 znaků, jaké mají standardy. Bez uvedené verze se pravidlo testuje na verzi, o kterou si řeklo v requires, jinak na PHP 8.0; verze předaná testeru parametrem $phpVersion má přednost před hlavičkou. Nikdy ne na verzi interpretu, který testy spouští, protože verdikt pravidla má být na počítači nezávislý. Bez risky se rizikový výskyt jen ohlásí a soubor .expected ho musí nechat, jak byl; fixtura s risky je zároveň důkaz pro referenci, že pravidlo takové opravy má.

Co deklarují jmenné prostory mimo fixturu, řeknou hlavičky ve tvaru položek příkazu use, stejně jako klíče namespaces a nameResolution v konfiguraci:

<?php
// namespacedFunctions App\helper, App\Utils\{format}
// namespacedConstants App\LIMIT
// nameResolution certain

Pravidlo s requiresTypes dostane typy kódu z PHPStanu projektu, ve kterém testy běží, spočítané nad fixturou a nad deklaracemi z podadresáře stubs/ v adresáři fixtur. Pravidlo, kterému typy jen pomáhají, je dostane tam, kde adresář stubs/ je.

Ukázky, které vypíše dresscode explain na terminál i do dokumentu s --output, mají stejný tvar jako fixtury, ale leží v adresáři examples/ vedle composer.json balíčku, ze kterého pravidlo pochází, v podadresáři se jménem pravidla bez vendora, třeba examples/exception-message-period/. Balíček je tak má s sebou i po instalaci přes Composer, kde adresář tests/ chybí, a platí to pro rozšíření stejně jako pro vestavěná pravidla. Ukázka má být krátká a čitelná. Pusťte ji v testech jako každou jinou fixturu, RuleTester::run(ExceptionMessagePeriodRule::class, __DIR__ . '/../examples/exception-message-period'), a ukázka nemůže zastarat.

Fixtura není ukázka do dokumentace: má být ošklivá a plná hraničních případů. Komentář uprostřed konstrukce, konstrukce na jednom řádku i přes tři, prázdné tělo, interpolovaný řetězec, alternativní syntaxe. Právě na těchhle místech se pravidla lámou.

RuleTester

use DressCode\Testing\RuleTester;
use Tester\Assert;

Assert::noError(fn() => RuleTester::run(ExceptionMessagePeriodRule::class, __DIR__ . '/fixtures/exception-message-period'));

Metoda run() projde všechny soubory *.code v adresáři a vrátí jejich počet; adresář bez nich je selhání, ne prázdný úspěch. Selhání je výjimka DressCode\Testing\TestFailure se jménem fixtury a s diffem, takže ji srozumitelně ukáže každý testovací framework. Nette Tester chce v každém testu aspoň jednu aserci, proto je volání v ukázce zabalené do Assert::noError(); z PHPUnit je to jedno volání v testovací metodě.

U každé fixtury tester ověří:

  • výstup se rovná souboru .expected (nebo vstupu, když .expected není);
  • hlášení se rovnají souboru .violations, řádek po řádku a ve stejném pořadí;
  • idempotenci: pravidlo nad vlastním výstupem už nic nezmění;
  • komentáře: ve výstupu jsou všechny komentáře ze vstupu, pokud pravidlo nemá modifiesComments;
  • potlačení: s komentářem dresscode:ignore-file v hlavičce pravidlo nic neohlásí ani nezmění;
  • kontrakt oprav: žádná změna stromu bez hlášení, které prošlo;
  • strom: každý uzel má správného rodiče, což se rozbije při chybném vkládání.

Pravidlo, které volby dostává jinak než z konfigurace (třeba se závislostí v konstruktoru), předáte místo třídy jako továrnu fn(array $options): Rule. Továrna dostane volby z hlavičky fixtury tak, jak jsou zapsané v JSONu, a bez hlavičky prázdné pole. U třídy tester volby prožene schématem pravidla, které je zvaliduje a doplní výchozí hodnoty; u továrny to neudělá. Výchozí hodnoty proto doplní továrna sama, třeba $options + ['functions' => ['var_dump', 'print_r']], a teprve pak zavolá configure().

Pro zvláštní případy jsou tu menší nástroje: RuleTester::runFixture() spustí jedinou fixturu, RuleTester::check() ověří instanci pravidla nad řetězcem bez souborů (hodí se na rychlou reprodukci) a RuleTester::collectViolations() vrátí hlášení nad fixturou přímo ve tvaru souboru .violations a collectOutput() výstup ve tvaru souboru .expected, takže si je můžete nechat zapsat a jen zkontrolovat diff, místo abyste je opisovali ručně. run(), runFixture() i obě collect*() přijmou jako poslední parametr $phpVersion.

Zkouška v reálném provozu

Test pravidla ověří pravidlo samotné. Jak se chová ve společnosti ostatních, ukáže až běh nad skutečným kódem. Přepínač dresscode check --strict-rules udělá z každého porušeného kontraktu chybu místo varování a --jobs 1 nechá všechno běžet v jediném procesu, kde se pohodlně ladí.

Než pravidlo zveřejníte, pusťte fix nad větším cizím kódem, třeba nad adresářem vendor/, a projděte si diff. Idempotenci a komentáře ohlídá tester, vkus ne.