Skip
The plugin provides the #[Skip]#[Skip(string $reason = '')]Marks a test, a test class or a test function as skipped without running it. attribute, which marks a test as skipped. The test is reported as Status::Skipped\Testo\Core\Value\Status::Skipped and counted in the totals, and an optional reason explains why it was skipped. Skip a test when it cannot run yet but it is too early to delete it: it reproduces a bug nobody has fixed yet, it is broken by a rework still in progress, or it was written ahead of the feature it checks.
Plugin class: SkipPlugin\Testo\Skip\SkipPlugin. Included in SuitePlugins\Testo\Application\Config\Plugin\SuitePlugins — enabled by default.
#[Skip]
Marks a test, a test class or a test function as skipped without running it.
#[Skip(string $reason = '')]Can be placed on a method, a free function, or a class — on a class every test of the case is skipped. The attribute is inherited from parent classes, traits and overridden methods. When both a method and its class carry #[Skip], the method's attribute takes precedence and its reason replaces the class one. This holds for an empty reason too: a bare #[Skip] on the method skips the test with no reason at all instead of falling back to the class reason. The attribute can be placed only once per target.
The attribute applies to plain tests only: on a non-test method it does nothing, and a #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Declares a benchmark comparing the method's performance against alternative implementations. or #[TestInline]#[TestInline(array $arguments, mixed $result = null)]Declares an inline test on a method or function. target runs as usual. Close in spirit to JUnit's @Disabled and Rust's #[ignore].
Parameters:
$reason- Why the test is skipped. No reason by default. A given reason is appended to the result message.
Examples:
Skip a single test:
use Testo\Skip;
use Testo\Test;
final class OrderTest
{
#[Test]
#[Skip('broken by the pricing rework')]
public function calculatesTotal(): void
{
// never runs — reported as Skipped with the reason above
}
#[Test]
public function createsOrder(): void { /* runs as usual */ }
}On a class — every test of the case is skipped, and a method may state its own reason:
#[Skip('the billing sandbox is down')]
final class BillingTest
{
#[Test]
public function chargesCard(): void { /* ... */ }
#[Test]
#[Skip('flaky since the gateway upgrade')] // this reason replaces the class one
public function refundsCard(): void { /* ... */ }
}What never runs
The skip is decided before the test starts: the test is reported as Status::Skipped\Testo\Core\Value\Status::Skipped on the spot, and that's the end of it. Nothing that normally prepares, wraps or repeats the test body ever runs:
- #[BeforeTest]
#[BeforeTest(int $priority = 0)]Runs a method before each test in the class. and #[AfterTest]#[AfterTest(int $priority = 0)]Runs a method after each test in the class. hooks are not called. - Data providers such as #[DataProvider]
#[DataProvider(callable|string $provider)]Provides data for a parameterized test from a method or callable. are not called: a data-driven test yields a single Status::Skipped\Testo\Core\Value\Status::Skippedentry, not one per data set. - #[Retry]
#[Retry(int $maxAttempts = 3, bool $markFlaky = true)]Declares a retry policy for a test on failure. and #[Repeat]#[Repeat(int $times = 2, int $maxFailures = 0, bool $markFlaky = true)]Runs a test a fixed number of times and decides the outcome by a failure threshold. never start their loop. - #[RunInFiber]
#[RunInFiber(Schedule $schedule = Schedule::Solo)]Runs a test (on a method) or a whole case (on a class) in fibers, under Testo's cooperative scheduler. doesn't start a fiber. - No code coverage is collected.
final class OrderTest
{
#[BeforeTest]
public function startTransaction(): void
{
// not called for calculatesTotal() — there is no body to prepare for
}
#[Test]
#[Skip('broken by the pricing rework')]
public function calculatesTotal(): void { /* ... */ }
#[Test]
public function createsOrder(): void
{
// startTransaction() runs for this one as usual
}
}Class-level hooks work differently, because they belong to the case rather than to a single test: #[BeforeClass]#[BeforeClass(int $priority = 0)]Runs a method once before all tests in the class. Suitable for expensive setup. and #[AfterClass]#[AfterClass(int $priority = 0)]Runs a method once after all tests in the class. Suitable for cleanup. still run as long as the case has at least one test that isn't skipped. When every test of the case is skipped, they are not called and the class is never even instantiated.
Skipped tests in reports
The test's result carries a message built from its qualified name — Class::method, or the fully qualified function name for a function test — and the marker is skipped via #[Skip], extended with the reason when one is given:
Tests\Unit\OrderTest::calculatesTotal is skipped via #[Skip] ==> broken by the pricing rework- The JUnit (
--log-junit), TeamCity (--teamcity) and HTML reports show that message. - The terminal prints the skipped line without the message.
- The compact
--jsonreport counts the test in its totals.
A run consisting only of skipped tests is a success: Status::Skipped\Testo\Core\Value\Status::Skipped is neither a failure nor an error.
Skipping at runtime
Sometimes the skip cannot be decided ahead of time: the test has to look around first and skip itself on what it finds — a missing extension, an unreachable service, a fixture that turned out empty. For that, throw SkipTest\Testo\Core\Exception\SkipTest from the test body. The test is reported as Status::Skipped\Testo\Core\Value\Status::Skipped with the exception message.
use Testo\Core\Exception\SkipTest;
#[Test]
public function requiresPdoMysql(): void
{
if (!\extension_loaded('pdo_mysql')) {
throw new SkipTest('pdo_mysql required');
}
// ...
}The two mechanisms reach the same status by different roads, and that is the point to keep in mind. The exception is thrown once the test is already running: #[BeforeTest]#[BeforeTest(int $priority = 0)]Runs a method before each test in the class. has done its work, the arguments are ready (from a data provider, if the test has one), and the test class has been instantiated if the method needs an instance. #[Skip]#[Skip(string $reason = '')]Marks a test, a test class or a test function as skipped without running it. is declared ahead of time and never reaches any of that. Telling them apart in a report is easy: only the attribute adds the is skipped via #[Skip] marker.
Throw SkipTest\Testo\Core\Exception\SkipTest from the test body only. Thrown from an interceptor it leaves the pipeline and the test lands as Status::Aborted\Testo\Core\Value\Status::Aborted, not Status::Skipped\Testo\Core\Value\Status::Skipped.
Skip, SkipTest or a group filter
All three keep a test from running, but they differ in when the decision is made and whether the test stays in the report:
- Use #[Skip]
#[Skip(string $reason = '')]Marks a test, a test class or a test function as skipped without running it. when the test must not run for now, and that decision should be visible both in the code and in the report. - Throw SkipTest
\Testo\Core\Exception\SkipTestwhen only the test itself can decide, based on what it finds at run time. - Use #[Group]
#[Group(string ...$names)]Labels a class, method, or function with one or more group names for selective filtering. with--group=!slowwhen the test is fine, it just doesn't need to run every time — for example, because it is slow.
| Tool | Decided | In the report |
|---|---|---|
#[Skip('…')] | in code, ahead of the run | Status::Skipped\Testo\Core\Value\Status::Skipped, with the reason |
throw new SkipTest('…') | inside the test, while it runs | Status::Skipped\Testo\Core\Value\Status::Skipped, with the message |
#[Group]#[Group(string ...$names)]Labels a class, method, or function with one or more group names for selective filtering. + --group=!slow | at the runner invocation | not at all |