Фильтрация тестов
Этот документ описывает бизнес-логику фильтрации тестов в Testo.
Обзор
Testo предоставляет гибкую систему фильтрации, которая работает в несколько этапов для последовательного сужения набора тестов. Фильтрацией можно управлять программно через класс Filter или автоматически из аргументов CLI.
Класс Filter
Класс Testo\Common\Filter представляет собой неизменяемый DTO, содержащий критерии фильтрации тестов:
use Testo\Common\Filter;
$filter = new Filter(
suites: ['Unit', 'Integration'],
names: ['UserTest::testLogin', 'testAuthentication'],
paths: ['tests/Unit/*', 'tests/Integration/*'],
);Свойства
testSuites: list<non-empty-string>
- Названия Test Suite для фильтрации
- Используется на Этапе 1 для определения, какие Test Suite загружать
names: list<non-empty-string>
- Имена классов, методов или функций для фильтрации
- Поддерживает три формата:
- Метод:
ClassName::methodNameилиNamespace\ClassName::methodName - FQN:
Namespace\ClassNameилиNamespace\functionName - Фрагмент:
methodName,functionNameилиShortClassName
- Метод:
- Опциональные индексы DataProvider:
name:providerIndex:datasetIndex- Предоставляет индексы для модуля провайдера данных
- Индексы начинаются с 0 и независимы от меток наборов данных
datasetIndexопционален (можно опустить, чтобы передать только индекс provider)- Примеры:
UserTest::testLogin:0,testAuth:1:3,UserTest:0
paths: list<non-empty-string>
- Пути к файлам или директориям для фильтрации
- Поддерживает glob-паттерны:
*,?,[abc]
Использование с Application
Объект Filter может быть передан в Application::run():
use Testo\Application;
use Testo\Common\Filter;
$app = Application::createFromInput(/* ... */);
$filter = new Filter(
suites: ['Unit'],
names: ['UserTest'],
);
$result = $app->run($filter);При запуске из CLI объект Filter автоматически заполняется из аргументов команды через Filter::fromScope().
Логика комбинирования фильтров
Одинаковый тип: логика ИЛИ
Несколько значений внутри одного типа фильтра комбинируются логикой ИЛИ:
names: ['test1', 'test2']→ совпадает, если имя test1 ИЛИ test2paths: ['path1', 'path2']→ совпадает, если путь path1 ИЛИ path2suites: ['Unit', 'Integration']→ совпадает, если Test Suite - Unit ИЛИ Integration
Разные типы: логика И
Разные типы фильтров комбинируются логикой И:
names: ['test1'], suites: ['Unit']→ совпадает, если имя test1 И Test Suite - Unitnames: ['UserTest'], paths: ['tests/Unit/*']→ совпадает, если имя UserTest И путь соответствует tests/Unit/*
Формула: AND(OR(names), OR(paths), OR(suites))
Пример:
$filter = new Filter(
names: ['test1', 'test2'], // test1 ИЛИ test2
paths: ['path1', 'path2'], // path1 ИЛИ path2
suites: ['Unit', 'Critical'], // Unit ИЛИ Critical
);
// Результат: (test1 ИЛИ test2) И (path1 ИЛИ path2) И (Unit ИЛИ Critical)Поведение фильтра по именам
Поведение фильтрации по именам реализовано в FilterInterceptor и зависит от формата имени:
Формат метода (ClassName::methodName)
При использовании формата метода с разделителем :::
- Совпадает только указанный метод
- Другие методы в том же классе исключаются
- Результат: Тестовый кейс с только указанным методом
Пример:
$filter = new Filter(names: ['UserTest::testLogin']);
// Результат: класс UserTest только с методом testLoginФормат FQN или фрагмента
При использовании FQN (с \) или простого фрагмента (без разделителей):
Случай 1: Имя класса совпадает
- Результат: Весь тестовый кейс со всеми методами
Случай 2: Имя класса не совпадает
- Система проверяет отдельные методы/функции
- Результат: Тестовый кейс с только совпавшими методами
- Если методы не совпадают: Тестовый кейс пропускается
Примеры:
// FQN - совпадает весь класс
$filter = new Filter(names: ['Tests\Unit\UserTest']);
// Результат: класс UserTest со всеми методами
// Фрагмент - совпадает весь класс
$filter = new Filter(names: ['UserTest']);
// Результат: класс UserTest со всеми методами
// Фрагмент - совпадает метод в любом классе
$filter = new Filter(names: ['testLogin']);
// Результат: Все классы с методом testLogin, каждый только с этим методомИндексы DataProvider
Когда тесты используют провайдер данных, имена могут включать индексы provider и dataset с использованием разделителя двоеточие. Эти индексы становятся доступны модулю провайдера данных.
Формат: name:providerIndex:datasetIndex
- Индексы - целые числа, начинающиеся с 0, независимые от меток наборов данных
datasetIndexопционален - опустите, чтобы передать только индекс provider- Работает со всеми форматами имен (Метод, FQN, Фрагмент)
Примеры:
// Передать индекс provider #0
$filter = new Filter(names: ['UserTest::testLogin:0']);
// Передать индекс provider #0 и индекс dataset #1
$filter = new Filter(names: ['UserTest::testLogin:0:1']);
// Передать индекс provider #1 и индекс dataset #3, совпадая с любым тестом с именем 'testAuth'
$filter = new Filter(names: ['testAuth:1:3']);
// Передать индекс provider #0 для всего класса UserTest
$filter = new Filter(names: ['UserTest:0']);Конвейер фильтрации
Фильтрация работает в пять этапов:
Этап 1: Фильтр Test Suite (уровень конфигурации)
Входные данные: Filter::$suites
- Фильтрует Test Suite по названиям
- Каждый Test Suite определяет расположение и паттерны сканирования файлов
- Определяет начальный набор директорий для сканирования
- Несколько Test Suite используют логику ИЛИ
Этап 2: Фильтр путей (уровень Finder)
Входные данные: Filter::$paths
- Применяется на уровне поиска файлов во время сканирования директорий
- Использует glob-паттерны для сопоставления путей файлов
- Поддерживает подстановочные символы:
*,?,[abc] - Несколько путей используют логику ИЛИ
- Возвращает список файлов для обработки
Этап 3: Фильтр файлов (уровень Tokenizer)
Входные данные: Filter::$namesРеализация: FilterInterceptor::locateFile()
- Предварительная фильтрация тестовых файлов перед загрузкой для рефлексии
- Использует легковесную токенизацию вместо полной рефлексии
- Проверяет, содержит ли файл какие-либо совпадающие классы, методы или функции
- Пропускает файлы, которые не соответствуют ни одному паттерну
- Несколько имен используют логику ИЛИ
Этап 4: Фильтр тестов (уровень рефлексии)
Входные данные: Filter::$namesРеализация: FilterInterceptor::locateTestCases()
- Фильтрует отдельные тестовые кейсы и методы после рефлексии
- Реализует иерархическую фильтрацию:
- Для формата метода (
::) - фильтрует только конкретные методы - Для формата FQN/фрагмента - сначала проверяет имя класса, затем методы
- Для формата метода (
- Извлекает индексы DataProvider и связывает их с совпавшими тестами
- Возвращает отфильтрованные определения тестовых кейсов, готовые к выполнению
Этап 5: Внедрение DataProvider (уровень выполнения)
Входные данные: Индексы DataProvider с Этапа 4 Реализация: FilterInterceptor::runTest()
- Внедряет
DataPointerв метаданные теста перед выполнением - Делает
DataPointerдоступным для других interceptor'ов - Если индексы не указаны:
DataPointerне внедряется
Сопоставление паттернов
FilterInterceptor использует сопоставление по границам целых слов с помощью регулярных выражений:
private static function has(string $needle, string $haystack): bool
{
return \preg_match('/\\b' . \preg_quote($needle, '/') . '\\b$/', $haystack) === 1;
}Поведение:
Userсовпадает сApp\User✓UserНЕ совпадает сApp\UserManager✗testсовпадает сtestMethod✓testНЕ совпадает сlatestMethod✗