Repeat
Плагин предоставляет атрибут #[Repeat]#[Repeat(int $times = 2, int $maxFailures = 0, bool $markFlaky = true)]Запускает тест фиксированное число раз и решает итог по порогу падений. и интерцептор, которые запускают тест фиксированное количество раз подряд. В отличие от #[Retry]#[Retry(int $maxAttempts = 3, bool $markFlaky = true)]Объявляет политику повторного запуска теста при падении., Repeat не смотрит, прошёл ли первый запуск — он всегда выполняет полный цикл. Используйте его, когда нужно убедиться, что тест стабилен на множестве прогонов, или проверить логику с возможным скрытым недетерминизмом. Атрибут можно повесить на метод, функцию или целый класс — в последнем случае политика применяется ко всем тестам внутри.
Плагин не требует настройки.
#[Repeat]
Запускает тест фиксированное число раз и решает итог по порогу падений.
#[Repeat(int $times = 2, int $maxFailures = 0, bool $markFlaky = true)]Работает с любым типом тестов: обычными, встроенными, бенчмарками. При размещении на классе (Test Case) применяется ко всем тестам внутри. Если очередной прогон получает статус Skipped, Cancelled или Aborted — цикл сразу прекращается и этот статус становится итоговым.
Параметры:
$times- Общее число запусков теста — не дополнительные повторы поверх первого.
times: 1— тест запускается один раз,times: 3— три раза. Минимум1. По смыслу совпадает сrepeat(times)в Kotlin и@RepeatedTestв JUnit. $maxFailures- Сколько упавших запусков допустимо до того, как весь цикл будет считаться упавшим. По умолчанию
0— любое падение прерывает цикл и проваливает тест. $markFlaky- Помечать ли тест как нестабильный (flaky), если хотя бы один запуск упал, но число падений уложилось в
$maxFailures. По умолчаниюtrue.
Примеры:
Прогнать один и тот же тест 5 раз — любое падение валит весь тест:
#[Repeat(times: 5)]
public function orderCalculationIsStable(): void
{
$order = new Order([new Item('A', 10), new Item('B', 20)]);
Assert::same(30, $order->total());
}Допустить до двух падений из десяти — полезно, когда подсистема заведомо немного шумит:
#[Repeat(times: 10, maxFailures: 2)]
public function externalServiceReturnsExpectedShape(): void
{
$response = HttpClient::get('https://api.example.com/users/1');
Assert::same(200, $response->statusCode);
}На классе — все тесты внутри получают политику повторов:
#[Repeat(times: 3)]
final class StabilityTest
{
public function firstCheck(): void { /* ... */ }
public function secondCheck(): void { /* ... */ }
}Допустимые падения
По умолчанию maxFailures: 0 означает, что цикл прерывается на первом же упавшем запуске — то, что нужно, когда каждый запуск обязан быть зелёным. Если поднять порог, Repeat превращается в мягкую проверку стабильности: тест считает, сколько запусков из N упало, и проваливается только тогда, когда счётчик переваливает за лимит.
Если цикл уложился в порог, но хотя бы один запуск всё-таки упал, тест получает статус Flaky (если не передать markFlaky: false). Так редкие падения становятся видны в отчётах, а не молча прячутся за зелёным результатом.
// Пройдёт, если упало не больше 1 из 5 запусков. С markFlaky: false тест просто зелёный.
#[Repeat(times: 5, maxFailures: 1, markFlaky: false)]
public function tolerantStabilityCheck(): void { /* ... */ }Комбинирование с Retry
Repeat и #[Retry]#[Retry(int $maxAttempts = 3, bool $markFlaky = true)]Объявляет политику повторного запуска теста при падении. можно использовать вместе — они ортогональны:
- Repeat прогоняет тест N раз, чтобы проверить стабильность.
- Retry перезапускает один упавший прогон, чтобы пережить случайную помеху.
Когда оба атрибута стоят рядом, Repeat работает внутри Retry: каждая попытка Retry выполняет полный цикл запусков, и если число упавших запусков в цикле превысит maxFailures, Retry запускает новую попытку для всего цикла целиком.
#[Retry(maxAttempts: 3)]
#[Repeat(times: 5, maxFailures: 1)]
public function noisyButImportantCheck(): void { /* ... */ }Здесь тест выполняется 5 раз за попытку; если в цикле упало больше одного запуска, цикл считается провальным и Retry запускает новую попытку (всего до 3-х).
Repeat или Retry
Плагины выглядят похоже, но решают противоположные задачи. Выбирайте тот, который соответствует вопросу, который вы задаёте к тесту:
- Берите #[Retry]
#[Retry(int $maxAttempts = 3, bool $markFlaky = true)]Объявляет политику повторного запуска теста при падении., когда достаточно, чтобы тест прошёл хотя бы раз, и хочется простить одиночную случайную помеху. Retry останавливается, как только тест стал зелёным, поэтому удачный первый запуск ничего не стоит. - Берите #[Repeat]
#[Repeat(int $times = 2, int $maxFailures = 0, bool $markFlaky = true)]Запускает тест фиксированное число раз и решает итог по порогу падений., когда тест должен проходить всегда (или почти всегда), и хочется доказать стабильность. Repeat прогоняет полный цикл, даже если первый запуск был зелёным.
#[Retry]#[Retry(int $maxAttempts = 3, bool $markFlaky = true)]Объявляет политику повторного запуска теста при падении. | #[Repeat]#[Repeat(int $times = 2, int $maxFailures = 0, bool $markFlaky = true)]Запускает тест фиксированное число раз и решает итог по порогу падений. | |
|---|---|---|
| Назначение | Пережить случайное падение | Проверить стабильность на множестве прогонов |
| Останавливается на первом успехе | Да | Нет — всегда $times запусков |
| Толерантность к падениям | Неявно: достаточно одного успеха | Явно через $maxFailures |
| Типичный сценарий | Внешний API, сеть, медленный CI | Гонки, логика, зависящая от времени, прогрев |
| Стоимость на здоровом тесте | Один прогон | N прогонов каждый раз |
Что будет, если один из повторов пропущен или прерван?
Цикл сразу останавливается, и тест получает соответствующий статус — Skipped, Cancelled или Aborted. В $maxFailures засчитываются только завершённые прогоны (passed или failed).