Skip to content
...

Фильтрация тестов

Этот документ описывает бизнес-логику фильтрации тестов в Testo.

Обзор

Testo предоставляет гибкую систему фильтрации, которая работает в несколько этапов для последовательного сужения набора тестов. Фильтрацией можно управлять программно через класс Filter или автоматически из аргументов CLI.

Класс Filter

Класс Testo\Common\Filter представляет собой неизменяемый DTO, содержащий критерии фильтрации тестов:

php
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():

php
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 ИЛИ test2
  • paths: ['path1', 'path2'] → совпадает, если путь path1 ИЛИ path2
  • suites: ['Unit', 'Integration'] → совпадает, если Test Suite - Unit ИЛИ Integration

Разные типы: логика И

Разные типы фильтров комбинируются логикой И:

  • names: ['test1'], suites: ['Unit'] → совпадает, если имя test1 И Test Suite - Unit
  • names: ['UserTest'], paths: ['tests/Unit/*'] → совпадает, если имя UserTest И путь соответствует tests/Unit/*

Формула: AND(OR(names), OR(paths), OR(suites))

Пример:

php
$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)

При использовании формата метода с разделителем :::

  • Совпадает только указанный метод
  • Другие методы в том же классе исключаются
  • Результат: Тестовый кейс с только указанным методом

Пример:

php
$filter = new Filter(names: ['UserTest::testLogin']);
// Результат: класс UserTest только с методом testLogin

Формат FQN или фрагмента

При использовании FQN (с \) или простого фрагмента (без разделителей):

Случай 1: Имя класса совпадает

  • Результат: Весь тестовый кейс со всеми методами

Случай 2: Имя класса не совпадает

  • Система проверяет отдельные методы/функции
  • Результат: Тестовый кейс с только совпавшими методами
  • Если методы не совпадают: Тестовый кейс пропускается

Примеры:

php
// 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, Фрагмент)

Примеры:

php
// Передать индекс 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 использует сопоставление по границам целых слов с помощью регулярных выражений:

php
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