Test Harness
Since: 0.18.0
How to run and extend the phlix-server test suite.
Running the suite
All tests (unit + integration)
./vendor/bin/phpunitUnit tests only
./vendor/bin/phpunit --testsuite UnitIntegration tests only
./vendor/bin/phpunit --testsuite IntegrationA single test file
./vendor/bin/phpunit tests/Unit/Auth/JwtHandlerTest.php --testdoxA single test method
./vendor/bin/phpunit tests/Unit/Auth/JwtHandlerTest.php --filter testJwtSigning --testdoxWith testdox output (human-readable)
./vendor/bin/phpunit --testdox--testdox can never name a skipped test
A testdox run still counts its skips in Skipped: N but prints no skipped-details list, whatever phpunit.xml says: PHPUnit builds the default result printer on that path with its $displayDetailsOnSkippedTests argument hardcoded false. Drop --testdox whenever you care which tests skipped — see Skipped tests: compare names, never counts below. No workflow in phlix-server passes --testdox for this reason.
Skipped tests: compare names, never counts
This suite has legitimately environment-dependent skips — no MySQL, no Chromium, no mysqldump and no FFI each remove a different set — so the Skipped: N figure differs between machines and between CI jobs. CI reports single digits where a bare developer box reports a couple of hundred. The number is also ambiguous: a test started skipping, a conditionally-skipped test turned into a failure and a test disappeared all move it the same way, and two of them together can leave it unmoved. Never conclude anything from a skip count.
phpunit.xml therefore sets displayDetailsOnSkippedTests="true", so every run that loads it and uses PHPUnit's default result printer names each skipped test, and scripts/skipped-test-names.sh turns that output into a sorted, deduplicated, comm-ready set:
./vendor/bin/phpunit 2>&1 | scripts/skipped-test-names.sh > /tmp/before.txt
# ...make your change...
./vendor/bin/phpunit 2>&1 | scripts/skipped-test-names.sh > /tmp/after.txt
comm -3 /tmp/before.txt /tmp/after.txt # left column: left the skip set. right: joined it.Against a CI run — the script strips gh's job<TAB>step<TAB>timestamp prefix itself:
gh run view <run-id> --log | scripts/skipped-test-names.sh > /tmp/ci.txtOnly the name set goes to stdout, so comm and diff can consume it directly; the denominators (lines read, skips summarised, names declared, names extracted) always go to stderr. It refuses to hand over a set it cannot vouch for, and the exit code says why:
| exit | meaning |
|---|---|
0 | parsed a real PHPUnit run — stdout is the set (zero lines is a valid answer) |
2 | the input is not PHPUnit output at all |
3 | the numbers do not add up: a run printed no list, or PHPUnit's format drifted |
4 | skips were counted but never named — the attribute is off, or that invocation did not load phpunit.xml |
5 | the unnamed skips are testdox skips, which cannot be named at all — re-run without --testdox |
6 | the set could not be written in full, so stdout is truncated |
A non-zero exit means this input must not be compared. Two invocations are outside the mechanism by construction rather than by mistake: a --testdox run (above), and the assertion-escape-probe CI job, whose PHPUnit output is captured into a PHP variable and never echoed.
The canonical, exhaustive statement of all this — every exit path with the input class that reaches it, and the five numbered known limits of the arithmetic — is the header comment of scripts/skipped-test-names.sh itself, summarised in phlix-server's README.md under "Comparing skip sets between two runs". Read the header before trusting a diagnosis on a whole-workflow log, where several runs share one input.
Test structure
tests/
├── Unit/
│ ├── Auth/
│ │ ├── JwtHandlerTest.php
│ │ └── UserRepositoryTest.php
│ ├── Media/
│ │ ├── LibraryScannerTest.php
│ │ └── MetadataManagerTest.php
│ └── Server/
│ └── ApplicationTest.php
└── Integration/
├── Server/
│ └── Core/
│ └── ApplicationTest.php # Full boot smoke test
└── Media/
└── ItemRepositoryTest.phpUnit tests live under tests/Unit/ and mock all external dependencies (database, filesystem, HTTP). Integration tests under tests/Integration/ may use a real temporary database (see the test DB setup in phpunit.xml).
Coding standards
PHPStan (static analysis, level 9)
./vendor/bin/phpstan analyze src/ --level=9PHPCS (PSR-12 style)
./vendor/bin/phpcs --standard=PSR12 src/PHP syntax check (all files)
find src -name '*.php' -exec php -l {} \;Test database
Integration tests use a temporary database built from the real schema:
phpunit.xmlexportsDB_HOST=127.0.0.1,DB_DATABASE=phlix_test,DB_USER=root,DB_PASSWORD=root— these must match the GitHub Actionsservices: mysql:8.0container which setsMYSQL_ROOT_PASSWORD=rootandMYSQL_DATABASE=phlix_test.tests/Integration/Server/Core/ApplicationTest.php::writeTempDbConfig()reads those env vars and writes a temporaryconfig/database.phpbefore the boot smoke test.
If either side changes (env vars or workflow service), update both together. CI will fail with Access denied for user 'root'@... (using password: NO) if they diverge.
Coverage
Coverage is generated on demand:
./vendor/bin/phpunit --coverage-textConfiguration in phpunit.xml produces:
coverage.xml— Clover format ( consumed by CI coverage threshold)coverage-report/— HTML report directory
The CI workflow enforces a minimum statement coverage floor computed from the Clover XML. Current floor is MIN_COVERAGE=40. Bump it as coverage grows; never set it above current coverage or every PR turns red.
Adding a new test
- Place the file in the appropriate directory under
tests/Unit/ortests/Integration/. - Name it
<ClassName>Test.phpto match PSR-4 conventions. - Extend
PHPUnit\Framework\TestCase. - Mock external dependencies with
$this->createMock(Connection::class)for the MySQL connection:
$db = $this->createMock(Connection::class);
$db->method('query')->willReturn([['col' => 'val']]);See tests/Unit/Auth/JwtHandlerTest.php for a complete example.