testo-configure

star 190

Set up or edit `testo.php` — the Testo application config. Use when the user is bootstrapping a project (including running `vendor/bin/testo init`), adding/removing a suite, scoping a finder, wiring an application-wide plugin (coverage, JUnit), or asking "where do I configure Testo" / "how do I initialize Testo".

php-testo By php-testo schedule Updated 6/4/2026

name: testo-configure description: Set up or edit testo.php — the Testo application config. Use when the user is bootstrapping a project (including running vendor/bin/testo init), adding/removing a suite, scoping a finder, wiring an application-wide plugin (coverage, JUnit), or asking "where do I configure Testo" / "how do I initialize Testo".

Configuring Testo (testo.php)

Testo's config is a real PHP file at the project root returning an ApplicationConfig. No XML, no JSON. This means: full IDE completion, refactoring, and conditional logic (e.g. CI-only suites).

Fetch https://php-testo.github.io/llms.txt before introducing new classes — the namespaces here are the most commonly drifted-on detail.

Bootstrap with init

If the project has no testo.php yet, prefer the built-in command over hand-writing the file:

vendor/bin/testo init
vendor/bin/testo init --path=app
vendor/bin/testo init --no-interaction

What it does, in order:

  1. Ensures the base directory (--path, default .) exists. --path is treated as the project root — every subsequent lookup (src/, tests/, composer.json) and every path baked into the generated testo.php is resolved relative to it.
  2. Resolves the source directory under --path:
    • if <path>/src exists, uses it;
    • otherwise in interactive mode prompts for a directory (default src, must exist under --path);
    • otherwise (non-interactive) fails — create <path>/src first or run interactively. The path is written into the config relative to testo.php, so a src entry resolves back to <path>/src at runtime, regardless of where vendor/bin/testo is invoked from.
  3. Creates <path>/tests/ and scans it for known suite folders: Unit, Integration, Functional, Acceptance, Feature, E2E, Contract. Whatever exists is picked up; Unit is always added (and <path>/tests/Unit/ created if missing).
  4. Writes scripts to the composer.json colocated with --path (so a monorepo sub-app updates its own composer.json, not the parent one). If no composer.json is present at that path the step is skipped silently.
    • composer testvendor/bin/testo
    • composer test:unit, composer test:integration, … one per detected suite (vendor/bin/testo --suite=<Name>). Existing keys are preserved.
  5. Generates <path>/testo.php from the stub, with src and one SuiteConfig per detected suite. If the file already exists: prompts to overwrite (interactive) or skips with a warning (non-interactive).

When to use init vs. hand-editing:

  • New project / empty repo → run init first, then tune testo.php.
  • Adding a suite to an existing project → create the directory (e.g. tests/Integration) and re-run init to get the matching composer script, or edit testo.php directly. init will not overwrite an existing config unless confirmed.
  • Monorepo / sub-app layout (a self-contained app under app/, with its own src/, tests, and optionally composer.json) → vendor/bin/testo init --path=app. --path is the sub-app's project root: app/src/ must exist, and the generated app/testo.php references src and tests/<Suite> as paths relative to itself — so they resolve correctly regardless of where vendor/bin/testo is invoked from.

After init completes, re-read testo.php and adjust src, suite locations, and plugins to match the project (see sections below).

Minimal testo.php

<?php
declare(strict_types=1);

use Testo\Application\Config\ApplicationConfig;
use Testo\Application\Config\SuiteConfig;

return new ApplicationConfig(
    src: ['src'],
    suites: [
        new SuiteConfig(name: 'Unit', location: ['tests/Unit']),
    ],
);

Run it: vendor/bin/testo. The single suite Unit will be discovered under tests/Unit.

Anatomy

Suite is the plugin-scope boundary. A Test Suite is a named, configured collection of Test Cases (a Test Case = methods of one class or functions of one file). Different suites can carry different plugin sets — that's the whole reason SuiteConfig::$plugins and SuitePlugins::only(...) exist.

ApplicationConfig takes:

  • src — directories (string[] or FinderConfig) holding production code. Used by coverage and inline tests.
  • suites — array of SuiteConfig, one per logical test area.
  • plugins — application-wide plugins (coverage, JUnit, anything cross-cutting).

SuiteConfig takes:

  • name — string, must be unique. Selectable via --suite=<name>.
  • location — directories or FinderConfig for the suite's test files.
  • plugins — suite-specific plugins, or SuitePlugins::only(...) to override inherited application plugins.

FinderConfig(includes, excludes) — when a flat array isn't enough (e.g. exclude module's own tests):

new FinderConfig(
    includes: ['core', 'plugin', 'bridge'],
    excludes: ['plugin/assert/tests', 'plugin/codecov/tests'],
);

Multi-suite layout

A typical real-world testo.php:

<?php
declare(strict_types=1);

use Testo\Application\Config\ApplicationConfig;
use Testo\Application\Config\FinderConfig;
use Testo\Application\Config\SuiteConfig;
use Testo\Application\Config\Plugin\SuitePlugins;
use Testo\Codecov\CodecovPlugin;
use Testo\Codecov\Config\CoverageLevel;
use Testo\Codecov\Report\CloverReport;
use Testo\Inline\InlineTestPlugin;

return new ApplicationConfig(
    src: ['src'],
    suites: [
        new SuiteConfig(name: 'Unit',        location: ['tests/Unit']),
        new SuiteConfig(name: 'Integration', location: ['tests/Integration']),
        new SuiteConfig(
            name: 'Sources',
            location: ['src'],
            plugins: SuitePlugins::only(new InlineTestPlugin()),
        ),
    ],
    plugins: [
        new CodecovPlugin(
            level: CoverageLevel::Line,
            reports: [
                new CloverReport(__DIR__ . '/runtime/clover.xml', 'MyProject'),
            ],
        ),
    ],
);

Notes on the example:

  • Sources scans the production tree to pick up #[TestInline] cases; SuitePlugins::only ensures only InlineTestPlugin runs for that suite.
  • CodecovPlugin is application-wide → it applies to every suite.
  • Names are arbitrary; keep them short — they appear in CI logs and in --suite=.

Conditional / dynamic config

Because testo.php is real PHP, conditional logic is fine:

$ciOnly = \filter_var(\getenv('CI'), FILTER_VALIDATE_BOOLEAN);

return new ApplicationConfig(
    src: ['src'],
    suites: \array_merge(
        [new SuiteConfig(name: 'Unit', location: ['tests/Unit'])],
        $ciOnly ? [] : [new SuiteConfig(name: 'Sandbox', location: ['tests/Sandbox'])],
    ),
);

Keep it readable — if the logic gets long, extract a helper, don't pile up ternaries.

CLI cheat-sheet

vendor/bin/testo init                  # bootstrap testo.php + composer scripts
vendor/bin/testo init --path=app       # bootstrap inside a subdirectory
vendor/bin/testo                       # all suites
vendor/bin/testo --suite=Unit          # one suite
vendor/bin/testo --suite=Unit --suite=Integration  # multiple
vendor/bin/testo --path=tests/Unit/Foo # subdirectory of a suite
vendor/bin/testo --filter='UserService'  # by name
vendor/bin/testo --type=test           # only #[Test], not benches/inline
vendor/bin/testo --coverage            # force coverage on
vendor/bin/testo --no-coverage         # force coverage off
vendor/bin/testo --coverage-clover=build/clover.xml        # write Clover report (no testo.php needed)
vendor/bin/testo --coverage-cobertura=build/cobertura.xml  # write Cobertura report
vendor/bin/testo --coverage-xml=build/coverage-xml         # write coverage XML dir (Infection)
vendor/bin/testo --teamcity            # TeamCity output (CI/IDE)
vendor/bin/testo --config=path/to/testo.php

Pitfalls

  • src must include production code only, not tests. Otherwise inline tests will run against test fixtures and coverage will count test files.
  • SuitePlugins::only(...) replaces inherited plugins. If the suite still needs coverage, add CodecovPlugin to the only() list too.
  • Don't name two suites the same. The first one wins silently in some builds — keep names unique.
  • Don't shell out to composer dump-autoload from testo.php. Composer's autoloader is already booted by the CLI entry. Avoid side-effects in config.
  • Don't read environment variables without a fallback. \getenv('FOO') ?: 'default' — CI dropouts otherwise silently change which suites run.
  • testo.php is required. If a project doesn't have one, run vendor/bin/testo init (see Bootstrap with init) or, for full control, write the minimal version above by hand.
Install via CLI
npx skills add https://github.com/php-testo/testo --skill testo-configure
Repository Details
star Stars 190
call_split Forks 10
navigation Branch main
article Path SKILL.md
More from Creator