---
title: "Základný test case pre balíky — Testing for Laravel | Roundly"
description: "PackageTestCase: SQLite so zapnutými cudzími kľúčmi, migrácie podľa providera, výmena modelu pred bootom, reset bez down() a voliteľne ponechaná schéma."
url: https://roundly-consulting.com/sk/open-source/navody/testing-for-laravel/zakladny-test-case
language: sk
---

[Všetky balíky](https://roundly-consulting.com/sk/open-source.md)

[Testing for Laravel](https://roundly-consulting.com/sk/open-source/navody/testing-for-laravel.md)

# 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:

```php
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:

```php
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:

```php
// 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:

```php
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:

```php
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:

```php
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/:

```php
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:

```yaml
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 --parallel
```

MySQL, ak balík potrebuje vetvu test-mysql — performance\_schema zostáva zapnutá, pretože testy súbehu čítajú data\_lock\_waits:

```yaml
- 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: root
```

Ak 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 kryptomien](https://roundly-consulting.com/sk/podporte-nas.md)

Odoslaním daru súhlasíte s našimi [podmienkami prijímania darov](https://roundly-consulting.com/sk/podmienky-darov.md).

[Podporte našu open-source prácu (otvorí sa v novej karte)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Pridajte sa k nám na Patreone (otvorí sa v novej karte)](https://www.patreon.com/cw/roundly)

## 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.

[Ponuka do 48 hodín](https://roundly-consulting.com/sk/kontakt.md) [Prezrieť všetky balíky](https://roundly-consulting.com/sk/open-source/navody/testing-for-laravel.md)
