com um clique
com um clique
Stabilize flaky Testo tests with
Migrate an existing PHPUnit test suite to Testo. Use when the user says "migrate from PHPUnit", "port phpunit tests", "convert TestCase to Testo", or has files extending PHPUnit\Framework\TestCase that need to move to Testo's attribute-based style.
Author a Testo plugin — event listeners, interceptors, custom container bindings, or new test attributes. Use when the user wants to extend Testo's behaviour (custom reporters, lifecycle hooks across the suite, attribute-driven middleware, integrating an external system) rather than writing a single test.
Write or modify tests in a project that uses the Testo PHP testing framework. Use when adding a
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".
Write or tune Testo performance benchmarks with
| name | testo-data-driven |
| description | Parameterize Testo tests with |
Testo offers five parameterization attributes. Pick by the shape of the data, not by habit.
| Attribute | When to use |
|---|---|
#[DataSet] | A handful of fixed cases, written inline next to the test method. One attribute = one case. |
#[DataProvider] | A larger or computed set, produced by a static method (iterable). |
#[DataUnion] | Concatenate several providers/datasets into one logical axis. Primarily a building block for DataCross / DataZip. |
#[DataZip] | Pair N providers by index (1st with 1st, 2nd with 2nd, …). Arguments from each axis are concatenated per row. |
#[DataCross] | Cartesian product across N providers. Each provider contributes its arguments to one slice of the final argument list; combinations multiply. |
Always fetch https://php-testo.github.io/llms.txt before writing — the attribute namespaces and
constructor signatures live there and may evolve.
#[DataSet] — inline casesuse Testo\Data\DataSet;
use Testo\Test;
use Testo\Assert;
#[Test]
final class AgeValidatorTest
{
#[DataSet([12, false], 'below minimum')]
#[DataSet([18, true], 'within range')]
#[DataSet([65, false], 'above maximum')]
public function isValid(int $age, bool $expected): void
{
Assert::same(AgeValidator::isValid($age), $expected);
}
}
The second argument is the case label — surface it for every case; it is what shows up in failure output.
#[DataProvider] — method-based casesuse Testo\Data\DataProvider;
#[DataProvider('emailScenarios')]
public function validatesEmail(string $email, bool $valid): void
{
Assert::same(EmailValidator::check($email), $valid);
}
public static function emailScenarios(): iterable
{
yield 'valid' => ['test@example.com', true];
yield 'invalid' => ['not-email', false];
yield 'empty' => ['', false];
}
Rules:
public static and return iterable.yield 'label' => [...] over numeric keys — labels appear in output.#[DataProvider(Other::class, 'method')] is supported by the version in use (verify against llms-full.txt).#[DataZip] — index-aligned pairingUse when two/more datasets are already aligned positionally and you do not want their cartesian product.
#[DataZip(
new DataProvider('credentials'),
new DataProvider('expectedPermissions'),
)]
public function loginYieldsPermissions(string $user, string $pass, array $perms): void
{
Assert::same((new Auth())->login($user, $pass)->permissions(), $perms);
}
If providers have different lengths, the shorter wins — surface that constraint to the user when designing the data.
#[DataCross] — cartesian productMental model. DataCross(P1, P2, …, Pn) takes n providers. For every combination of one row from each provider, it concatenates their arguments into the final call. So:
|P1| × |P2| × … × |Pn|.arity(P1) + arity(P2) + … + arity(Pn).Each #[DataSet] attribute is one row of one argument-set. If you want several rows on the same axis, use a DataProvider, or wrap several DataSets in DataUnion.
#[DataCross(
new DataProvider('browsers'),
new DataProvider('viewports'),
)]
public function layoutRenders(string $browser, array $size): void
{
// 2 browsers × 2 viewports = 4 combinations
}
public static function browsers(): iterable
{
yield 'chrome' => ['chrome'];
yield 'firefox' => ['firefox'];
}
public static function viewports(): iterable
{
yield 'desktop' => [[1920, 1080]];
yield 'tablet' => [[768, 1024]];
}
DataUnion of DataSets per axisWhen the data is tiny and you don't want to write provider methods, wrap each axis in a DataUnion:
use Testo\Data\{DataCross, DataSet, DataUnion};
#[DataCross(
new DataUnion(
new DataSet(['chrome'], 'chrome'),
new DataSet(['firefox'], 'firefox'),
),
new DataUnion(
new DataSet([[1920, 1080]], 'desktop'),
new DataSet([[768, 1024]], 'tablet'),
),
)]
public function layoutRenders(string $browser, array $size): void
{
// 2 × 2 = 4 combinations
}
// WRONG — 4 axes × 1 row each = 1 run with 4 concatenated arguments,
// but the method takes only 2 parameters → arity mismatch.
#[DataCross(
new DataSet(['chrome'], 'chrome'),
new DataSet(['firefox'], 'firefox'),
new DataSet([[1920, 1080]], 'desktop'),
new DataSet([[768, 1024]], 'tablet'),
)]
public function layoutRenders(string $browser, array $size): void { /* broken */ }
Each argument position of DataCross is an axis, not a row.
Each provider can contribute more than one argument. They are concatenated in declaration order:
#[DataCross(
new DataProvider('numbers'), // yields [int, int]
new DataProvider('letters'), // yields [string, string]
)]
public function combined(int $a, int $b, string $c, string $d): void { }
Cardinality explodes quickly — warn the user if any axis exceeds ~5 entries.
#[DataSet].#[DataProvider].#[DataZip] (each axis = one provider, or a DataUnion of DataSets).#[DataCross] (same rule for axes).#[DataUnion] and pass the union as a single axis to DataCross / DataZip.When parameterizing numeric or sized inputs, include: 0, 1, n-1, n, n+1, null, the type's lower/upper limit.
Surface this checklist when the user asks for "parameterize this test" — don't only port the existing examples.
#[DataProvider] returning array of arrays without labels — failure output becomes unreadable.#[DataSet] = one row, not one axis. Putting several bare DataSets into DataCross / DataZip adds axes, not rows. To add rows to one axis, use DataProvider or wrap the DataSets in DataUnion.DataCross / DataZip, not the count of providers.#[DataCross] inside another #[DataCross] — flatten by listing all axes once.#[BeforeTest] hook.