Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Testo Configure
Testo Configure
testo/testo
claude-code
testing
test
tests
dev
Install
php artisan boost:add-skill testo/testo

Save this content to: .claude/skills/testo-configure/SKILL.md

---
package: testo/testo
source_path: skills/testo-configure/SKILL.md
repo: https://github.com/php-testo/testo
---

---
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 test` → `vendor/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
<?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):

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

## Multi-suite layout

A typical real-world `testo.php`:

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

```php
$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 --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.

package: testo/testo source_path: skills/testo-configure/SKILL.md repo: https://github.com/php-testo/testo


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

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope
anil/file-picker
broqit/fields-ai