Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ gtag('config', 'G-VYGDN3X0PR');`],
{ text: 'Data Providers', link: '/docs/plugins/data.md' },
{ text: 'Retry', link: '/docs/plugins/retry.md' },
{ text: 'Repeat', link: '/docs/plugins/repeat.md' },
{ text: 'Skip', link: '/docs/plugins/skip.md' },
{ text: 'Fiber', link: '/docs/plugins/fiber.md' },
{ text: 'Bench', link: '/docs/plugins/bench.md' },
{ text: '\#[Test]', link: '/docs/plugins/test.md' },
Expand Down Expand Up @@ -193,6 +194,7 @@ gtag('config', 'G-VYGDN3X0PR');`],
{ text: 'Inline (встроенные тесты)', link: '/ru/docs/plugins/inline.md' },
{ text: 'Retry', link: '/ru/docs/plugins/retry.md' },
{ text: 'Repeat', link: '/ru/docs/plugins/repeat.md' },
{ text: 'Skip (пропуск тестов)', link: '/ru/docs/plugins/skip.md' },
{ text: 'Fiber (файберы)', link: '/ru/docs/plugins/fiber.md' },
{ text: 'Bench', link: '/ru/docs/plugins/bench.md' },
{ text: '\#[Test]', link: '/ru/docs/plugins/test.md' },
Expand Down
7 changes: 7 additions & 0 deletions docs/intro/writing-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ Instead of base classes or magic methods, Testo bets on attributes.
public function flakyExternalService(): void { /* ... */ }
```

- The <attr>\Testo\Skip</attr> attribute from the <plugin>Skip</plugin> plugin skips a test without deleting it: the test is reported as <enum>\Testo\Core\Value\Status::Skipped</enum> with its reason, and none of its code runs — not even its <attr>\Testo\Lifecycle\BeforeTest</attr> and <attr>\Testo\Lifecycle\AfterTest</attr> hooks:

```php
#[Skip('broken by the pricing rework')]
public function calculatesTotal(): void { /* ... */ }
```

- Lifecycle hooks from the <plugin>Lifecycle</plugin> plugin help set up the environment and clean state between tests:
- <attr>\Testo\Lifecycle\BeforeTest</attr> — runs before each test.
- <attr>\Testo\Lifecycle\AfterTest</attr> — runs after each test.
Expand Down
4 changes: 4 additions & 0 deletions docs/plugins/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ BeforeClass (once)
AfterClass (once)
```

::: info
A test marked with <attr>\Testo\Skip</attr> never reaches these hooks: it is reported as <enum>\Testo\Core\Value\Status::Skipped</enum> before its run begins, so <attr>\Testo\Lifecycle\BeforeTest</attr> and <attr>\Testo\Lifecycle\AfterTest</attr> are not called for it. The class hooks still run while the case has at least one test left to run; when every test of the case is skipped, <attr>\Testo\Lifecycle\BeforeClass</attr> and <attr>\Testo\Lifecycle\AfterClass</attr> are not called and the class is never constructed. See the <plugin>Skip</plugin> plugin.
:::

## Basic Example

```php
Expand Down
2 changes: 2 additions & 0 deletions docs/plugins/repeat.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,6 @@ The two plugins look similar but solve opposite problems. Pick the one that matc

::: question What happens if a repetition is skipped or aborted?
The loop terminates immediately and the test reports the corresponding status — Skipped, Cancelled, or Aborted. Only completed runs (passed or failed) count toward `$maxFailures`.

A test marked with <attr>\Testo\Skip</attr> is a different case: the skip is reported before the loop starts, so no repetition happens at all.
:::
4 changes: 4 additions & 0 deletions docs/plugins/retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ Retry forgives a single transient failure and stops as soon as the test passes.
::: question What happens if a retry policy is defined at multiple levels?
When multiple retry policies are defined, only the closest one to the test applies. For example, if the Test Suite has `maxAttempts: 3`, the class has `2`, and the method has `5`, the test will retry **up to 5 times**. Policies do not stack.
:::

::: question Does Retry apply to a test marked with `#[Skip]`?
No. A test marked with <attr>\Testo\Skip</attr> is reported as <enum>\Testo\Core\Value\Status::Skipped</enum> before any attempt is made, so neither Retry nor Repeat engages for it.
:::
145 changes: 145 additions & 0 deletions docs/plugins/skip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
outline: [2, 3]
llms_description: "How to skip a test declaratively with #[Skip]: the test is reported as Skipped with its reason before anything runs, so #[BeforeTest]/#[AfterTest], data providers, #[Retry]/#[Repeat] and coverage never engage, and a fully skipped class runs no #[BeforeClass]/#[AfterClass]. Class-level skip and inheritance, where the reason shows up in reports, the SkipTest exception for skipping at run time, and how PHPUnit and Pest compare."
---

# Skip

The plugin provides the <attr>\Testo\Skip</attr> attribute and an interceptor that mark a test as skipped before it ever starts. The test is reported as <enum>\Testo\Core\Value\Status::Skipped</enum> 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. The attribute can be placed on a method, function, or an entire class — in the latter case, every test in the class is skipped.

<plugin-info class="\Testo\Skip\SkipPlugin" name="Skip" included="\Testo\Application\Config\Plugin\SuitePlugins" />

<signature h="2" name="#[\Testo\Skip(string $reason = '')]">
<short>Marks a test, a test class or a test function as skipped without running it.</short>
<description>
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. 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 <attr>\Testo\Bench</attr> or <attr>\Testo\Inline\TestInline</attr> target runs as usual. Close in spirit to JUnit's `@Disabled` and Rust's `#[ignore]`.
</description>
<param name="$reason">Why the test is skipped. No reason by default. A given reason is appended to the result message and shows up in the JUnit, TeamCity and HTML reports.</param>
<example>
Skip a single test:

```php
use Testo\Skip;
use Testo\Test;

final class PricingTest
{
#[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 */ }
}
```
</example>
<example>
On a class — every test of the case is skipped, and a method may state its own reason:

```php
#[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 { /* ... */ }
}
```
</example>
</signature>

## What never runs

The skip is decided before the test starts, and the test is reported right where its own run would begin. Nothing that prepares, wraps or repeats a test body gets a chance to engage:

- <attr>\Testo\Lifecycle\BeforeTest</attr> and <attr>\Testo\Lifecycle\AfterTest</attr> hooks are not called.
- Data providers are not called: a data-driven test yields a **single** <enum>\Testo\Core\Value\Status::Skipped</enum> entry, not one per data set.
- <attr>\Testo\Retry</attr> and <attr>\Testo\Repeat</attr> never start their loop.
- A method-level <attr>\Testo\Fiber\RunInFiber</attr> never wraps the test in a fiber, and no coverage is collected for it. Under a class-level `#[RunInFiber]` the skipped test still takes its turn in the case scheduler, but returns at once.

The class-level hooks follow the case, not the test: <attr>\Testo\Lifecycle\BeforeClass</attr> and <attr>\Testo\Lifecycle\AfterClass</attr> still run while the case has at least one test left to run. When every test of the case is skipped, they are not called and the class is never constructed.

```php
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
}
}
```

A run consisting only of skipped tests is a success: <enum>\Testo\Core\Value\Status::Skipped</enum> is neither a failure nor an error, so the exit code is `0`.

## Where the reason shows up

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\PricingTest::calculatesTotal is skipped via #[Skip] ==> broken by the pricing rework
```

- The JUnit ([`--log-junit`](../guide/cli-reference.md#log-junit)), TeamCity ([`--teamcity`](../guide/cli-reference.md#teamcity)) and HTML reports show that message.
- The terminal prints the skipped line without it.
- The compact [`--json`](../guide/cli-reference.md#json) report counts the test in its totals.

## 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 <class>\Testo\Core\Exception\SkipTest</class> from the test body. The test is reported as <enum>\Testo\Core\Value\Status::Skipped</enum> with the exception message.

```php
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: <attr>\Testo\Lifecycle\BeforeTest</attr> 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. <attr>\Testo\Skip</attr> is declared ahead of time and never reaches any of that. In reports the two are easy to tell apart: a declared skip carries the `is skipped via #[Skip]` marker in its message.

::: warning
Throw <class>\Testo\Core\Exception\SkipTest</class> from the test body only. Thrown from an interceptor it leaves the pipeline and the test lands as <enum>\Testo\Core\Value\Status::Aborted</enum>, not <enum>\Testo\Core\Value\Status::Skipped</enum>.
:::

## 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 <attr>\Testo\Skip</attr> when **the test must not run for now**, and that decision should be visible both in the code and in the report.
- Throw <class>\Testo\Core\Exception\SkipTest</class> when **only the test itself can decide**, based on what it finds at run time.
- Use <attr>\Testo\Filter\Group</attr> with `--group=!slow` when **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 | <enum>\Testo\Core\Value\Status::Skipped</enum>, with the reason |
| `throw new SkipTest('…')` | inside the test, while it runs | <enum>\Testo\Core\Value\Status::Skipped</enum>, with the message |
| <attr>\Testo\Filter\Group</attr> + `--group=!slow` | at the runner invocation | not at all |

::: question Do I need to register the plugin?
No. `SkipPlugin` is part of the default suite plugins, and the attribute wires its own interceptor. In a suite configured without the plugin the test is still reported as <enum>\Testo\Core\Value\Status::Skipped</enum>, and its `#[BeforeTest]`/`#[AfterTest]` hooks are still not called. What is lost is the class-level decision: a class whose tests are all skipped then runs its `#[BeforeClass]`/`#[AfterClass]` hooks, and a non-static hook constructs the class.
:::
7 changes: 7 additions & 0 deletions ru/docs/intro/writing-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ Expect::notLeaks($connection);
public function flakyExternalService(): void { /* ... */ }
```

- Атрибут <attr>\Testo\Skip</attr> из плагина <plugin>Skip</plugin> пропускает тест, не удаляя его: тест попадает в отчёт со статусом <enum>\Testo\Core\Value\Status::Skipped</enum> и своей причиной, а его код не выполняется — даже его хуки <attr>\Testo\Lifecycle\BeforeTest</attr> и <attr>\Testo\Lifecycle\AfterTest</attr>:

```php
#[Skip('broken by the pricing rework')]
public function calculatesTotal(): void { /* ... */ }
```

- Хуки жизненного цикла из плагина <plugin>Lifecycle</plugin> помогут подготовить окружение и очистить состояние между тестами:
- <attr>\Testo\Lifecycle\BeforeTest</attr> — выполняется перед каждым тестом.
- <attr>\Testo\Lifecycle\AfterTest</attr> — выполняется после каждого теста.
Expand Down
4 changes: 4 additions & 0 deletions ru/docs/plugins/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ BeforeClass (один раз)
AfterClass (один раз)
```

::: info
Тест с атрибутом <attr>\Testo\Skip</attr> до этих хуков не доходит: он попадает в отчёт со статусом <enum>\Testo\Core\Value\Status::Skipped</enum> ещё до начала своего запуска, поэтому <attr>\Testo\Lifecycle\BeforeTest</attr> и <attr>\Testo\Lifecycle\AfterTest</attr> для него не вызываются. Хуки класса выполняются, пока в тест-кейсе есть хотя бы один непропущенный тест; если пропущены все тесты, <attr>\Testo\Lifecycle\BeforeClass</attr> и <attr>\Testo\Lifecycle\AfterClass</attr> не вызываются, а класс не создаётся. Подробнее — в описании плагина <plugin>Skip</plugin>.
:::

## Базовый пример

```php
Expand Down
2 changes: 2 additions & 0 deletions ru/docs/plugins/repeat.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,6 @@ public function noisyButImportantCheck(): void { /* ... */ }

::: question Что будет, если один из повторов пропущен или прерван?
Цикл сразу останавливается, и тест получает соответствующий статус — Skipped, Cancelled или Aborted. В `$maxFailures` засчитываются только завершённые прогоны (passed или failed).

Тест с атрибутом <attr>\Testo\Skip</attr> — другой случай: пропуск фиксируется ещё до начала цикла, так что повторов не происходит вовсе.
:::
4 changes: 4 additions & 0 deletions ru/docs/plugins/retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,7 @@ Retry прощает одно случайное падение и остана
::: question Что будет, если задать политику повторов на нескольких уровнях?
При множественном определении политики повторов применяется только ближайшая к тесту. Например, если на Test Suite задано `maxAttempts: 3`, на классе — `2`, а на методе — `5`, тест будет повторяться **до 5 раз**. Политики не накапливаются.
:::

::: question Действует ли Retry на тест с атрибутом `#[Skip]`?
Нет. Тест с атрибутом <attr>\Testo\Skip</attr> попадает в отчёт со статусом <enum>\Testo\Core\Value\Status::Skipped</enum> раньше любой попытки, поэтому ни Retry, ни Repeat для него не включаются.
:::
Loading