Základný test case pre balíky
PackageTestCase nahrádza takmer identický tests/TestCase.php, ktorý sa kopíruje do testov každého Laravel balíka. Rozširuje Orchestra Testbench, beží nad SQLite v pamäti so zapnutými obmedzeniami cudzích kľúčov, načítava migrácie podľa triedy providera a konfiguráciu aj výmenu modelov aplikuje ešte pred bootom providerov:
use RoundlyConsulting\Testing\PackageTestCase;
abstract class TestCase extends PackageTestCase
{
protected function packageProviders(): array
{
return [CryptoServiceProvider::class, PasskeysServiceProvider::class];
}
protected function migrationSources(): array
{
return [PasskeysServiceProvider::class];
}
protected function configBeforeBoot(): array
{
return ['passkeys.rp.id' => 'example.test'];
}
}Svoj TestCase deklarujte ako abstract, nikdy nie final: Pest pre každý testovací súbor vygeneruje triedu, ktorá ho rozširuje, takže final TestCase skončí fatálnou chybou PHP hneď pri načítaní testov.
Metódy na prepísanie
| Metóda | Predvolené | Účel |
|---|---|---|
packageProviders() | abstract | Testované service providery v poradí registrácie. Jediná metóda, ktorú musíte implementovať. |
migrationSources() | [] | Triedy providerov (reflexiou preložené na ich adresár database/migrations) a/alebo priame adresáre s migráciami. |
configBeforeBoot() | [] | Konfigurácia aplikovaná v defineEnvironment() pred bootom providerov — miesto na prednastavenie kľúča či výmenu modelu. |
keepsMigratedSchema() | false | Vráťte true a worker zmigruje schému raz pre každú signatúru a medzi testami resetuje len dáta — pozrite Ponechanie zmigrovanej schémy nižšie. |
schemaSignatureExtras() | [] | Čokoľvek ďalšie, čo vaše migrácie čítajú a signatúra to nevidí — premenná prostredia, statický príznak. Uplatní sa len pri ponechanej schéme. |
connectionAvailable($connection) | — | Verejný pomocník: či je pomenované pripojenie dostupné. Podmieňte ním asercie na reálnej databáze. |
Migrácie podľa triedy providera
migrationSources() prijíma triedy providerov aj priame adresáre. Provider sa reflexiou preloží na adresár database/migrations svojho balíka (hľadá sa smerom nahor od súboru providera, najďalej po koreň balíka s jeho composer.json), takže rovnaké volanie funguje pre balík pripojený symlinkom aj pre inštaláciu z VCS a provider bez vlastných migrácií hlasno zlyhá namiesto toho, aby načítal migrácie hostiteľskej aplikácie. Pomenovať jednotlivý súbor migrácie zámerne nejde — presne táto chyba rozbila testy piatich balíkov. Zdroj, ktorý nie je triedou ani existujúcim adresárom, hlasno zlyhá a samotná kontrola nepridá testu žiadnu aserciu:
protected function migrationSources(): array
{
return [
PasskeysServiceProvider::class, // resolved to its database/migrations by reflection
__DIR__.'/Fixtures/database/migrations', // a literal directory works too
];
}Výmena modelu pred bootom
Providery pri boote vešajú observery a listenery na nakonfigurovanú triedu modelu, preto musí výmena prebehnúť ešte pred bootom. Vráťte ju z configBeforeBoot() alebo zavolajte swapModel() v defineEnvironment() ešte pred odovzdaním riadenia rodičovi, ktorý aplikuje všetky zaregistrované výmeny:
// Either return the swap from the before-boot config window…
protected function configBeforeBoot(): array
{
return ['media.media_model' => CustomMedia::class];
}
// …or register it in defineEnvironment() before the parent applies every swap.
protected function defineEnvironment($app): void
{
$this->swapModel('media.media_model', CustomMedia::class);
parent::defineEnvironment($app);
}Reset stavu bez down()
Roundly balíky migrujú len dopredu a metódu down() nemajú. Testbench bežne resetuje stav príkazom migrate:rollback po každom teste — a keďže migrátor Laravelu chýbajúce down() ticho preskočí, na reálnej databáze tento rollback nespraví nič. Tabuľky prežijú a ďalší test zlyhá na duplicitnej tabuľke, pričom chybová hláška ukazuje na nevinnú migráciu.
PackageTestCase preto reálnu databázu resetuje zmazaním všetkých tabuliek a opätovnou migráciou, potom uzavrie všetky pripojenia, ktoré test otvoril — takže Postgres sada nenecháva otvorený jeden backend na každý test. Na SQLite :memory: je resetom samotné pripojenie, preto sa od verzie 1.6 vynecháva aj rollback, ktorý by Testbench spustil pre každú položku migrationSources() — mazal len riadky tabuľky migrations v databáze, ktorá vzápätí aj tak zanikne, a zaberal zhruba pätinu času SQLite sady (tickets-for-laravel: z 83 s na 60 s). Pri migrátore, ktorý si test pridá sám (loadMigrationsFrom(), loadLaravelMigrations()), rollback prebehne ako doteraz. Nič netreba konfigurovať, stačí rozšíriť základný test case:
abstract class TestCase extends PackageTestCase
{
protected function packageProviders(): array { return [WalletServiceProvider::class]; }
protected function migrationSources(): array { return [WalletServiceProvider::class]; }
}- Sady používajúce RefreshDatabase (alebo LazilyRefreshDatabase) zostávajú nedotknuté — reset si riadia samy.
- Aj sady bez migrácií sa resetujú: tabuľka, ktorú si fixture v jednom teste vytvorí ručne, neprežije do ďalšieho.
- Prečo nie RefreshDatabase ako predvolba: obalí každý test transakciou a pridá tak jednu úroveň — záznamníky zámkov však overujú práve transactionDepth. Zmazanie je čisté DDL a žiadnu transakciu neotvára.
Vlastný TestCase
Ak PackageTestCase nerozširujete, zavolajte DriverMatrix::configure($app) vo vlastnom defineEnvironment() a reset zmazaním tabuliek si zopakujte sami — bez neho vetva s reálnou databázou fungovať nebude.
Ponechanie zmigrovanej schémy (voliteľné)
Od verzie 1.6. Predvolene každý test zmigruje všetky zdroje a po ňom sa zmažú všetky tabuľky. Pri mnohých zdrojoch migrácií alebo na reálnej databáze na to padne väčšina času sady: tickets-for-laravel spúšťal 1061 migrácií nad 9 zdrojmi a migrácia pred každým testom tvorila asi 90 % jeho MySQL vetvy. Keď ponechanie zapnete, worker zmigruje schému raz pre každú signatúru a potom resetuje už len dáta:
abstract class TestCase extends PackageTestCase
{
protected function keepsMigratedSchema(): bool
{
return true;
}
// Only if a migration reads something the signature cannot see:
protected function schemaSignatureExtras(): array
{
return ['tenancy' => env('PKG_TENANCY')];
}
}Kedy ho zapnúť
- Veľa zdrojov migrácií — balík, ktorého testy migrujú vlastnú schému a k tomu schémy viacerých závislostí (tickets-for-laravel: 9 zdrojov, asi 35 tabuliek).
- Vetvy s reálnou databázou (test-pgsql, test-mysql) — migrácia tam stojí DDL volania pre každú tabuľku pri každom teste. MySQL vetva tickets-for-laravel sa skrátila z 32 min na 1 min 40 s a Postgres vetva z 5 min 10 s na 1 min 26 s, pričom všetky testy súbehu bežia naďalej.
- Neoplatí sa pri sade s jedným malým zdrojom migrácií, ktorá beží len na SQLite — predvolený reset je tam lacný už teraz.
Čo každý test naďalej dostane
| Záruka | Ako |
|---|---|
| Začína na prázdnych tabuľkách | Po každom teste sa vyprázdni každá tabuľka, do ktorej test zapisoval — na MySQL a SQLite cez DELETE, na Postgrese cez TRUNCATE … CASCADE — s ohľadom na cudzie kľúče. |
| Id ako po čerstvej migrácii | Každé počítadlo kľúčov sa vráti tam, kde ho migrácia nechala — MySQL AUTO_INCREMENT, sekvencie Postgresu (setval vrátane startingValue()), SQLite sqlite_sequence —, a to aj počítadlá, ktoré posunul vrátený insert. |
| Bez obaľujúcej transakcie | transactionLevel() testu patrí len jemu, takže transactionDepth v LockRecorderi aj forknuté súbežné procesy sa správajú presne ako pri resete zmazaním. |
| Zmenená schéma sa nikdy nepoužije znova | Kontrola katalógu po každom teste porovná schému so snímkou po migrácii; pri akomkoľvek rozdiele sa schéma zahodí a ďalší test migruje od začiatku. |
| Seedované riadky prežijú | Ak migrácie zapisujú riadky, harness ich po prvej migrácii zistí a pre túto schému ponechá reset zmazaním po každom teste — žiadny test o svoje seedované riadky nepríde. |
| Databáza skončí prázdna | Keď worker skončí, zmaže svoje tabuľky, ako ich nechával reset zmazaním; čerstvá migrácia navyše najprv odstráni zvyšky po prerušenom behu. |
Signatúra schémy a podporované enginy
Signatúru tvorí pripojenie (driver, databáza workera, voľby), adresáre migrácií v poradí, načítané providery v poradí, celý konfiguračný repozitár, statické predvoľby schema buildera (defaultStringLength, defaultMorphKeyType, defaultTimePrecision) a schemaSignatureExtras(). Objekty v konfigurácii sa porovnávajú podľa triedy — enum podľa prípadu —, takže dve closures vyzerajú rovnako. Test s inou signatúrou — vetva s kľúčmi uuid či ulid, vymenený model, iná konfigurácia — ponechanú schému zahodí a zmigruje si vlastnú. Worker drží naraz jednu schému.
Podporované sú MySQL, MariaDB, PostgreSQL a SQLite v pamäti (:memory:). SQLite databáza v súbore alebo so zdieľanou cache, SQL Server, pripojenie s prefixom tabuliek a každá sada s RefreshDatabase zostávajú pri resete zmazaním po každom teste — zapnutie potom nemá žiadny účinok.
Test case bez migrationSources() nemá čo ponechať, a preto tiež zostáva pri resete zmazaním po každom teste — tabuľka, ktorú si test vytvorí ručne, pred ďalším testom zmizne. Opravené vo verzii 1.6.1: 1.6.0 tam každý databázový test zhodila chybou „no such table: migrations“ — na 1.6.0 v takom test case prepíšte keepsMigratedSchema() tak, aby vracala false.
Čo pokrýva kontrola katalógu
Obmedzená na vlastnú schému pripojenia (Postgres) alebo jeho databázu (MySQL):
- Každý engine — každá tabuľka a jej stĺpce (poradie, typ, nullability, predvolená hodnota), indexy, primárne, unikátne, cudzie a check obmedzenia, pohľady, triggery a každý riadok tabuľky migrations.
- PostgreSQL — navyše druh každej relácie, identity a generované stĺpce, definície pohľadov a pri každej sekvencii typ, hranice, začiatok a krok.
- MySQL — navyše engine a collation každej tabuľky a collation a extra každého stĺpca.
- SQLite — DDL každého objektu v sqlite_master a sqlite_temp_master (vrátane create temp table), prepínač foreign_keys a pripojené databázy.
Nepokrýva — test, ktorý niečo z toho zmení, to musí sám vrátiť, presne ako pri resete zmazaním, ktorý to tiež neodstráni: typy Postgresu (enumy, domény), funkcie, rozšírenia a iné schémy; uložené procedúry, funkcie, udalosti a oprávnenia v MySQL; a pri SQLite v pamäti zvyšný stav relácie zdieľaného pripojenia — ostatné pragmy (recursive_triggers, case_sensitive_like, …) a funkcie zaregistrované na PDO. Na SQLite je ponechaná databáza jedno PDO, ktoré dostane každý test; na reálnej databáze sa relácia každého testu po ňom zatvorí, takže stav relácie tam uniknúť nemôže.
Riziká a pravidlá
- Forknuté procesy musia skončiť cez SIGKILL. Súbežný proces vytvorený cez pcntl_fork() zdieľa databázové relácie rodiča — reláciu testu aj ponechanej schémy. Proces, ktorý zavolá exit() alebo sa vráti, spustí deštruktory a tie tieto relácie na serveri zatvoria. Po zapísaní výsledku ho ukončite cez posix_kill(posix_getpid(), SIGKILL) a dajte mu vlastné pripojenie (DriverMatrix::connectionConfig() pod iným názvom). Ak proces ponechanú reláciu aj tak zatvorí, ďalší reset odmietne pokračovať a ďalší test migruje od začiatku — nikdy nevznikne napoly resetovaná schéma.
- Stav, ktorý migrácia číta a signatúra ho nevidí — premenná prostredia, statický príznak, stav objektu —, patrí do schemaSignatureExtras(). Inak dva testy, ktoré by migrovali rozdielne, zdieľajú jednu schému a kontrola katalógu to nerozozná: porovnáva s tou prvou.
- Iné test cases nad tou istou reálnou databázou: test, ktorý schému neponecháva, ponechanú schému pred svojou migráciou zahodí, takže zmiešané sady fungujú — pokiaľ každý test case prechádza cez PackageTestCase::defineDatabaseMigrations(). Test case, ktorý ju nad tou istou databázou workera obíde, uvidí ponechané tabuľky.
- Seedujúce migrácie nezrýchlite — zostávajú pri resete zmazaním. Ak o zrýchlenie stojíte, seedujte v teste alebo v defineDatabaseSeeders().
Úskalia
Prvé dve sú tiché: sada zostane zelená aj správna, len migruje pred každým testom, akoby ponechanie nikdy nezapli — príznakom je vetva s reálnou databázou, ktorá nie je o nič rýchlejšia. Tretie zafarbí test na červeno.
Náhodná hodnota konfigurácie v každom teste. Signatúra obsahuje celú konfiguráciu, takže configBeforeBoot(), ktorá pre každý test vygeneruje nový app.key — alebo akúkoľvek inú náhodnú hodnotu —, dá každému testu novú signatúru a nič sa nikdy neponechá: advertisements-for-laravel migroval 443-krát v 443 testoch. Hodnotu vygenerujte raz na proces:
private static ?string $appKey = null;
protected function configBeforeBoot(): array
{
return ['app.key' => self::$appKey ??= 'base64:'.base64_encode(random_bytes(32))];
}Tabuľky pre fixtures mimo migrationSources(). Prepísané defineDatabaseMigrations(), ktoré po parent::defineDatabaseMigrations() vytvorí tabuľky pre fixtures (alebo migruje ďalšie), či atribút #[DefineDatabase] zmenia schému až po snímke, takže kontrola katalógu zistí odchýlku a po každom teste schému zahodí. Tabuľky pre fixtures presuňte do vlastného adresára migrácií a uveďte ho v migrationSources() — addresses-for-laravel takto presunul svoju tabuľku test_models do tests/database/migrations/:
protected function migrationSources(): array
{
return [AddressesServiceProvider::class, __DIR__.'/database/migrations'];
}Event::fake() spolu s assertNothingDispatched(). Taký test môže prejsť len preto, že migrácia pred každým testom nabootovala modely balíka ešte pred Event::fake(). S ponechanou schémou ich nabootujú až vlastné dopyty testu, keď je fake aktívny, a fake zaznamená interné udalosti Eloquentu eloquent.booting a eloquent.booted — assertNothingDispatched() potom zlyhá na udalostiach, ktoré testovaný kód nikdy nevyvolal. Fakeujte len udalosti, o ktoré vám ide (Event::fake([OrderPlaced::class])), alebo okno zúžte: fake tesne pred testovanou akciou, overenie hneď po nej.
CI pre vetvy s reálnou databázou
Ponechaná schéma odstráni migráciu pred každým testom; ďalším nákladom je trvanlivosť zápisu samotného enginu. Engine spustite cez docker run ako prvý krok úlohy — nabehne, kým sa inštaluje PHP — na tmpfs s vypnutou trvanlivosťou zápisu. Plánovač, úrovne izolácie, zámky aj limity zostávajú rovnaké; nič, čo testovacia vetva dokazuje, nezávisí od toho, či databáza prežije výpadok prúdu:
test-pgsql:
runs-on: ubuntu-latest
name: P8.4 - pgsql real-engine
steps:
- name: Start postgres
run: >-
docker run -d --name postgres --tmpfs /var/lib/postgresql/data
-e POSTGRES_DB=testing -e POSTGRES_USER=testing -e POSTGRES_PASSWORD=secret
-p 5432:5432 postgres:16
-c fsync=off -c synchronous_commit=off -c full_page_writes=off
# … checkout, setup-php (pgsql, pdo_pgsql, pcntl; coverage: none), composer update …
# The image's init server listens on the socket only, so a TCP answer means the real one is up.
- name: Wait for postgres
run: |
for _ in $(seq 1 90); do
docker exec postgres psql -h 127.0.0.1 -U testing -d testing -c 'select 1' >/dev/null 2>&1 && exit 0
sleep 1
done
docker logs postgres
exit 1
- name: Execute tests against postgres
env:
TESTING_DB_DRIVER: pgsql
TESTING_DB_HOST: 127.0.0.1
TESTING_DB_PORT: 5432
TESTING_DB_DATABASE: testing
TESTING_DB_USERNAME: testing
TESTING_DB_PASSWORD: secret
run: vendor/bin/pest --ci --parallelMySQL, ak balík potrebuje vetvu test-mysql — performance_schema zostáva zapnutá, pretože testy súbehu čítajú data_lock_waits:
- name: Start mysql
run: >-
docker run -d --name mysql --tmpfs /var/lib/mysql
-e MYSQL_DATABASE=testing -e MYSQL_ROOT_PASSWORD=secret
-p 3306:3306 mysql:8
--skip-log-bin --sync-binlog=0 --innodb-flush-log-at-trx-commit=0 --innodb-doublewrite=OFF
# wait: docker exec mysql mysql -h 127.0.0.1 -uroot -psecret -e 'select 1' testing
# env: TESTING_DB_DRIVER: mysql, TESTING_DB_PORT: 3306, TESTING_DB_USERNAME: rootAk sada testuje správanie binárneho logu (v testoch hľadajte log_bin alebo binlog), vynechajte --skip-log-bin — StatementBinlogTest v lifecycle sa bez binárneho logu ticho preskočí — a --sync-binlog=0 ponechajte. Porovnajte počet preskočených testov na reálnej databáze pred prechodom a po ňom; práve tak sa na to prišlo.
--parallel dá každému workeru vlastnú databázu (<database>_<token>), takže každý drží vlastnú schému. Vlastná pgsql vetva balíka pridáva po testoch ešte jeden krok: zlyhá, ak v databáze zostane tabuľka — a práve to dokazuje, že worker pri skončení svoje tabuľky zmaže.
Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.