From 71bdc362ab7e55c6f4b14a66f171277b9a2fb7bd Mon Sep 17 00:00:00 2001 From: Meacue Date: Thu, 24 Sep 2026 06:18:55 +0500 Subject: [PATCH 1/4] docs(plugins): add the Skip plugin page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `#[Skip]` shipped in testo/testo 0.10.49 as the `testo/skip` package and had no page on the site, so it was missing from the attribute and plugin lists and from `llms.txt`. The page opens the way the Retry and Repeat pages do — what the plugin provides and when to skip a test — and documents the attribute and its reason, what a skipped test never runs — the per-test hooks, providers, retries, fibers and coverage, and the class hooks of a fully skipped case — where the reason shows up in reports, the runtime `SkipTest` alternative, and the choice between the two and a group filter. The writing-tests, lifecycle, Retry and Repeat pages mention it. EN and RU, wired into both sidebars after Repeat. Assisted-By: Claude Opus 5.5 --- .vitepress/config.mts | 2 + docs/intro/writing-tests.md | 7 ++ docs/plugins/lifecycle.md | 4 + docs/plugins/repeat.md | 2 + docs/plugins/retry.md | 4 + docs/plugins/skip.md | 145 +++++++++++++++++++++++++++++++++ ru/docs/intro/writing-tests.md | 7 ++ ru/docs/plugins/lifecycle.md | 4 + ru/docs/plugins/repeat.md | 2 + ru/docs/plugins/retry.md | 4 + ru/docs/plugins/skip.md | 145 +++++++++++++++++++++++++++++++++ 11 files changed, 326 insertions(+) create mode 100644 docs/plugins/skip.md create mode 100644 ru/docs/plugins/skip.md diff --git a/.vitepress/config.mts b/.vitepress/config.mts index 0c88432..a2e4563 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -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' }, @@ -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' }, diff --git a/docs/intro/writing-tests.md b/docs/intro/writing-tests.md index 1551570..7efd869 100644 --- a/docs/intro/writing-tests.md +++ b/docs/intro/writing-tests.md @@ -134,6 +134,13 @@ Instead of base classes or magic methods, Testo bets on attributes. public function flakyExternalService(): void { /* ... */ } ``` +- The \Testo\Skip attribute from the Skip plugin skips a test without deleting it: the test is reported as \Testo\Core\Value\Status::Skipped with its reason, and none of its code runs — not even its \Testo\Lifecycle\BeforeTest and \Testo\Lifecycle\AfterTest hooks: + + ```php + #[Skip('broken by the pricing rework')] + public function calculatesTotal(): void { /* ... */ } + ``` + - Lifecycle hooks from the Lifecycle plugin help set up the environment and clean state between tests: - \Testo\Lifecycle\BeforeTest — runs before each test. - \Testo\Lifecycle\AfterTest — runs after each test. diff --git a/docs/plugins/lifecycle.md b/docs/plugins/lifecycle.md index ee35ec0..9501e79 100644 --- a/docs/plugins/lifecycle.md +++ b/docs/plugins/lifecycle.md @@ -83,6 +83,10 @@ BeforeClass (once) AfterClass (once) ``` +::: info +A test marked with \Testo\Skip never reaches these hooks: it is reported as \Testo\Core\Value\Status::Skipped before its run begins, so \Testo\Lifecycle\BeforeTest and \Testo\Lifecycle\AfterTest 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, \Testo\Lifecycle\BeforeClass and \Testo\Lifecycle\AfterClass are not called and the class is never constructed. See the Skip plugin. +::: + ## Basic Example ```php diff --git a/docs/plugins/repeat.md b/docs/plugins/repeat.md index 05e12eb..ca7ccd9 100644 --- a/docs/plugins/repeat.md +++ b/docs/plugins/repeat.md @@ -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 \Testo\Skip is a different case: the skip is reported before the loop starts, so no repetition happens at all. ::: diff --git a/docs/plugins/retry.md b/docs/plugins/retry.md index 7bbdaea..00dc45c 100644 --- a/docs/plugins/retry.md +++ b/docs/plugins/retry.md @@ -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 \Testo\Skip is reported as \Testo\Core\Value\Status::Skipped before any attempt is made, so neither Retry nor Repeat engages for it. +::: diff --git a/docs/plugins/skip.md b/docs/plugins/skip.md new file mode 100644 index 0000000..3d49543 --- /dev/null +++ b/docs/plugins/skip.md @@ -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 \Testo\Skip attribute and an interceptor that mark a test as skipped before it ever starts. The test is reported as \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. The attribute can be placed on a method, function, or an entire class — in the latter case, every test in the class is skipped. + + + + +Marks a test, a test class or a test function as skipped without running it. + +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 \Testo\Bench or \Testo\Inline\TestInline target runs as usual. Close in spirit to JUnit's `@Disabled` and Rust's `#[ignore]`. + +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. + +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 */ } +} +``` + + +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 { /* ... */ } +} +``` + + + +## 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: + +- \Testo\Lifecycle\BeforeTest and \Testo\Lifecycle\AfterTest hooks are not called. +- Data providers are not called: a data-driven test yields a **single** \Testo\Core\Value\Status::Skipped entry, not one per data set. +- \Testo\Retry and \Testo\Repeat never start their loop. +- A method-level \Testo\Fiber\RunInFiber 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: \Testo\Lifecycle\BeforeClass and \Testo\Lifecycle\AfterClass 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: \Testo\Core\Value\Status::Skipped 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 \Testo\Core\Exception\SkipTest from the test body. The test is reported as \Testo\Core\Value\Status::Skipped 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: \Testo\Lifecycle\BeforeTest 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. \Testo\Skip 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 \Testo\Core\Exception\SkipTest from the test body only. Thrown from an interceptor it leaves the pipeline and the test lands as \Testo\Core\Value\Status::Aborted, not \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 \Testo\Skip when **the test must not run for now**, and that decision should be visible both in the code and in the report. +- Throw \Testo\Core\Exception\SkipTest when **only the test itself can decide**, based on what it finds at run time. +- Use \Testo\Filter\Group 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 | \Testo\Core\Value\Status::Skipped, with the reason | +| `throw new SkipTest('…')` | inside the test, while it runs | \Testo\Core\Value\Status::Skipped, with the message | +| \Testo\Filter\Group + `--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 \Testo\Core\Value\Status::Skipped, 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. +::: diff --git a/ru/docs/intro/writing-tests.md b/ru/docs/intro/writing-tests.md index 9c0b9af..604bc0f 100644 --- a/ru/docs/intro/writing-tests.md +++ b/ru/docs/intro/writing-tests.md @@ -133,6 +133,13 @@ Expect::notLeaks($connection); public function flakyExternalService(): void { /* ... */ } ``` +- Атрибут \Testo\Skip из плагина Skip пропускает тест, не удаляя его: тест попадает в отчёт со статусом \Testo\Core\Value\Status::Skipped и своей причиной, а его код не выполняется — даже его хуки \Testo\Lifecycle\BeforeTest и \Testo\Lifecycle\AfterTest: + + ```php + #[Skip('broken by the pricing rework')] + public function calculatesTotal(): void { /* ... */ } + ``` + - Хуки жизненного цикла из плагина Lifecycle помогут подготовить окружение и очистить состояние между тестами: - \Testo\Lifecycle\BeforeTest — выполняется перед каждым тестом. - \Testo\Lifecycle\AfterTest — выполняется после каждого теста. diff --git a/ru/docs/plugins/lifecycle.md b/ru/docs/plugins/lifecycle.md index 6ee2291..273f6eb 100644 --- a/ru/docs/plugins/lifecycle.md +++ b/ru/docs/plugins/lifecycle.md @@ -83,6 +83,10 @@ BeforeClass (один раз) AfterClass (один раз) ``` +::: info +Тест с атрибутом \Testo\Skip до этих хуков не доходит: он попадает в отчёт со статусом \Testo\Core\Value\Status::Skipped ещё до начала своего запуска, поэтому \Testo\Lifecycle\BeforeTest и \Testo\Lifecycle\AfterTest для него не вызываются. Хуки класса выполняются, пока в тест-кейсе есть хотя бы один непропущенный тест; если пропущены все тесты, \Testo\Lifecycle\BeforeClass и \Testo\Lifecycle\AfterClass не вызываются, а класс не создаётся. Подробнее — в описании плагина Skip. +::: + ## Базовый пример ```php diff --git a/ru/docs/plugins/repeat.md b/ru/docs/plugins/repeat.md index 97b9c32..c93738b 100644 --- a/ru/docs/plugins/repeat.md +++ b/ru/docs/plugins/repeat.md @@ -97,4 +97,6 @@ public function noisyButImportantCheck(): void { /* ... */ } ::: question Что будет, если один из повторов пропущен или прерван? Цикл сразу останавливается, и тест получает соответствующий статус — Skipped, Cancelled или Aborted. В `$maxFailures` засчитываются только завершённые прогоны (passed или failed). + +Тест с атрибутом \Testo\Skip — другой случай: пропуск фиксируется ещё до начала цикла, так что повторов не происходит вовсе. ::: diff --git a/ru/docs/plugins/retry.md b/ru/docs/plugins/retry.md index 5b69a08..5b74258 100644 --- a/ru/docs/plugins/retry.md +++ b/ru/docs/plugins/retry.md @@ -74,3 +74,7 @@ Retry прощает одно случайное падение и остана ::: question Что будет, если задать политику повторов на нескольких уровнях? При множественном определении политики повторов применяется только ближайшая к тесту. Например, если на Test Suite задано `maxAttempts: 3`, на классе — `2`, а на методе — `5`, тест будет повторяться **до 5 раз**. Политики не накапливаются. ::: + +::: question Действует ли Retry на тест с атрибутом `#[Skip]`? +Нет. Тест с атрибутом \Testo\Skip попадает в отчёт со статусом \Testo\Core\Value\Status::Skipped раньше любой попытки, поэтому ни Retry, ни Repeat для него не включаются. +::: diff --git a/ru/docs/plugins/skip.md b/ru/docs/plugins/skip.md new file mode 100644 index 0000000..0440ad3 --- /dev/null +++ b/ru/docs/plugins/skip.md @@ -0,0 +1,145 @@ +--- +outline: [2, 3] +--- + +# Пропуск тестов + +Плагин предоставляет атрибут \Testo\Skip и интерцептор, которые помечают тест пропущенным ещё до его запуска. Тест попадает в отчёт со статусом \Testo\Core\Value\Status::Skipped и учитывается в итогах, а необязательная причина объясняет, почему он пропущен. Пропускайте тест, когда запускать его пока нельзя, а удалять рано: он воспроизводит ещё не починенный баг, сломан незавершённым рефакторингом или написан раньше фичи, которую проверяет. Атрибут можно повесить на метод, функцию или целый класс — в последнем случае пропускаются все тесты в классе. + + + + +Помечает тест, класс тестов или тестовую функцию как пропущенные, не запуская их. + +Можно повесить на метод, свободную функцию или класс — на классе пропускаются все тесты тест-кейса. Атрибут наследуется от родительских классов, трейтов и переопределённых методов. Если `#[Skip]` стоит и на методе, и на классе, действует атрибут метода, и его причина заменяет причину класса. Повесить атрибут на один элемент дважды нельзя. + +Атрибут действует только на обычные тесты: на не-тестовом методе он ничего не делает, а \Testo\Bench и \Testo\Inline\TestInline выполняются как обычно. По смыслу близок к `@Disabled` в JUnit и `#[ignore]` в Rust. + +Почему тест пропущен. По умолчанию причины нет. Заданная причина дописывается в сообщение результата и видна в отчётах JUnit, TeamCity и HTML. + +Пропустить один тест: + +```php +use Testo\Skip; +use Testo\Test; + +final class PricingTest +{ + #[Test] + #[Skip('broken by the pricing rework')] + public function calculatesTotal(): void + { + // никогда не выполняется — в отчёте Skipped с причиной выше + } + + #[Test] + public function createsOrder(): void { /* выполняется как обычно */ } +} +``` + + +На классе — пропускаются все тесты тест-кейса, а метод может указать свою причину: + +```php +#[Skip('the billing sandbox is down')] +final class BillingTest +{ + #[Test] + public function chargesCard(): void { /* ... */ } + + #[Test] + #[Skip('flaky since the gateway upgrade')] // эта причина заменяет причину класса + public function refundsCard(): void { /* ... */ } +} +``` + + + +## Что не выполняется + +Решение о пропуске принимается до старта теста, и в отчёт тест попадает ровно там, где начался бы его запуск. Ничто из того, что подготавливает, оборачивает или повторяет тело теста, не успевает включиться: + +- Хуки \Testo\Lifecycle\BeforeTest и \Testo\Lifecycle\AfterTest не вызываются. +- Провайдеры данных не вызываются: параметризованный тест даёт **одну** запись \Testo\Core\Value\Status::Skipped, а не по одной на каждый набор данных. +- \Testo\Retry и \Testo\Repeat не начинают свой цикл. +- \Testo\Fiber\RunInFiber на методе не оборачивает тест в файбер, и покрытие для него не собирается. При `#[RunInFiber]` на классе пропущенный тест всё же получает свою очередь в планировщике кейса, но сразу возвращает результат. + +Хуки уровня класса подчиняются тест-кейсу, а не тесту: \Testo\Lifecycle\BeforeClass и \Testo\Lifecycle\AfterClass выполняются, пока в тест-кейсе есть хотя бы один непропущенный тест. Если пропущены все тесты, эти хуки не вызываются, а класс так и не создаётся. + + +```php +final class OrderTest +{ + #[BeforeTest] + public function startTransaction(): void + { + // для calculatesTotal() не вызывается — готовить нечего + } + + #[Test] + #[Skip('broken by the pricing rework')] + public function calculatesTotal(): void { /* ... */ } + + #[Test] + public function createsOrder(): void + { + // для этого теста startTransaction() выполняется как обычно + } +} +``` + +Прогон, состоящий из одних пропущенных тестов, считается успешным: \Testo\Core\Value\Status::Skipped — это ни падение, ни ошибка, поэтому код выхода `0`. + +## Причина в отчётах + +В результат теста попадает сообщение из полного имени теста — `Класс::метод` или полное имя функции для функционального теста — и маркера `is skipped via #[Skip]`; если указана причина, она дописывается в конец: + +``` +Tests\Unit\PricingTest::calculatesTotal is skipped via #[Skip] ==> broken by the pricing rework +``` + +- Отчёты JUnit ([`--log-junit`](../guide/cli-reference.md#log-junit)), TeamCity ([`--teamcity`](../guide/cli-reference.md#teamcity)) и HTML показывают это сообщение. +- Терминал печатает строку пропущенного теста без него. +- Компактный отчёт [`--json`](../guide/cli-reference.md#json) учитывает тест в итогах. + +## Пропуск во время выполнения + +Иногда решение о пропуске нельзя принять заранее: тест должен сначала осмотреться и пропустить себя по тому, что обнаружит — нет расширения, недоступен сервис, фикстура оказалась пустой. Для этого бросьте \Testo\Core\Exception\SkipTest из тела теста. Тест попадёт в отчёт со статусом \Testo\Core\Value\Status::Skipped и сообщением исключения. + +```php +use Testo\Core\Exception\SkipTest; + +#[Test] +public function requiresPdoMysql(): void +{ + if (!\extension_loaded('pdo_mysql')) { + throw new SkipTest('pdo_mysql required'); + } + + // ... +} +``` + +Оба механизма приводят к одному статусу, но разными путями, и это стоит помнить. Исключение бросается, когда тест уже выполняется: \Testo\Lifecycle\BeforeTest отработал, аргументы подготовлены (провайдером данных, если он есть), а класс создан, если методу нужен экземпляр. \Testo\Skip объявляется заранее и до всего этого просто не доходит. В отчётах их легко различить: у объявленного пропуска в сообщении есть маркер `is skipped via #[Skip]`. + +::: warning +Бросайте \Testo\Core\Exception\SkipTest только из тела теста. Если бросить его из интерцептора, оно покинет пайплайн, и тест получит статус \Testo\Core\Value\Status::Aborted, а не \Testo\Core\Value\Status::Skipped. +::: + +## Skip, SkipTest или фильтр по группе + +Все три не дают тесту выполниться, но различаются тем, когда принимается решение и остаётся ли тест в отчёте: + +- Берите \Testo\Skip, когда **тест пока не должен запускаться** и это решение должно быть видно и в коде, и в отчёте. +- Бросайте \Testo\Core\Exception\SkipTest, когда **решить может только сам тест** — по тому, что он обнаружит во время выполнения. +- Берите \Testo\Filter\Group с ключом `--group=!slow`, когда **с тестом всё в порядке**, а запускать его в каждом прогоне незачем — например, он слишком долгий. + +| Инструмент | Где принимается решение | В отчёте | +|------------|-----------------------------------|--------------------------------------------------------------| +| `#[Skip('…')]` | до запуска теста | \Testo\Core\Value\Status::Skipped, с причиной | +| `throw new SkipTest('…')` | внутри теста, во время выполнения | \Testo\Core\Value\Status::Skipped, с сообщением | +| \Testo\Filter\Group + `--group=!slow` | при запуске раннера | не отображается | + +::: question Нужно ли регистрировать плагин? +Нет. `SkipPlugin` входит в набор плагинов по умолчанию, а атрибут сам подключает свой интерцептор. В Test Suite, настроенном без плагина, тест всё равно попадёт в отчёт со статусом \Testo\Core\Value\Status::Skipped, и `#[BeforeTest]`/`#[AfterTest]` для него по-прежнему не вызовутся. Теряется только решение на уровне класса: у класса, все тесты которого пропущены, выполнятся `#[BeforeClass]`/`#[AfterClass]`, а ради нестатического хука будет создан экземпляр класса. +::: From 16e42190f6d2872f6cd579a352bfab38fe182bde Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 25 Sep 2026 19:52:08 +0400 Subject: [PATCH 2/4] docs(plugins): trim the Skip mentions outside the Skip page The Retry and Repeat notes only restated what the Skip page already says, so they are gone; the Lifecycle and intro notes shrink to the one fact a reader of those pages needs. Assisted-By: Claude Opus 5.5 --- docs/intro/writing-tests.md | 2 +- docs/plugins/lifecycle.md | 2 +- docs/plugins/repeat.md | 2 -- docs/plugins/retry.md | 4 ---- docs/plugins/skip.md | 12 ++++++------ ru/docs/intro/writing-tests.md | 2 +- ru/docs/plugins/lifecycle.md | 2 +- ru/docs/plugins/repeat.md | 2 -- ru/docs/plugins/retry.md | 4 ---- ru/docs/plugins/skip.md | 19 +++++++++---------- 10 files changed, 19 insertions(+), 32 deletions(-) diff --git a/docs/intro/writing-tests.md b/docs/intro/writing-tests.md index 7efd869..6f9f2d0 100644 --- a/docs/intro/writing-tests.md +++ b/docs/intro/writing-tests.md @@ -134,7 +134,7 @@ Instead of base classes or magic methods, Testo bets on attributes. public function flakyExternalService(): void { /* ... */ } ``` -- The \Testo\Skip attribute from the Skip plugin skips a test without deleting it: the test is reported as \Testo\Core\Value\Status::Skipped with its reason, and none of its code runs — not even its \Testo\Lifecycle\BeforeTest and \Testo\Lifecycle\AfterTest hooks: +- The \Testo\Skip attribute from the Skip plugin skips a test without deleting it: the test doesn't run, but stays in the report as \Testo\Core\Value\Status::Skipped with its reason: ```php #[Skip('broken by the pricing rework')] diff --git a/docs/plugins/lifecycle.md b/docs/plugins/lifecycle.md index 9501e79..8796fc8 100644 --- a/docs/plugins/lifecycle.md +++ b/docs/plugins/lifecycle.md @@ -84,7 +84,7 @@ AfterClass (once) ``` ::: info -A test marked with \Testo\Skip never reaches these hooks: it is reported as \Testo\Core\Value\Status::Skipped before its run begins, so \Testo\Lifecycle\BeforeTest and \Testo\Lifecycle\AfterTest 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, \Testo\Lifecycle\BeforeClass and \Testo\Lifecycle\AfterClass are not called and the class is never constructed. See the Skip plugin. +For a test marked with \Testo\Skip, the \Testo\Lifecycle\BeforeTest and \Testo\Lifecycle\AfterTest hooks are not called: the test is reported as skipped before its run begins. See the Skip plugin for details. ::: ## Basic Example diff --git a/docs/plugins/repeat.md b/docs/plugins/repeat.md index ca7ccd9..05e12eb 100644 --- a/docs/plugins/repeat.md +++ b/docs/plugins/repeat.md @@ -101,6 +101,4 @@ 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 \Testo\Skip is a different case: the skip is reported before the loop starts, so no repetition happens at all. ::: diff --git a/docs/plugins/retry.md b/docs/plugins/retry.md index 00dc45c..7bbdaea 100644 --- a/docs/plugins/retry.md +++ b/docs/plugins/retry.md @@ -78,7 +78,3 @@ 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 \Testo\Skip is reported as \Testo\Core\Value\Status::Skipped before any attempt is made, so neither Retry nor Repeat engages for it. -::: diff --git a/docs/plugins/skip.md b/docs/plugins/skip.md index 3d49543..bc2e245 100644 --- a/docs/plugins/skip.md +++ b/docs/plugins/skip.md @@ -1,6 +1,6 @@ --- 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." +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 when to use #[Skip], SkipTest or a #[Group] filter." --- # Skip @@ -61,11 +61,9 @@ final class BillingTest 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: - \Testo\Lifecycle\BeforeTest and \Testo\Lifecycle\AfterTest hooks are not called. -- Data providers are not called: a data-driven test yields a **single** \Testo\Core\Value\Status::Skipped entry, not one per data set. +- Data providers such as \Testo\Data\DataProvider are not called: a data-driven test yields a **single** \Testo\Core\Value\Status::Skipped entry, not one per data set. - \Testo\Retry and \Testo\Repeat never start their loop. -- A method-level \Testo\Fiber\RunInFiber 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: \Testo\Lifecycle\BeforeClass and \Testo\Lifecycle\AfterClass 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. +- \Testo\Fiber\RunInFiber doesn't start a fiber for it, and no coverage is collected. ```php final class OrderTest @@ -88,6 +86,8 @@ final class OrderTest } ``` +The class-level hooks follow the case, not the test: \Testo\Lifecycle\BeforeClass and \Testo\Lifecycle\AfterClass 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. + A run consisting only of skipped tests is a success: \Testo\Core\Value\Status::Skipped is neither a failure nor an error, so the exit code is `0`. ## Where the reason shows up @@ -141,5 +141,5 @@ All three keep a test from running, but they differ in when the decision is made | \Testo\Filter\Group + `--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 \Testo\Core\Value\Status::Skipped, 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. +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 \Testo\Core\Value\Status::Skipped, and its \Testo\Lifecycle\BeforeTest/\Testo\Lifecycle\AfterTest hooks are still not called. What is lost is the class-level decision: a class whose tests are all skipped then runs its \Testo\Lifecycle\BeforeClass/\Testo\Lifecycle\AfterClass hooks, and a non-static hook constructs the class. ::: diff --git a/ru/docs/intro/writing-tests.md b/ru/docs/intro/writing-tests.md index 604bc0f..5e6a205 100644 --- a/ru/docs/intro/writing-tests.md +++ b/ru/docs/intro/writing-tests.md @@ -133,7 +133,7 @@ Expect::notLeaks($connection); public function flakyExternalService(): void { /* ... */ } ``` -- Атрибут \Testo\Skip из плагина Skip пропускает тест, не удаляя его: тест попадает в отчёт со статусом \Testo\Core\Value\Status::Skipped и своей причиной, а его код не выполняется — даже его хуки \Testo\Lifecycle\BeforeTest и \Testo\Lifecycle\AfterTest: +- Атрибут \Testo\Skip из плагина Skip пропускает тест, не удаляя его: тест не выполняется, но остаётся в отчёте со статусом \Testo\Core\Value\Status::Skipped и указанной причиной: ```php #[Skip('broken by the pricing rework')] diff --git a/ru/docs/plugins/lifecycle.md b/ru/docs/plugins/lifecycle.md index 273f6eb..4df702a 100644 --- a/ru/docs/plugins/lifecycle.md +++ b/ru/docs/plugins/lifecycle.md @@ -84,7 +84,7 @@ AfterClass (один раз) ``` ::: info -Тест с атрибутом \Testo\Skip до этих хуков не доходит: он попадает в отчёт со статусом \Testo\Core\Value\Status::Skipped ещё до начала своего запуска, поэтому \Testo\Lifecycle\BeforeTest и \Testo\Lifecycle\AfterTest для него не вызываются. Хуки класса выполняются, пока в тест-кейсе есть хотя бы один непропущенный тест; если пропущены все тесты, \Testo\Lifecycle\BeforeClass и \Testo\Lifecycle\AfterClass не вызываются, а класс не создаётся. Подробнее — в описании плагина Skip. +Для теста с атрибутом \Testo\Skip хуки \Testo\Lifecycle\BeforeTest и \Testo\Lifecycle\AfterTest не вызываются: тест помечается пропущенным ещё до начала запуска. Подробнее — в описании плагина Skip. ::: ## Базовый пример diff --git a/ru/docs/plugins/repeat.md b/ru/docs/plugins/repeat.md index c93738b..97b9c32 100644 --- a/ru/docs/plugins/repeat.md +++ b/ru/docs/plugins/repeat.md @@ -97,6 +97,4 @@ public function noisyButImportantCheck(): void { /* ... */ } ::: question Что будет, если один из повторов пропущен или прерван? Цикл сразу останавливается, и тест получает соответствующий статус — Skipped, Cancelled или Aborted. В `$maxFailures` засчитываются только завершённые прогоны (passed или failed). - -Тест с атрибутом \Testo\Skip — другой случай: пропуск фиксируется ещё до начала цикла, так что повторов не происходит вовсе. ::: diff --git a/ru/docs/plugins/retry.md b/ru/docs/plugins/retry.md index 5b74258..5b69a08 100644 --- a/ru/docs/plugins/retry.md +++ b/ru/docs/plugins/retry.md @@ -74,7 +74,3 @@ Retry прощает одно случайное падение и остана ::: question Что будет, если задать политику повторов на нескольких уровнях? При множественном определении политики повторов применяется только ближайшая к тесту. Например, если на Test Suite задано `maxAttempts: 3`, на классе — `2`, а на методе — `5`, тест будет повторяться **до 5 раз**. Политики не накапливаются. ::: - -::: question Действует ли Retry на тест с атрибутом `#[Skip]`? -Нет. Тест с атрибутом \Testo\Skip попадает в отчёт со статусом \Testo\Core\Value\Status::Skipped раньше любой попытки, поэтому ни Retry, ни Repeat для него не включаются. -::: diff --git a/ru/docs/plugins/skip.md b/ru/docs/plugins/skip.md index 0440ad3..556fb97 100644 --- a/ru/docs/plugins/skip.md +++ b/ru/docs/plugins/skip.md @@ -60,12 +60,9 @@ final class BillingTest Решение о пропуске принимается до старта теста, и в отчёт тест попадает ровно там, где начался бы его запуск. Ничто из того, что подготавливает, оборачивает или повторяет тело теста, не успевает включиться: - Хуки \Testo\Lifecycle\BeforeTest и \Testo\Lifecycle\AfterTest не вызываются. -- Провайдеры данных не вызываются: параметризованный тест даёт **одну** запись \Testo\Core\Value\Status::Skipped, а не по одной на каждый набор данных. +- Провайдеры данных, например \Testo\Data\DataProvider, не вызываются: параметризованный тест даёт **одну** запись \Testo\Core\Value\Status::Skipped, а не по одной на каждый набор данных. - \Testo\Retry и \Testo\Repeat не начинают свой цикл. -- \Testo\Fiber\RunInFiber на методе не оборачивает тест в файбер, и покрытие для него не собирается. При `#[RunInFiber]` на классе пропущенный тест всё же получает свою очередь в планировщике кейса, но сразу возвращает результат. - -Хуки уровня класса подчиняются тест-кейсу, а не тесту: \Testo\Lifecycle\BeforeClass и \Testo\Lifecycle\AfterClass выполняются, пока в тест-кейсе есть хотя бы один непропущенный тест. Если пропущены все тесты, эти хуки не вызываются, а класс так и не создаётся. - +- \Testo\Fiber\RunInFiber не запускает для него файбер, и покрытие не собирается. ```php final class OrderTest @@ -88,6 +85,8 @@ final class OrderTest } ``` +Хуки уровня класса подчиняются тест-кейсу, а не тесту: \Testo\Lifecycle\BeforeClass и \Testo\Lifecycle\AfterClass выполняются, пока в тест-кейсе есть хотя бы один непропущенный тест. Если пропущены все тесты, эти хуки не вызываются, а класс так и не создаётся. + Прогон, состоящий из одних пропущенных тестов, считается успешным: \Testo\Core\Value\Status::Skipped — это ни падение, ни ошибка, поэтому код выхода `0`. ## Причина в отчётах @@ -134,12 +133,12 @@ public function requiresPdoMysql(): void - Бросайте \Testo\Core\Exception\SkipTest, когда **решить может только сам тест** — по тому, что он обнаружит во время выполнения. - Берите \Testo\Filter\Group с ключом `--group=!slow`, когда **с тестом всё в порядке**, а запускать его в каждом прогоне незачем — например, он слишком долгий. -| Инструмент | Где принимается решение | В отчёте | -|------------|-----------------------------------|--------------------------------------------------------------| -| `#[Skip('…')]` | до запуска теста | \Testo\Core\Value\Status::Skipped, с причиной | +| Инструмент | Где принимается решение | В отчёте | +|------------|-------------------------|----------| +| `#[Skip('…')]` | до запуска теста | \Testo\Core\Value\Status::Skipped, с причиной | | `throw new SkipTest('…')` | внутри теста, во время выполнения | \Testo\Core\Value\Status::Skipped, с сообщением | -| \Testo\Filter\Group + `--group=!slow` | при запуске раннера | не отображается | +| \Testo\Filter\Group + `--group=!slow` | при запуске раннера | не отображается | ::: question Нужно ли регистрировать плагин? -Нет. `SkipPlugin` входит в набор плагинов по умолчанию, а атрибут сам подключает свой интерцептор. В Test Suite, настроенном без плагина, тест всё равно попадёт в отчёт со статусом \Testo\Core\Value\Status::Skipped, и `#[BeforeTest]`/`#[AfterTest]` для него по-прежнему не вызовутся. Теряется только решение на уровне класса: у класса, все тесты которого пропущены, выполнятся `#[BeforeClass]`/`#[AfterClass]`, а ради нестатического хука будет создан экземпляр класса. +Нет. `SkipPlugin` входит в набор плагинов по умолчанию, а атрибут сам подключает свой интерцептор. В Test Suite, настроенном без плагина, тест всё равно попадёт в отчёт со статусом \Testo\Core\Value\Status::Skipped, и \Testo\Lifecycle\BeforeTest/\Testo\Lifecycle\AfterTest для него по-прежнему не вызовутся. Теряется только решение на уровне класса: у класса, все тесты которого пропущены, выполнятся \Testo\Lifecycle\BeforeClass/\Testo\Lifecycle\AfterClass, а ради нестатического хука будет создан экземпляр класса. ::: From 72588de58ac26b754e7f4d1a6fb05996625c08bc Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 26 Sep 2026 16:13:41 +0400 Subject: [PATCH 3/4] docs(plugins): reword the "What never runs" section of the Skip page Assisted-By: Claude Opus 5.5 --- docs/plugins/skip.md | 9 +++++---- ru/docs/plugins/skip.md | 15 ++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/plugins/skip.md b/docs/plugins/skip.md index bc2e245..e5243e3 100644 --- a/docs/plugins/skip.md +++ b/docs/plugins/skip.md @@ -58,12 +58,13 @@ final class BillingTest ## 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: +The skip is decided before the test starts: the test is reported as \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: - \Testo\Lifecycle\BeforeTest and \Testo\Lifecycle\AfterTest hooks are not called. - Data providers such as \Testo\Data\DataProvider are not called: a data-driven test yields a **single** \Testo\Core\Value\Status::Skipped entry, not one per data set. - \Testo\Retry and \Testo\Repeat never start their loop. -- \Testo\Fiber\RunInFiber doesn't start a fiber for it, and no coverage is collected. +- \Testo\Fiber\RunInFiber doesn't start a fiber. +- No code coverage is collected. ```php final class OrderTest @@ -86,9 +87,9 @@ final class OrderTest } ``` -The class-level hooks follow the case, not the test: \Testo\Lifecycle\BeforeClass and \Testo\Lifecycle\AfterClass 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. +Class-level hooks work differently, because they belong to the case rather than to a single test: \Testo\Lifecycle\BeforeClass and \Testo\Lifecycle\AfterClass 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. -A run consisting only of skipped tests is a success: \Testo\Core\Value\Status::Skipped is neither a failure nor an error, so the exit code is `0`. +A run consisting only of skipped tests is a success: \Testo\Core\Value\Status::Skipped is neither a failure nor an error. ## Where the reason shows up diff --git a/ru/docs/plugins/skip.md b/ru/docs/plugins/skip.md index 556fb97..0750988 100644 --- a/ru/docs/plugins/skip.md +++ b/ru/docs/plugins/skip.md @@ -55,14 +55,15 @@ final class BillingTest -## Что не выполняется +## Что не запускается -Решение о пропуске принимается до старта теста, и в отчёт тест попадает ровно там, где начался бы его запуск. Ничто из того, что подготавливает, оборачивает или повторяет тело теста, не успевает включиться: +Решение о пропуске принимается ещё до старта теста: тест сразу получает статус \Testo\Core\Value\Status::Skipped, и на этом всё. Поэтому ничего из того, что обычно готовит, оборачивает или повторяет тело теста, не срабатывает: - Хуки \Testo\Lifecycle\BeforeTest и \Testo\Lifecycle\AfterTest не вызываются. -- Провайдеры данных, например \Testo\Data\DataProvider, не вызываются: параметризованный тест даёт **одну** запись \Testo\Core\Value\Status::Skipped, а не по одной на каждый набор данных. -- \Testo\Retry и \Testo\Repeat не начинают свой цикл. -- \Testo\Fiber\RunInFiber не запускает для него файбер, и покрытие не собирается. +- Провайдеры данных, например \Testo\Data\DataProvider, тоже не вызываются. Параметризованный тест даёт в отчёте **одну** запись \Testo\Core\Value\Status::Skipped, а не по записи на каждый набор данных. +- \Testo\Retry и \Testo\Repeat не запускают повторы. +- \Testo\Fiber\RunInFiber не создаёт файбер. +- Покрытие кода не собирается. ```php final class OrderTest @@ -85,9 +86,9 @@ final class OrderTest } ``` -Хуки уровня класса подчиняются тест-кейсу, а не тесту: \Testo\Lifecycle\BeforeClass и \Testo\Lifecycle\AfterClass выполняются, пока в тест-кейсе есть хотя бы один непропущенный тест. Если пропущены все тесты, эти хуки не вызываются, а класс так и не создаётся. +С хуками уровня класса всё иначе: они привязаны к тест-кейсу, а не к отдельному тесту. \Testo\Lifecycle\BeforeClass и \Testo\Lifecycle\AfterClass выполняются, если в тест-кейсе остался хотя бы один непропущенный тест. Если же пропущены все тесты, эти хуки не вызываются, а экземпляр класса даже не создаётся. -Прогон, состоящий из одних пропущенных тестов, считается успешным: \Testo\Core\Value\Status::Skipped — это ни падение, ни ошибка, поэтому код выхода `0`. +Прогон, в котором пропущены все тесты, считается успешным: \Testo\Core\Value\Status::Skipped не относится ни к падениям, ни к ошибкам. ## Причина в отчётах From 09520b180926d70a895d1ba205940b345f7df861 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 26 Sep 2026 17:52:43 +0400 Subject: [PATCH 4/4] docs(plugins): remove duplication from the Skip page and drop its plugin registration FAQ docs: link internal pages by their .md path so IDE navigation works Assisted-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- docs/plugins/skip.md | 26 +++++++++++--------------- ru/docs/plugins/skip.md | 26 +++++++++++--------------- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 15c6370..adb4e32 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ ru/ # Russian locale (same structure) **Adding pages:** 1. Create both `docs/page.md` (EN) and `ru/docs/page.md` (RU) 2. Add to sidebar in `.vitepress/config.mts` for both locales -3. Internal links: `./page` or `/docs/page` (no `.html`) +3. Internal links: point at the source file with its `.md` extension, relative (`./page.md`, `../guide/page.md#anchor`) or root-absolute within the locale (`/docs/page.md`, `/ru/docs/page.md`) — the IDE resolves `.md` paths for navigation, and VitePress rewrites them to clean URLs **Syncing translations:** - **CRITICAL:** When changing documentation content (adding sections, examples, explanations), ALWAYS update BOTH English and Russian versions diff --git a/docs/plugins/skip.md b/docs/plugins/skip.md index e5243e3..7201d56 100644 --- a/docs/plugins/skip.md +++ b/docs/plugins/skip.md @@ -1,22 +1,22 @@ --- 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 when to use #[Skip], SkipTest or a #[Group] filter." +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, how skipped tests and their reasons show up in reports, the SkipTest exception for skipping at run time, and when to use #[Skip], SkipTest or a #[Group] filter." --- # Skip -The plugin provides the \Testo\Skip attribute and an interceptor that mark a test as skipped before it ever starts. The test is reported as \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. The attribute can be placed on a method, function, or an entire class — in the latter case, every test in the class is skipped. +The plugin provides the \Testo\Skip attribute, which marks a test as skipped. The test is reported as \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. Marks a test, a test class or a test function as skipped without running it. -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. +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 \Testo\Bench or \Testo\Inline\TestInline target runs as usual. Close in spirit to JUnit's `@Disabled` and Rust's `#[ignore]`. -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. +Why the test is skipped. No reason by default. A given reason is appended to the result message. Skip a single test: @@ -24,7 +24,7 @@ Skip a single test: use Testo\Skip; use Testo\Test; -final class PricingTest +final class OrderTest { #[Test] #[Skip('broken by the pricing rework')] @@ -89,20 +89,20 @@ final class OrderTest Class-level hooks work differently, because they belong to the case rather than to a single test: \Testo\Lifecycle\BeforeClass and \Testo\Lifecycle\AfterClass 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. -A run consisting only of skipped tests is a success: \Testo\Core\Value\Status::Skipped is neither a failure nor an error. - -## Where the reason shows up +## 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\PricingTest::calculatesTotal is skipped via #[Skip] ==> broken by the pricing rework +Tests\Unit\OrderTest::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 terminal prints the skipped line without the message. - The compact [`--json`](../guide/cli-reference.md#json) report counts the test in its totals. +A run consisting only of skipped tests is a success: \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 \Testo\Core\Exception\SkipTest from the test body. The test is reported as \Testo\Core\Value\Status::Skipped with the exception message. @@ -121,7 +121,7 @@ public function requiresPdoMysql(): void } ``` -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: \Testo\Lifecycle\BeforeTest 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. \Testo\Skip 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. +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: \Testo\Lifecycle\BeforeTest 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. \Testo\Skip 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. ::: warning Throw \Testo\Core\Exception\SkipTest from the test body only. Thrown from an interceptor it leaves the pipeline and the test lands as \Testo\Core\Value\Status::Aborted, not \Testo\Core\Value\Status::Skipped. @@ -140,7 +140,3 @@ All three keep a test from running, but they differ in when the decision is made | `#[Skip('…')]` | in code, ahead of the run | \Testo\Core\Value\Status::Skipped, with the reason | | `throw new SkipTest('…')` | inside the test, while it runs | \Testo\Core\Value\Status::Skipped, with the message | | \Testo\Filter\Group + `--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 \Testo\Core\Value\Status::Skipped, and its \Testo\Lifecycle\BeforeTest/\Testo\Lifecycle\AfterTest hooks are still not called. What is lost is the class-level decision: a class whose tests are all skipped then runs its \Testo\Lifecycle\BeforeClass/\Testo\Lifecycle\AfterClass hooks, and a non-static hook constructs the class. -::: diff --git a/ru/docs/plugins/skip.md b/ru/docs/plugins/skip.md index 0750988..f885489 100644 --- a/ru/docs/plugins/skip.md +++ b/ru/docs/plugins/skip.md @@ -4,18 +4,18 @@ outline: [2, 3] # Пропуск тестов -Плагин предоставляет атрибут \Testo\Skip и интерцептор, которые помечают тест пропущенным ещё до его запуска. Тест попадает в отчёт со статусом \Testo\Core\Value\Status::Skipped и учитывается в итогах, а необязательная причина объясняет, почему он пропущен. Пропускайте тест, когда запускать его пока нельзя, а удалять рано: он воспроизводит ещё не починенный баг, сломан незавершённым рефакторингом или написан раньше фичи, которую проверяет. Атрибут можно повесить на метод, функцию или целый класс — в последнем случае пропускаются все тесты в классе. +Плагин предоставляет атрибут \Testo\Skip, который помечает тест пропущенным. Тест попадает в отчёт со статусом \Testo\Core\Value\Status::Skipped и учитывается в итогах, а необязательная причина объясняет, почему он пропущен. Пропускайте тест, когда запускать его пока нельзя, а удалять рано: он воспроизводит ещё не починенный баг, сломан незавершённым рефакторингом или написан раньше фичи, которую проверяет. Помечает тест, класс тестов или тестовую функцию как пропущенные, не запуская их. -Можно повесить на метод, свободную функцию или класс — на классе пропускаются все тесты тест-кейса. Атрибут наследуется от родительских классов, трейтов и переопределённых методов. Если `#[Skip]` стоит и на методе, и на классе, действует атрибут метода, и его причина заменяет причину класса. Повесить атрибут на один элемент дважды нельзя. +Можно повесить на метод, свободную функцию или класс — на классе пропускаются все тесты тест-кейса. Атрибут наследуется от родительских классов, трейтов и переопределённых методов. Если `#[Skip]` стоит и на методе, и на классе, действует атрибут метода, и его причина заменяет причину класса. Это верно и для пустой причины: если на методе стоит `#[Skip]` без аргумента, тест пропускается без причины, а причина класса не подставляется. Повесить атрибут на один элемент дважды нельзя. Атрибут действует только на обычные тесты: на не-тестовом методе он ничего не делает, а \Testo\Bench и \Testo\Inline\TestInline выполняются как обычно. По смыслу близок к `@Disabled` в JUnit и `#[ignore]` в Rust. -Почему тест пропущен. По умолчанию причины нет. Заданная причина дописывается в сообщение результата и видна в отчётах JUnit, TeamCity и HTML. +Почему тест пропущен. По умолчанию причины нет. Заданная причина дописывается в сообщение результата. Пропустить один тест: @@ -23,7 +23,7 @@ outline: [2, 3] use Testo\Skip; use Testo\Test; -final class PricingTest +final class OrderTest { #[Test] #[Skip('broken by the pricing rework')] @@ -88,20 +88,20 @@ final class OrderTest С хуками уровня класса всё иначе: они привязаны к тест-кейсу, а не к отдельному тесту. \Testo\Lifecycle\BeforeClass и \Testo\Lifecycle\AfterClass выполняются, если в тест-кейсе остался хотя бы один непропущенный тест. Если же пропущены все тесты, эти хуки не вызываются, а экземпляр класса даже не создаётся. -Прогон, в котором пропущены все тесты, считается успешным: \Testo\Core\Value\Status::Skipped не относится ни к падениям, ни к ошибкам. - -## Причина в отчётах +## Пропуск в отчётах В результат теста попадает сообщение из полного имени теста — `Класс::метод` или полное имя функции для функционального теста — и маркера `is skipped via #[Skip]`; если указана причина, она дописывается в конец: ``` -Tests\Unit\PricingTest::calculatesTotal is skipped via #[Skip] ==> broken by the pricing rework +Tests\Unit\OrderTest::calculatesTotal is skipped via #[Skip] ==> broken by the pricing rework ``` - Отчёты JUnit ([`--log-junit`](../guide/cli-reference.md#log-junit)), TeamCity ([`--teamcity`](../guide/cli-reference.md#teamcity)) и HTML показывают это сообщение. -- Терминал печатает строку пропущенного теста без него. +- Терминал печатает строку пропущенного теста без этого сообщения. - Компактный отчёт [`--json`](../guide/cli-reference.md#json) учитывает тест в итогах. +Прогон, в котором пропущены все тесты, считается успешным: \Testo\Core\Value\Status::Skipped не относится ни к падениям, ни к ошибкам. + ## Пропуск во время выполнения Иногда решение о пропуске нельзя принять заранее: тест должен сначала осмотреться и пропустить себя по тому, что обнаружит — нет расширения, недоступен сервис, фикстура оказалась пустой. Для этого бросьте \Testo\Core\Exception\SkipTest из тела теста. Тест попадёт в отчёт со статусом \Testo\Core\Value\Status::Skipped и сообщением исключения. @@ -120,7 +120,7 @@ public function requiresPdoMysql(): void } ``` -Оба механизма приводят к одному статусу, но разными путями, и это стоит помнить. Исключение бросается, когда тест уже выполняется: \Testo\Lifecycle\BeforeTest отработал, аргументы подготовлены (провайдером данных, если он есть), а класс создан, если методу нужен экземпляр. \Testo\Skip объявляется заранее и до всего этого просто не доходит. В отчётах их легко различить: у объявленного пропуска в сообщении есть маркер `is skipped via #[Skip]`. +Оба механизма приводят к одному статусу, но разными путями, и это стоит помнить. Исключение бросается, когда тест уже выполняется: \Testo\Lifecycle\BeforeTest отработал, аргументы подготовлены (провайдером данных, если он есть), а класс создан, если методу нужен экземпляр. \Testo\Skip объявляется заранее и до всего этого просто не доходит. Отличить их в отчёте просто: маркер `is skipped via #[Skip]` ставит только атрибут. ::: warning Бросайте \Testo\Core\Exception\SkipTest только из тела теста. Если бросить его из интерцептора, оно покинет пайплайн, и тест получит статус \Testo\Core\Value\Status::Aborted, а не \Testo\Core\Value\Status::Skipped. @@ -136,10 +136,6 @@ public function requiresPdoMysql(): void | Инструмент | Где принимается решение | В отчёте | |------------|-------------------------|----------| -| `#[Skip('…')]` | до запуска теста | \Testo\Core\Value\Status::Skipped, с причиной | +| `#[Skip('…')]` | в коде, до запуска | \Testo\Core\Value\Status::Skipped, с причиной | | `throw new SkipTest('…')` | внутри теста, во время выполнения | \Testo\Core\Value\Status::Skipped, с сообщением | | \Testo\Filter\Group + `--group=!slow` | при запуске раннера | не отображается | - -::: question Нужно ли регистрировать плагин? -Нет. `SkipPlugin` входит в набор плагинов по умолчанию, а атрибут сам подключает свой интерцептор. В Test Suite, настроенном без плагина, тест всё равно попадёт в отчёт со статусом \Testo\Core\Value\Status::Skipped, и \Testo\Lifecycle\BeforeTest/\Testo\Lifecycle\AfterTest для него по-прежнему не вызовутся. Теряется только решение на уровне класса: у класса, все тесты которого пропущены, выполнятся \Testo\Lifecycle\BeforeClass/\Testo\Lifecycle\AfterClass, а ради нестатического хука будет создан экземпляр класса. -:::