Skip to content

Testing and Quality Checks

QUIQQER packages should provide repeatable local checks for behavior, static analysis, and PHP code style. A package should be testable from its own repository without running the Core test suites.

The recommended toolchain is:

  • PHPUnit for unit and integration tests
  • PHPStan at level 8 for static analysis
  • PHP_CodeSniffer with PSR-12 for PHP code style
  • PHIVE for package-local tool installations
  • Composer scripts as stable entry points for developers and CI

AI-Assisted Package Quality

QUIQQER Core provides the MCP skill quiqqer_package_quality_upgrade. Load this skill before asking an AI client to modernize an existing package or start a new package with the current quality standards:

text
quiqqer_mcp_skill_get name=quiqqer_package_quality_upgrade

For an existing package, the skill guides a controlled quality upgrade while preserving behavior. For a new package, it provides the target structure and quality baseline from the beginning. The workflow includes repository-state checks, PHIVE tools, PHPStan level 8, DBAL and portable database.xml changes, optional-dependency stubs, PHPUnit integration coverage, package metadata, and final validation.

Use the skill for a full modernization or when establishing a new package. For a focused change, apply only the relevant checks from this page. Skill instructions guide the AI workflow; the package-local Composer commands remain the authoritative verification steps.

See MCP Skills for skill discovery and integration details.

text
my-package/
├─ .phive/
│  └─ phars.xml
├─ tests/
│  ├─ unit/
│  ├─ integration/
│  ├─ phpunit-bootstrap.php
│  └─ phpstan-bootstrap.php
├─ phpcs.xml.dist
├─ phpstan.dist.neon
├─ phpunit.dist.xml
└─ composer.json

Add only the directories needed by the package. Keep fast tests without external state in tests/unit/. Put tests that use the installed QUIQQER system, its database, or projects in tests/integration/.

Install Package-Local Tools With PHIVE

Keep the required PHAR tools in .phive/phars.xml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
    <phar name="phpunit" version="^10.5" location="./tools/phpunit" copy="false"/>
    <phar name="phpstan" version="2.*" location="./tools/phpstan" copy="false"/>
    <phar name="phpcs" version="4.*" location="./tools/phpcs" copy="false"/>
    <phar name="phpcbf" version="4.*" location="./tools/phpcbf" copy="false"/>
</phive>

Install the tools from the package root:

shell
phive install --temporary

Commit the PHIVE configuration and signatures required by the repository, but do not commit downloaded tools from tools/.

Composer Scripts

Expose consistent commands in composer.json:

json
{
  "scripts": {
    "test": [
      "@dev:lint",
      "@dev:phpunit"
    ],
    "dev:phpunit": "./tools/phpunit",
    "dev:lint": [
      "@dev:lint:phpstan",
      "@dev:lint:style"
    ],
    "dev:lint:phpstan": "./tools/phpstan",
    "dev:lint:style": "./tools/phpcs",
    "dev:lint:style:fix": "./tools/phpcbf",
    "dev:init:tools": "phive install --temporary"
  }
}

The normal verification command is then:

shell
composer test

Run individual checks while developing:

shell
composer dev:phpunit
composer dev:lint:phpstan
composer dev:lint:style

PHPUnit Configuration

Use a package-local phpunit.dist.xml. Separate unit and integration suites when the package has both:

xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/phpunit-bootstrap.php">
    <testsuites>
        <testsuite name="Unit Test Suite">
            <directory>tests/unit</directory>
        </testsuite>
        <testsuite name="Integration Test Suite">
            <directory>tests/integration</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory suffix=".php">src</directory>
        </include>
    </source>
</phpunit>

PHPUnit only discovers tests in the directories listed by the package configuration. Loading the Core PHPUnit bootstrap does not run Core tests.

Choose the PHPUnit Bootstrap

The bootstrap in phpunit.dist.xml belongs to the package. Pure unit tests can use a package-specific loader and do not need the Core PHPUnit bootstrap. Keep that loader as small as the test suite allows.

Load the Core PHPUnit bootstrap when tests require an initialized QUIQQER system, database access, PHPUnit projects, or the central cleanup lifecycle:

php
<?php

require_once dirname(__DIR__, 4)
    . '/packages/quiqqer/core/tests/phpunit-bootstrap.php';

This path assumes the standard installation layout packages/<vendor>/<package>/tests. The Core bootstrap initializes QUIQQER and registers the central test cleanup for normal shutdown. When PHP provides the pcntl extension, the same cleanup also handles SIGINT and SIGTERM.

Packages can instead provide their own loader when they need different or more limited initialization. A package that uses the Core bootstrap can load its own test helpers afterward:

php
require_once __DIR__ . '/integration/PackageTestFixtures.php';

PackageTestFixtures::registerCleanup();

Register Package Fixture Cleanup

Core owns PHPUnit project cleanup. Each package owns and removes only the test data it creates, such as database rows, files, categories, or configuration entries.

Register a runtime listener for the Core cleanup event:

php
<?php

use QUI\System\TestCleanup;

final class PackageTestFixtures
{
    private static bool $cleanupRegistered = false;

    public static function registerCleanup(): void
    {
        if (self::$cleanupRegistered) {
            return;
        }

        self::$cleanupRegistered = true;
        QUI::getEvents()->addEvent(
            TestCleanup::EVENT,
            static function (): void {
                self::cleanup();
            }
        );
    }

    public static function cleanup(): void
    {
        // Remove only fixtures created by this package.
    }
}

Use a closure or another callable for this runtime-only listener. Do not add a test cleanup listener to the package's production events.xml.

Cleanup implementations should be:

  • idempotent, so repeated calls are safe
  • limited to fixtures created by the package
  • based on recorded identifiers or an unambiguous test prefix
  • able to run after a partially completed setup
  • ordered so package fixtures are removed before their PHPUnit project

Continue to clean per-test data in tearDown() where practical. The Core event is the final safety net for the end of the process and interrupted test runs.

SIGKILL cannot be handled. Remove orphaned PHPUnit projects manually from the Core package when a process was killed without cleanup:

shell
cd packages/quiqqer/core
composer dev:test:cleanup

PHPStan at Level 8

QUIQQER modules should use PHPStan level 8. New modules should start at level 8 without a baseline.

Create phpstan.dist.neon:

yaml
parameters:
    level: 8
    paths:
        - src
        - ajax
    bootstrapFiles:
        - tests/phpstan-bootstrap.php

List only directories that exist in the package. Set the supported PHP version range when PHPStan cannot infer it from Composer metadata:

yaml
parameters:
    phpVersion:
        min: 80200
        max: 80509

The PHPStan bootstrap loads QUIQQER without loading PHPUnit:

php
<?php

if (!defined('QUIQQER_SYSTEM')) {
    define('QUIQQER_SYSTEM', true);
}

if (!defined('QUIQQER_AJAX')) {
    define('QUIQQER_AJAX', true);
}

putenv('QUIQQER_OTHER_AUTOLOADERS=KEEP');

require_once dirname(__DIR__, 4) . '/bootstrap.php';

Add narrow test stubs when optional dependencies are intentionally unavailable during analysis. Do not use stubs to hide errors in package code.

Existing modules may use a baseline while they are moved to level 8. A baseline is a migration aid, not a place for new findings. Fix new errors and reduce the baseline whenever affected code is changed.

PHPCS With PSR-12

Create phpcs.xml.dist:

xml
<?xml version="1.0"?>
<ruleset>
    <rule ref="PSR12"/>
    <arg name="extensions" value="php"/>
    <arg name="warning-severity" value="0"/>
    <arg name="basepath" value="."/>
    <arg name="colors"/>
    <file>.</file>
</ruleset>

Exclude generated code, third-party code, and historical fixtures only when they are part of the repository and cannot be checked. Do not exclude normal package source code to make the check pass.

Use PHPCBF only for mechanically fixable findings and review its diff:

shell
composer dev:lint:style:fix
git diff

Integration Test Rules

Integration tests share an installed QUIQQER system. Keep them isolated:

  • create dedicated fixtures instead of modifying production-like records
  • use unique names or prefixes for database fixtures
  • record created identifiers immediately
  • remove fixtures in tearDown() and, when used, through the Core cleanup event
  • let Core create and remove PHPUnit projects
  • skip with a clear reason when a required service or database is unavailable
  • never assume test execution order

Run unit and integration suites separately while diagnosing failures:

shell
./tools/phpunit --testsuite "Unit Test Suite"
./tools/phpunit --testsuite "Integration Test Suite"

Before handing over a package change, run PHPStan, PHPCS, and all relevant PHPUnit suites. A successful unit suite does not replace integration tests for database, project, filesystem, or event behavior.

Released under GPL-3.0-or-later.